diff --git a/.clangd b/.clangd new file mode 100644 index 0000000000..3fc64da811 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +Diagnostics: + Suppress: 'pp_including_mainfile_in_preamble' diff --git a/.eslintrc b/.eslintrc index 5aa84678b6..9623dd6e93 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,9 +1,9 @@ { "parserOptions": { - "ecmaVersion": 2020 + "ecmaVersion": "latest", + "sourceType": "module" }, "env": { "es6": true - }, - "sourceType": "module" + } } diff --git a/README.md b/README.md index de76ab5220..9b2e66dc41 100644 --- a/README.md +++ b/README.md @@ -21,40 +21,54 @@ The `Socket runtime CLI` outputs hybrid native-web apps that combine your code w ### Compatibility Matrix +Socket Supports both ESM and CommonJS + > [!NOTE] -> Socket supports many of the Node.js APIs. It is **NOT** a drop in replacement for Node.js, nor will it ever be since socket is for building software and node.js is for building servers. Below is a high level overview of partially supported APIs and modules. - -| Module | Node.js | Socket | -| ----------------- | ---------- | --------- | -| assert | ✔︎ | ⏱ | -| async/await | ✔︎ | ✔︎ | -| buffer | ✔︎ | ✔︎️ | -| child_process | ✔︎ | ✔︎️ \* | -| console | ✔︎ | ✔︎ | -| crypto | ✔︎ | ✔︎ \* | -| dgram | ✔︎ | ✔︎️ | -| dns | ✔︎ | ✔︎️ | -| os | ✔︎ | ✔︎️ | -| encoding | ✔︎ | ✔︎ | -| events | ✔︎ | ✔︎ | -| fetch | ✔︎ | ✔︎ | -| fs/promises | ✔︎ | ✔︎ | -| fs | ✔︎ | ✔︎ | -| path | ✔︎ | ✔︎ | -| process | ✔︎ | ✔︎ | -| streams | ✔︎ | ✔︎ | -| string_decoder | ✔︎ | ⏱ | -| test | ✔︎ | ✔︎️ | -| timers | ✔︎ | ⏱ | -| uuid | ✔︎ | ⏱ | -| vm | ✔︎ | ✔︎ | -| ESM | ✔︎ | ✔︎ | -| CJS | ✔︎ | ✔︎ | -| URL | ✔︎ | ✔︎ | - -_⏱ = planned support_ -_\* = Supported but Works differently; may be refactored to match the nodejs API_ -_\*\* = Use fetch instead_ +> Socket supports many of the Node.js APIs. It is **NOT** a drop in replacement for Node.js, it most likely wont ever be since Socket is for building software and node.js is for building servers. Below is a high level overview of fully or partially supported APIs and modules. + +| Module | Node.js | Socket | +| ----------------- | ---------- | -------- | +| assert | √ | ⏱ | +| async_hooks | √ | √ | +| buffer | √ | √ | +| child_process | √ | √ | +| cluster | √ | § | +| console | √ | √ | +| crypto | √ | √ \* | +| dgram (udp) | √ | √ | +| diagnostics_channel | √ | √ | +| dns | √ | √ | +| encoding | √ | √ | +| events | √ | √ | +| fetch | √ | √ | +| fs | √ | √ | +| fs/promises | √ | √ | +| http | √ | √ | +| http2 | √ | √ | +| https | √ | √ | +| inspector | √ | ⏱ | +| module | √ | √ | +| net | √ | ⏱ | +| os | √ | √ | +| path | √ | √ | +| process | √ | √ | +| streams | √ | √ | +| string_decoder | √ | √ | +| test | √ | √ | +| timers | √ | √ | +| tls | √ | ⏱ | +| tty | √ | √ | +| URL | √ | √ | +| uuid | √ | ⏱ ☀︎ | +| vm | √ | √ | +| worker_threads | √ | √ | + +| Symbol | Meaning | +| :----: | :-------------------------------------------------------------------------- | +| ⏱ | Planned support | +| § | Not Relevant or not necessary since socket doesn't require high-concurrency | +| \* | Supported but Works differently; may be refactored to match the nodejs API | +| ☀︎ | Use `crypto.randomUUID()` instead | ### FAQ @@ -67,7 +81,7 @@ Check the FAQs on our [Website](https://socketsupply.co/guides/#faq) to learn mo `Create Socket App` is similar to React's `Create React App`, we provide a few basic boilerplates and some strong opinions so you can get coding on a production-quality app as quickly as possible. Please check [create-socket-app Repo](https://github.com/socketsupply/create-socket-app) to get started and to learn more. You can also check our `Examples` in the [Examples Repo](https://github.com/socketsupply/socket-examples). - +§§§ ### Documentation @@ -75,6 +89,28 @@ The full documentation can be found on the [Socket Runtime](https://socketsupply The `Socket Runtime` documentation covers Socket APIs, includes examples, multiple guides (`Apple`, `Desktop`, and `Mobile`), `P2P` documentation, and more. +### Development + +If you are developing socket, and you want your apps to point to your dev branch. + + +#### Step 1 + +```bash +cd ~/projects/socket # navigate into the location where you cloned this repo +./bin/uninstall.sh # remove existing installation +npm rm -g @socketsupply/socket # remove any global links or prior global npm installs +npm run relink # this will call `./bin/publish-npm-modules.sh --link` (accepts NO_ANDROID=1, NO_IOS=1, and DEBUG=1) +``` + +#### Step 2 + +```bash +cd ~/projects/ # navigate into your project (replacing with whatever it's actually called +npm link @socketsupply/socket # link socket and you'll be ready to go. +``` + + ### Testing `Socket` provides a built-in `test runner` similar to `node:test` which outputs the test results in [TAP](https://testanything.org/) format. diff --git a/VERSION.txt b/VERSION.txt index 7d8568351b..a3a7988754 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.5.4 +0.6.0-next diff --git a/api/CONFIG.md b/api/CONFIG.md index 954d2c2236..b18826d7f0 100644 --- a/api/CONFIG.md +++ b/api/CONFIG.md @@ -1,14 +1,16 @@ -# Configuration basics +# Configuration +## Overview -The configuration file is a simple INI `socket.ini` file in the root of the project. -The file is read on startup and the values are used to configure the project. -Sometimes it's useful to overide the values in `socket.ini` or keep some of the values local (e.g. `[ios] simulator_device`) -or secret (e.g. `[ios] codesign_identity`, `[ios] provisioning_profile`, etc.) -This can be done by creating a file called `.sscrc` in the root of the project. -It is possible to override both Command Line Interface (CLI) and Configuration File (INI) options. + +The configuration file is an INI file (`socket.ini`) in the root of every Socket runtime project. +The file is read at compile time. Sometimes it's useful to overide its values or keep some of the +values locally, only on your computer (e.g. `[ios] simulator_device`) or secrets +(e.g. `[ios] codesign_identity`, `[ios] provisioning_profile`, etc.); this can be done by +creating a file called `.sscrc` in the root of the project. It is possible to override both +Command Line Interface (CLI) and Configuration File (INI) options. Example: @@ -33,7 +35,7 @@ platform = ios ; override the `ssc build --platform` CLI option [settings.ios] ; override the `[ios]` section in `socket.ini` codesign_identity = "iPhone Developer: John Doe (XXXXXXXXXX)" -distribution_method = "ad-hoc" +distribution_method = "release-testing" provisioning_profile = "johndoe.mobileprovision" simulator_device = "iPhone 15" ``` @@ -45,7 +47,7 @@ simulator_device = "iPhone 15" Use the full path instead. -# `build` +### `build` Key | Default Value | Description :--- | :--- | :--- @@ -58,31 +60,35 @@ name | | The name of the program and executable to be output. Can't contain sp output | "build" | The binary output path. It's recommended to add this path to .gitignore. script | | The build script. It runs before the `[build] copy` phase. -# `build.script` +### `build.script` Key | Default Value | Description :--- | :--- | :--- forward_arguments | false | If true, it will pass build arguments to the build script. WARNING: this could be deprecated in the future. -# `build.watch` +### `build.watch` Key | Default Value | Description :--- | :--- | :--- sources[] | | Configure your project to watch for sources that could change when running `ssc`. Could be a string or an array of strings -# `webview` +### `webview` Key | Default Value | Description :--- | :--- | :--- -headers[] | "" | +root | "/" | Make root open index.html +default_index | "" | Set default 'index.html' path to open for implicit routes +watch | false | Tell the webview to watch for changes in its resources +headers[] | "" | Custom headers injected on all webview routes -# `webview.watch` +### `webview.watch` Key | Default Value | Description :--- | :--- | :--- reload | true | Configure webview to reload when a file changes +service_worker_reload_timeout | 500 | Timeout in milliseconds to wait for service worker to reload before reloading webview -# `webview.navigator.mounts` +### `webview.navigator.mounts` Key | Default Value | Description :--- | :--- | :--- @@ -90,7 +96,13 @@ $HOST_HOME/directory-in-home-folder/ | | $HOST_CONTAINER/directory-app-container/ | | $HOST_PROCESS_WORKING_DIRECTORY/directory-in-app-process-working-directory/ | | -# `permissions` +### `webview.navigator.policies` + +Key | Default Value | Description +:--- | :--- | :--- +allowed[] | | + +### `permissions` Key | Default Value | Description :--- | :--- | :--- @@ -107,13 +119,13 @@ allow_data_access | true | Allow/Disallow data access in application allow_airplay | true | Allow/Disallow AirPlay access in application (macOS/iOS) only allow_hotkeys | true | Allow/Disallow HotKey binding registration (desktop only) -# `debug` +### `debug` Key | Default Value | Description :--- | :--- | :--- flags | | Advanced Compiler Settings for debug purposes (ie C++ compiler -g, etc). -# `meta` +### `meta` Key | Default Value | Description :--- | :--- | :--- @@ -128,11 +140,10 @@ title | | The title of the app used in metadata files. This is NOT a window ti type | "" | Builds an extension when set to "extension". version | | A string that indicates the version of the application. It should be a semver triple like 1.2.3. Defaults to 1.0.0. -# `android` +### `android` Key | Default Value | Description :--- | :--- | :--- -icon | | The icon to use for identifying your app on Android. aapt_no_compress | | Extensions of files that will not be stored compressed in the APK. enable_standard_ndk_build | | Enables gradle based ndk build rather than using external native build (standard ndk is the old slow way) main_activity | | Name of the MainActivity class. Could be overwritten by custom native code. @@ -142,85 +153,99 @@ native_cflags | | Used for adding custom source files and related compiler att native_sources | | native_makefile | | sources | | +icon | | The icon to use for identifying your app on Android. +icon_sizes | | The various sizes and scales of the icons to create, required minimum are listed by default. -# `ios` +### `ios` Key | Default Value | Description :--- | :--- | :--- codesign_identity | | signing guide: https://socketsupply.co/guides/#ios-1 -distribution_method | | Describes how Xcode should export the archive. Available options: app-store, package, ad-hoc, enterprise, development, and developer-id. +distribution_method | | Describes how Xcode should export the archive. Available options: app-store, package, release-testing, enterprise, development, and developer-id. provisioning_profile | | A path to the provisioning profile used for signing iOS app. simulator_device | | which device to target when building for the simulator. nonexempt_encryption | false | Indicate to Apple if you are using encryption that is not exempt. +icon | | The icon to use for identifying your app on iOS. +icon_sizes | | The various sizes and scales of the icons to create, required minimum are listed by default. -# `linux` +### `linux` Key | Default Value | Description :--- | :--- | :--- categories | | Helps to make your app searchable in Linux desktop environments. cmd | | The command to execute to spawn the "back-end" process. icon | | The icon to use for identifying your app in Linux desktop environments. +icon_sizes | | The various sizes and scales of the icons to create, required minimum are listed by default. -# `mac` +### `mac` Key | Default Value | Description :--- | :--- | :--- category | | A category in the App Store cmd | | The command to execute to spawn the "back-end" process. -icon | | The icon to use for identifying your app on MacOS. codesign_identity | | TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates codesign_paths | | Additional paths to codesign minimum_supported_version | "13.0.0" | Minimum supported MacOS version +window_control_offsets | | If titlebar_style is "hiddenInset", this will determine the x and y offsets of the window controls (traffic lights). +icon | | The icon to use for identifying your app on MacOS. +icon_sizes | | The various sizes and scales of the icons to create, required minimum are listed by default. -# `native` +### `native` Key | Default Value | Description :--- | :--- | :--- files | | Files that should be added to the compile step. headers | | Extra Headers -# `win` +### `win` Key | Default Value | Description :--- | :--- | :--- cmd | | The command to execute to spawn the “back-end” process. -icon | | The icon to use for identifying your app on Windows. logo | | The icon to use for identifying your app on Windows, relative to copied path resources pfx | | A relative path to the pfx file used for signing. +icon | | The signing information needed by the appx api. The icon to use for identifying your app on Windows. +icon_sizes | | The various sizes and scales of the icons to create, required minimum are listed by default. -# `window` +### `window` Key | Default Value | Description :--- | :--- | :--- height | | The initial height of the first window in pixels or as a percentage of the screen. width | | The initial width of the first window in pixels or as a percentage of the screen. +backgroundColorDark | "" | The initial color of the window in dark mode. If not provided, matches the current theme. +backgroundColorLight | "" | The initial color of the window in light mode. If not provided, matches the current theme. +titlebar_style | "" | Determine if the titlebar style (hidden, hiddenInset) max_height | 100% | Maximum height of the window in pixels or as a percentage of the screen. max_width | 100% | Maximum width of the window in pixels or as a percentage of the screen. min_height | 0 | Minimum height of the window in pixels or as a percentage of the screen. min_width | 0 | Minimum width of the window in pixels or as a percentage of the screen. -resizable | true | If the window is resizable or not. -frameless | false | If the window has a title bar or not. -utility | false | If the window is utility window or not. +frameless | false | Determines if the window has a title bar and border. +resizable | true | Determines if the window is resizable. +maximizable | true | Determines if the window is maximizable. +minimizable | true | Determines if the window is minimizable. +closable | true | Determines if the window is closable. +utility | false | Determines the window is utility window. -# `window.alert` +### `window.alert` Key | Default Value | Description :--- | :--- | :--- title | | The title that appears in the 'alert', 'prompt', and 'confirm' dialogs. If this value is not present, then the application title is used instead. Currently only supported on iOS/macOS. defalut value = "" -# `application` +### `application` Key | Default Value | Description :--- | :--- | :--- agent | false | If agent is set to true, the app will not display in the tab/window switcher or dock/task-bar etc. Useful if you are building a tray-only app. -# `tray` +### `tray` Key | Default Value | Description :--- | :--- | :--- icon | | The icon to be displayed in the operating system tray. On Windows, you may need to use ICO format. defalut value = "" -# `headless` +### `headless` Key | Default Value | Description :--- | :--- | :--- diff --git a/api/README.md b/api/README.md index 6017d619ca..fa4527b133 100644 --- a/api/README.md +++ b/api/README.md @@ -1,7 +1,21 @@ + +# [Buffer](https://github.com/socketsupply/socket/blob/master/api/buffer.js) + +Buffer module is a [third party](https://github.com/feross/buffer) vendor module provided by Feross Aboukhadijeh and other contributors (MIT License). + +External docs: https://nodejs.org/api/buffer.html + + +# [Events](https://github.com/socketsupply/socket/blob/master/api/events.js) + +Events module is a [third party](https://github.com/browserify/events/blob/main/events.js) module provided by Browserify and Node.js contributors (MIT License). + +External docs: https://nodejs.org/api/events.html + -# [Application](https://github.com/socketsupply/socket/blob/master/api/application.js#L13) +# [application](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L14) Provides Application level methods @@ -11,7 +25,36 @@ import { createWindow } from 'socket:application' ``` -## [`getCurrentWindowIndex()`](https://github.com/socketsupply/socket/blob/master/api/application.js#L27) +## [MAX_WINDOWS](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L62) + +This is a `VariableDeclaration` named `MAX_WINDOWS` in `api/application.js`, it's exported but undocumented. + + +## [ApplicationWindowList](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L64) + +This is a `ClassDeclaration` named `ApplicationWindowList` in `api/application.js`, it's exported but undocumented. + + +## [`addEventListener(type, listener, |boolean)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L191) + +Add an application event `type` callback `listener` with `options`. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| type | string | | false | | +| listener | function(Event \| MessageEvent \| CustomEvent \| ApplicationURLEvent): boolean | | false | | +| |boolean | { once?: boolean | } options | false | | + +## [`removeEventListener(type, listener)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L200) + +Remove an application event `type` callback `listener` with `options`. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| type | string | | false | | +| listener | function(Event \| MessageEvent \| CustomEvent \| ApplicationURLEvent): boolean | | false | | + +## [`getCurrentWindowIndex()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L208) Returns the current window index @@ -19,16 +62,26 @@ Returns the current window index | :--- | :--- | :--- | | Not specified | number | | -## [`createWindow(opts)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L50) +## [`createWindow(opts)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L243) Creates a new window and returns an instance of ApplicationWindow. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | opts | object | | false | an options object | -| opts.index | number | | false | the index of the window | -| opts.path | string | | false | the path to the HTML file to load into the window | -| opts.title | string | | true | the title of the window | +| opts.aspectRatio | string | | true | a string (split on ':') provides two float values which set the window's aspect ratio. | +| opts.closable | boolean | | true | deterime if the window can be closed. | +| opts.minimizable | boolean | | true | deterime if the window can be minimized. | +| opts.maximizable | boolean | | true | deterime if the window can be maximized. | +| opts.margin | number | | true | a margin around the webview. (Private) | +| opts.radius | number | | true | a radius on the webview. (Private) | +| opts.index | number | | false | the index of the window. | +| opts.path | string | | false | the path to the HTML file to load into the window. | +| opts.title | string | | true | the title of the window. | +| opts.titlebarStyle | string | | true | determines the style of the titlebar (MacOS only). | +| opts.windowControlOffsets | string | | true | a string (split on 'x') provides the x and y position of the traffic lights (MacOS only). | +| opts.backgroundColorDark | string | | true | determines the background color of the window in dark mode. | +| opts.backgroundColorLight | string | | true | determines the background color of the window in light mode. | | opts.width | number \| string | | true | the width of the window. If undefined, the window will have the main window width. | | opts.height | number \| string | | true | the height of the window. If undefined, the window will have the main window height. | | opts.minWidth | number \| string | 0 | true | the minimum width of the window | @@ -38,14 +91,24 @@ Creates a new window and returns an instance of ApplicationWindow. | opts.resizable | boolean | true | true | whether the window is resizable | | opts.frameless | boolean | false | true | whether the window is frameless | | opts.utility | boolean | false | true | whether the window is utility (macOS only) | -| opts.canExit | boolean | false | true | whether the window can exit the app | +| opts.shouldExitApplicationOnClose | boolean | false | true | whether the window can exit the app | | opts.headless | boolean | false | true | whether the window will be headless or not (no frame) | +| opts.userScript | string | null | true | A user script that will be injected into the window (desktop only) | +| opts.protocolHandlers | string | | true | An array of protocol handler schemes to register with the new window (requires service worker) | | Return Value | Type | Description | | :--- | :--- | :--- | | Not specified | Promise | | -## [`getScreenSize()`](https://github.com/socketsupply/socket/blob/master/api/application.js#L115) +### [`radius()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L270) + + + +### [`margin()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L275) + + + +## [`getScreenSize()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L363) Returns the current screen size. @@ -53,7 +116,7 @@ Returns the current screen size. | :--- | :--- | :--- | | Not specified | Promise<{ width: number, height: number | >} | -## [`getWindows(indices)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L141) +## [`getWindows(indices)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L394) Returns the ApplicationWindow instances for the given indices or all windows if no indices are provided. @@ -63,9 +126,9 @@ Returns the ApplicationWindow instances for the given indices or all windows if | Return Value | Type | Description | | :--- | :--- | :--- | -| Not specified | Promise> | | +| Not specified | Promise | | -## [`getWindow(index)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L171) +## [`getWindow(index)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L448) Returns the ApplicationWindow instance for the given index @@ -77,7 +140,7 @@ Returns the ApplicationWindow instance for the given index | :--- | :--- | :--- | | Not specified | Promise | the ApplicationWindow instance or null if the window does not exist | -## [`getCurrentWindow()`](https://github.com/socketsupply/socket/blob/master/api/application.js#L181) +## [`getCurrentWindow()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L458) Returns the ApplicationWindow instance for the current window. @@ -85,7 +148,7 @@ Returns the ApplicationWindow instance for the current window. | :--- | :--- | :--- | | Not specified | Promise | | -## [`exit(code)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L190) +## [`exit(code)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L467) Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. @@ -97,7 +160,7 @@ Quits the backend process and then quits the render process, the exit code used | :--- | :--- | :--- | | Not specified | Promise | | -## [`setSystemMenu(options)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L287) +## [`setSystemMenu(options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L564) Set the native menu for the app. @@ -192,11 +255,11 @@ Set the native menu for the app. | :--- | :--- | :--- | | Not specified | Promise | | -## [`setTrayMenu()`](https://github.com/socketsupply/socket/blob/master/api/application.js#L294) +## [`setTrayMenu()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L571) An alias to setSystemMenu for creating a tary menu -## [`setSystemMenuItemEnabled(value)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L303) +## [`setSystemMenuItemEnabled(value)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L580) Set the enabled state of the system menu. @@ -208,23 +271,31 @@ Set the enabled state of the system menu. | :--- | :--- | :--- | | Not specified | Promise | | -## [runtimeVersion](https://github.com/socketsupply/socket/blob/master/api/application.js#L311) +## [`isPaused()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L588) + +Predicate function to determine if application is in a "paused" state. + +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | boolean | | + +## [runtimeVersion](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L596) Socket Runtime version. -## [debug](https://github.com/socketsupply/socket/blob/master/api/application.js#L317) +## [debug](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L602) Runtime debug flag. -## [config](https://github.com/socketsupply/socket/blob/master/api/application.js#L323) +## [config](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L608) Application configuration. -## [backend](https://github.com/socketsupply/socket/blob/master/api/application.js#L328) +## [backend](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L613) The application's backend instance. -### [`open(opts)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L334) +### [`open(opts)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L619) @@ -237,7 +308,7 @@ The application's backend instance. | :--- | :--- | :--- | | Not specified | Promise | | -### [`close()`](https://github.com/socketsupply/socket/blob/master/api/application.js#L342) +### [`close()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/application.js#L627) @@ -249,7 +320,7 @@ The application's backend instance. -# [Bluetooth](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L12) +# [bluetooth](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/bluetooth.js#L12) A high-level, cross-platform API for Bluetooth Pub-Sub @@ -259,11 +330,11 @@ The application's backend instance. import { Bluetooth } from 'socket:bluetooth' ``` -## [`Bluetooth` (extends `EventEmitter`)](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L32) +## [`Bluetooth` (extends `EventEmitter`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/bluetooth.js#L32) Create an instance of a Bluetooth service. -### [`constructor(serviceId)`](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L40) +### [`constructor(serviceId)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/bluetooth.js#L40) constructor is an example property that is set to `true` Creates a new service with key-value pairs @@ -272,7 +343,7 @@ constructor is an example property that is set to `true` | :--- | :--- | :---: | :---: | :--- | | serviceId | string | | false | Given a default value to determine the type | -### [`start()`](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L90) +### [`start()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/bluetooth.js#L90) Start the Bluetooth service. @@ -280,7 +351,7 @@ Start the Bluetooth service. | :--- | :--- | :--- | | Not specified | Promise | | -### [`subscribe(id)`](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L119) +### [`subscribe(id)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/bluetooth.js#L119) Start scanning for published values that correspond to a well-known UUID. Once subscribed to a UUID, events that correspond to that UUID will be @@ -303,7 +374,7 @@ Start scanning for published values that correspond to a well-known UUID. | :--- | :--- | :--- | | Not specified | Promise | | -### [`publish(id, value)`](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L142) +### [`publish(id, value)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/bluetooth.js#L142) Start advertising a new value for a well-known UUID @@ -317,17 +388,10 @@ Start advertising a new value for a well-known UUID | Not specified | Promise | | - -# [Buffer](https://github.com/socketsupply/socket/blob/master/api/buffer.js) - -Buffer module is a [third party](https://github.com/feross/buffer) vendor module provided by Feross Aboukhadijeh and other contributors (MIT License). - -External docs: https://nodejs.org/api/buffer.html - -# [Crypto](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L15) +# [crypto](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L15) Some high-level methods around the `crypto.subtle` API for getting @@ -338,16 +402,16 @@ External docs: https://nodejs.org/api/buffer.html import { randomBytes } from 'socket:crypto' ``` -## [webcrypto](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L30) +## [webcrypto](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L30) External docs: https://developer.mozilla.org/en-US/docs/Web/API/Crypto WebCrypto API -## [ready](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L59) +## [ready](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L59) A promise that resolves when all internals to be loaded/ready. -## [`getRandomValues(buffer)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L74) +## [`getRandomValues(buffer)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L74) External docs: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues Generate cryptographically strong random values into the `buffer` @@ -360,7 +424,7 @@ Generate cryptographically strong random values into the `buffer` | :--- | :--- | :--- | | Not specified | TypedArray | | -## [`rand64()`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L97) +## [`rand64()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L97) Generate a random 64-bit number. @@ -368,19 +432,19 @@ Generate a random 64-bit number. | :--- | :--- | :--- | | A random 64-bit number. | BigInt | | -## [RANDOM_BYTES_QUOTA](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L105) +## [RANDOM_BYTES_QUOTA](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L105) Maximum total size of random bytes per page -## [MAX_RANDOM_BYTES](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L110) +## [MAX_RANDOM_BYTES](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L110) Maximum total size for random bytes. -## [MAX_RANDOM_BYTES_PAGES](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L115) +## [MAX_RANDOM_BYTES_PAGES](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L115) Maximum total amount of allocated per page of bytes (max/quota) -## [`randomBytes(size)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L123) +## [`randomBytes(size)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L123) Generate `size` random bytes. @@ -392,7 +456,7 @@ Generate `size` random bytes. | :--- | :--- | :--- | | Not specified | Buffer | A promise that resolves with an instance of socket.Buffer with random bytes. | -## [`createDigest(algorithm, message)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L150) +## [`createDigest(algorithm, message)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L150) @@ -405,7 +469,7 @@ Generate `size` random bytes. | :--- | :--- | :--- | | Not specified | Promise | A promise that resolves with an instance of socket.Buffer with the hash. | -## [`murmur3(value, seed)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L161) +## [`murmur3(value, seed)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/crypto.js#L161) A murmur3 hash implementation based on https://github.com/jwerle/murmurhash.c that works on strings and `ArrayBuffer` views (typed arrays) @@ -423,92 +487,7 @@ A murmur3 hash implementation based on https://github.com/jwerle/murmurhash.c -# [DNS](https://github.com/socketsupply/socket/blob/master/api/dns/index.js#L17) - - - This module enables name resolution. For example, use it to look up IP - addresses of host names. Although named for the Domain Name System (DNS), - it does not always use the DNS protocol for lookups. dns.lookup() uses the - operating system facilities to perform name resolution. It may not need to - perform any network communication. To perform name resolution the way other - applications on the same system do, use dns.lookup(). - - Example usage: - ```js - import { lookup } from 'socket:dns' - ``` - -## [`lookup(hostname, options, cb)`](https://github.com/socketsupply/socket/blob/master/api/dns/index.js#L60) - -External docs: https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback -Resolves a host name (e.g. `example.org`) into the first found A (IPv4) or - AAAA (IPv6) record. All option properties are optional. If options is an - integer, then it must be 4 or 6 – if options is 0 or not provided, then IPv4 - and IPv6 addresses are both returned if found. - - From the node.js website... - - > With the all option set to true, the arguments for callback change to (err, - addresses), with addresses being an array of objects with the properties - address and family. - - > On error, err is an Error object, where err.code is the error code. Keep in - mind that err.code will be set to 'ENOTFOUND' not only when the host name does - not exist but also when the lookup fails in other ways such as no available - file descriptors. dns.lookup() does not necessarily have anything to do with - the DNS protocol. The implementation uses an operating system facility that - can associate names with addresses and vice versa. This implementation can - have subtle but important consequences on the behavior of any Node.js program. - Please take some time to consult the Implementation considerations section - before using dns.lookup(). - - -| Argument | Type | Default | Optional | Description | -| :--- | :--- | :---: | :---: | :--- | -| hostname | string | | false | The host name to resolve. | -| options | object \| intenumberger | | true | An options object or record family. | -| options.family | number \| string | 0 | true | The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. | -| cb | function | | false | The function to call after the method is complete. | - - - - - -# [DNS.promises](https://github.com/socketsupply/socket/blob/master/api/dns/promises.js#L17) - - - This module enables name resolution. For example, use it to look up IP - addresses of host names. Although named for the Domain Name System (DNS), - it does not always use the DNS protocol for lookups. dns.lookup() uses the - operating system facilities to perform name resolution. It may not need to - perform any network communication. To perform name resolution the way other - applications on the same system do, use dns.lookup(). - - Example usage: - ```js - import { lookup } from 'socket:dns/promises' - ``` - -## [`lookup(hostname, opts)`](https://github.com/socketsupply/socket/blob/master/api/dns/promises.js#L37) - -External docs: https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options - - -| Argument | Type | Default | Optional | Description | -| :--- | :--- | :---: | :---: | :--- | -| hostname | string | | false | The host name to resolve. | -| opts | Object | | true | An options object. | -| opts.family | number \| string | 0 | true | The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. | - -| Return Value | Type | Description | -| :--- | :--- | :--- | -| Not specified | Promise | | - - - - - -# [Dgram](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L13) +# [dgram](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L13) This module provides an implementation of UDP datagram sockets. It does @@ -519,7 +498,7 @@ External docs: https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options import { createSocket } from 'socket:dgram' ``` -## [`createSocket(options, callback)`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L623) +## [`createSocket(options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L662) Creates a `Socket` instance. @@ -538,12 +517,12 @@ Creates a `Socket` instance. | :--- | :--- | :--- | | Not specified | Socket | | -## [`Socket` (extends `EventEmitter`)](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L629) +## [`Socket` (extends `EventEmitter`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L668) New instances of dgram.Socket are created using dgram.createSocket(). The new keyword is not to be used to create dgram.Socket instances. -### [`bind(port, address, callback)`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L705) +### [`bind(port, address, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L752) External docs: https://nodejs.org/api/dgram.html#socketbindport-address-callback Listen for datagram messages on a named port and optional address @@ -560,7 +539,7 @@ Listen for datagram messages on a named port and optional address | address | string | | false | The address to bind to (0.0.0.0) | | callback | function | | false | With no parameters. Called when binding is complete. | -### [`connect(port, host, connectListener)`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L781) +### [`connect(port, host, connectListener)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L873) External docs: https://nodejs.org/api/dgram.html#socketconnectport-address-callback Associates the dgram.Socket to a remote address and port. Every message sent @@ -580,7 +559,7 @@ Associates the dgram.Socket to a remote address and port. Every message sent | host | string | | true | Host the client should connect to. | | connectListener | function | | true | Common parameter of socket.connect() methods. Will be added as a listener for the 'connect' event once. | -### [`disconnect()`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L816) +### [`disconnect()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L910) External docs: https://nodejs.org/api/dgram.html#socketdisconnect A synchronous function that disassociates a connected dgram.Socket from @@ -588,7 +567,7 @@ A synchronous function that disassociates a connected dgram.Socket from disconnected socket will result in an ERR_SOCKET_DGRAM_NOT_CONNECTED exception. -### [`send(msg, offset, length, port, address, callback)`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L875) +### [`send(msg, offset, length, port, address, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L969) External docs: https://nodejs.org/api/dgram.html#socketsendmsg-offset-length-port-address-callback Broadcasts a datagram on the socket. For connectionless sockets, the @@ -639,7 +618,7 @@ Broadcasts a datagram on the socket. For connectionless sockets, the | address | string | | true | Destination host name or IP address. | | callback | Function | | true | Called when the message has been sent. | -### [`close(callback)`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L955) +### [`close(callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1067) External docs: https://nodejs.org/api/dgram.html#socketclosecallback Close the underlying socket and stop listening for data on it. If a @@ -651,7 +630,7 @@ Close the underlying socket and stop listening for data on it. If a | :--- | :--- | :---: | :---: | :--- | | callback | function | | true | Called when the connection is completed or on error. | -### [`address()`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1021) +### [`address()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1143) External docs: https://nodejs.org/api/dgram.html#socketaddress Returns an object containing the address information for a socket. For @@ -667,7 +646,7 @@ Returns an object containing the address information for a socket. For | socketInfo.port | string | The port of the socket | | socketInfo.family | string | The IP family of the socket | -### [`remoteAddress()`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1056) +### [`remoteAddress()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1178) External docs: https://nodejs.org/api/dgram.html#socketremoteaddress Returns an object containing the address, family, and port of the remote @@ -682,7 +661,7 @@ Returns an object containing the address, family, and port of the remote | socketInfo.port | string | The port of the socket | | socketInfo.family | string | The IP family of the socket | -### [`setRecvBufferSize(size)`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1087) +### [`setRecvBufferSize(size)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1209) External docs: https://nodejs.org/api/dgram.html#socketsetrecvbuffersizesize Sets the SO_RCVBUF socket option. Sets the maximum socket receive buffer in @@ -693,7 +672,7 @@ Sets the SO_RCVBUF socket option. Sets the maximum socket receive buffer in | :--- | :--- | :---: | :---: | :--- | | size | number | | false | The size of the new receive buffer | -### [`setSendBufferSize(size)`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1104) +### [`setSendBufferSize(size)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1226) External docs: https://nodejs.org/api/dgram.html#socketsetsendbuffersizesize Sets the SO_SNDBUF socket option. Sets the maximum socket send buffer in @@ -704,12 +683,12 @@ Sets the SO_SNDBUF socket option. Sets the maximum socket send buffer in | :--- | :--- | :---: | :---: | :--- | | size | number | | false | The size of the new send buffer | -### [`getRecvBufferSize()`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1117) +### [`getRecvBufferSize()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1239) External docs: https://nodejs.org/api/dgram.html#socketgetrecvbuffersize -### [`getSendBufferSize()`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1125) +### [`getSendBufferSize()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1247) External docs: https://nodejs.org/api/dgram.html#socketgetsendbuffersize @@ -718,46 +697,124 @@ External docs: https://nodejs.org/api/dgram.html#socketgetsendbuffersize | :--- | :--- | :--- | | Not specified | number | the SO_SNDBUF socket send buffer size in bytes. | -### [`code()`](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1193) +### [`code()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1315) -## [`ERR_SOCKET_ALREADY_BOUND` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1199) +## [`ERR_SOCKET_ALREADY_BOUND` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1321) Thrown when a socket is already bound. -## [`ERR_SOCKET_DGRAM_IS_CONNECTED` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1216) +## [`ERR_SOCKET_DGRAM_IS_CONNECTED` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1338) Thrown when the socket is already connected. -## [`ERR_SOCKET_DGRAM_NOT_CONNECTED` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1223) +## [`ERR_SOCKET_DGRAM_NOT_CONNECTED` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1345) Thrown when the socket is not connected. -## [`ERR_SOCKET_DGRAM_NOT_RUNNING` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1231) +## [`ERR_SOCKET_DGRAM_NOT_RUNNING` (extends `SocketError`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1353) Thrown when the socket is not running (not bound or connected). -## [`ERR_SOCKET_BAD_TYPE` (extends `TypeError`)](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1238) +## [`ERR_SOCKET_BAD_TYPE` (extends `TypeError`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1360) Thrown when a bad socket type is used in an argument. -## [`ERR_SOCKET_BAD_PORT` (extends `RangeError`)](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L1248) +## [`ERR_SOCKET_BAD_PORT` (extends `RangeError`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dgram.js#L1370) Thrown when a bad port is given. + + -# [Events](https://github.com/socketsupply/socket/blob/master/api/events.js) +# [dns](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dns/index.js#L17) -Events module is a [third party](https://github.com/browserify/events/blob/main/events.js) module provided by Browserify and Node.js contributors (MIT License). -External docs: https://nodejs.org/api/events.html + This module enables name resolution. For example, use it to look up IP + addresses of host names. Although named for the Domain Name System (DNS), + it does not always use the DNS protocol for lookups. dns.lookup() uses the + operating system facilities to perform name resolution. It may not need to + perform any network communication. To perform name resolution the way other + applications on the same system do, use dns.lookup(). + + Example usage: + ```js + import { lookup } from 'socket:dns' + ``` + +## [`lookup(hostname, options, cb)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dns/index.js#L60) + +External docs: https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback +Resolves a host name (e.g. `example.org`) into the first found A (IPv4) or + AAAA (IPv6) record. All option properties are optional. If options is an + integer, then it must be 4 or 6 – if options is 0 or not provided, then IPv4 + and IPv6 addresses are both returned if found. + + From the node.js website... + + > With the all option set to true, the arguments for callback change to (err, + addresses), with addresses being an array of objects with the properties + address and family. + + > On error, err is an Error object, where err.code is the error code. Keep in + mind that err.code will be set to 'ENOTFOUND' not only when the host name does + not exist but also when the lookup fails in other ways such as no available + file descriptors. dns.lookup() does not necessarily have anything to do with + the DNS protocol. The implementation uses an operating system facility that + can associate names with addresses and vice versa. This implementation can + have subtle but important consequences on the behavior of any Node.js program. + Please take some time to consult the Implementation considerations section + before using dns.lookup(). + + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| hostname | string | | false | The host name to resolve. | +| options | object \| intenumberger | | true | An options object or record family. | +| options.family | number \| string | 0 | true | The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. | +| cb | function | | false | The function to call after the method is complete. | + + + + + +# [dns.promises](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dns/promises.js#L17) + + + This module enables name resolution. For example, use it to look up IP + addresses of host names. Although named for the Domain Name System (DNS), + it does not always use the DNS protocol for lookups. dns.lookup() uses the + operating system facilities to perform name resolution. It may not need to + perform any network communication. To perform name resolution the way other + applications on the same system do, use dns.lookup(). + + Example usage: + ```js + import { lookup } from 'socket:dns/promises' + ``` + +## [`lookup(hostname, opts)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/dns/promises.js#L37) + +External docs: https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options + + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| hostname | string | | false | The host name to resolve. | +| opts | Object | | true | An options object. | +| opts.family | number \| string | 0 | true | The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. | + +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | Promise | | + -# [FS](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L26) +# [fs](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L26) This module enables interacting with the file system in a way modeled on @@ -781,9 +838,9 @@ External docs: https://nodejs.org/api/events.html import * as fs from 'socket:fs'; ``` -## [`access(path, mode, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L85) +## [`access(path, mode, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L111) -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback +External docs: https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback Asynchronously check access a file for a given mode calling `callback` upon success or error. @@ -793,7 +850,36 @@ Asynchronously check access a file for a given mode calling `callback` | mode | string? \| function(Error?)? | F_OK(0) | true | | | callback | function(Error?)? | | true | | -## [`chmod(path, mode, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L118) +## [`accessSync(path, mode)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L143) + +External docs: https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback +Synchronously check access a file for a given mode calling `callback` + upon success or error. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| mode | string? | F_OK(0) | true | | + +## [`exists(path, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L160) + +Checks if a path exists + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| callback | function(Boolean)? | | true | | + +## [`existsSync(path, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L177) + +Checks if a path exists + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| callback | function(Boolean)? | | true | | + +## [`chmod(path, mode, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L197) External docs: https://nodejs.org/api/fs.html#fschmodpath-mode-callback Asynchronously changes the permissions of a file. @@ -807,7 +893,18 @@ Asynchronously changes the permissions of a file. | mode | number | | false | | | callback | function(Error?) | | false | | -## [`chown(path, uid, gid, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L143) +## [`chmodSync(path, mode)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L232) + +External docs: https://nodejs.org/api/fs.html#fschmodpath-mode-callback +Synchronously changes the permissions of a file. + + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| mode | number | | false | | + +## [`chown(path, uid, gid, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L256) Changes ownership of file or directory at `path` with `uid` and `gid`. @@ -818,9 +915,19 @@ Changes ownership of file or directory at `path` with `uid` and `gid`. | gid | number | | false | | | callback | function | | false | | -## [`close(fd, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L171) +## [`chownSync(path, uid, gid)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L294) + +Changes ownership of file or directory at `path` with `uid` and `gid`. -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsclosefd-callback +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string | | false | | +| uid | number | | false | | +| gid | number | | false | | + +## [`close(fd, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L321) + +External docs: https://nodejs.org/api/fs.html#fsclosefd-callback Asynchronously close a file descriptor calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | @@ -828,9 +935,17 @@ Asynchronously close a file descriptor calling `callback` upon success or error. | fd | number | | false | | | callback | function(Error?)? | | true | | -## [`copyFile(src, dest, flags, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L195) +## [`closeSync(fd)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L341) + +Synchronously close a file descriptor. -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscopyfilesrc-dest-mode-callback +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| fd | number | | false | fd | + +## [`copyFile(src, dest, flags, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L358) + +External docs: https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback Asynchronously copies `src` to `dest` calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | @@ -840,9 +955,20 @@ Asynchronously copies `src` to `dest` calling `callback` upon success or error. | flags | number | | false | Modifiers for copy operation. | | callback | function(Error=) | | true | The function to call after completion. | -## [`createReadStream(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L223) +## [`copyFileSync(src, dest, flags)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L401) + +External docs: https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback +Synchronously copies `src` to `dest` calling `callback` upon success or error. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| src | string | | false | The source file path. | +| dest | string | | false | The destination file path. | +| flags | number | | false | Modifiers for copy operation. | + +## [`createReadStream(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L430) -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options +External docs: https://nodejs.org/api/fs.html#fscreatewritestreampath-options | Argument | Type | Default | Optional | Description | @@ -854,9 +980,9 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewri | :--- | :--- | :--- | | Not specified | ReadStream | | -## [`createWriteStream(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L266) +## [`createWriteStream(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L475) -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options +External docs: https://nodejs.org/api/fs.html#fscreatewritestreampath-options | Argument | Type | Default | Optional | Description | @@ -868,9 +994,9 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewri | :--- | :--- | :--- | | Not specified | WriteStream | | -## [`fstat(fd, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L312) +## [`fstat(fd, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L529) -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsfstatfd-options-callback +External docs: https://nodejs.org/api/fs.html#fsfstatfd-options-callback Invokes the callback with the for the file descriptor. See the POSIX fstat(2) documentation for more detail. @@ -882,7 +1008,7 @@ Invokes the callback with the for the file descriptor. See | options | object? \| function? | | true | An options object. | | callback | function? | | false | The function to call after completion. | -## [`fsync(fd, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L339) +## [`fsync(fd, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L556) Request that all data for the open file descriptor is flushed to the storage device. @@ -892,7 +1018,7 @@ Request that all data for the open file descriptor is flushed | fd | number | | false | A file descriptor. | | callback | function | | false | The function to call after completion. | -## [`ftruncate(fd, offset, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L361) +## [`ftruncate(fd, offset, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L578) Truncates the file up to `offset` bytes. @@ -902,7 +1028,7 @@ Truncates the file up to `offset` bytes. | offset | number= \| function | 0 | true | | | callback | function? | | false | The function to call after completion. | -## [`lchown(path, uid, gid, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L389) +## [`lchown(path, uid, gid, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L606) Chages ownership of link at `path` with `uid` and `gid. @@ -913,7 +1039,7 @@ Chages ownership of link at `path` with `uid` and `gid. | gid | number | | false | | | callback | function | | false | | -## [`link(src, dest, )`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L417) +## [`link(src, dest, )`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L645) Creates a link to `dest` from `src`. @@ -923,9 +1049,9 @@ Creates a link to `dest` from `src`. | dest | string | | false | | | (Position 0) | function | | false | | -## [`open(path, flags, mode, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L473) +## [`open(path, flags, mode, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L733) -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback +External docs: https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback Asynchronously open a file calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | @@ -936,9 +1062,20 @@ Asynchronously open a file calling `callback` upon success or error. | options | object? \| function? | | true | | | callback | function(Error?, number?)? | | true | | -## [`opendir(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L526) +## [`openSync(path, flags, mode, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L786) + +Synchronously open a file. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| flags | string? | r | true | | +| mode | string? | 0o666 | true | | +| options | object? \| function? | | true | | -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback +## [`opendir(path, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L833) + +External docs: https://nodejs.org/api/fs.html#fsreaddirpath-options-callback Asynchronously open a directory calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | @@ -949,9 +1086,25 @@ Asynchronously open a directory calling `callback` upon success or error. | options.withFileTypes | boolean? | false | true | | | callback | function(Error?, Dir?)? | | false | | -## [`read(fd, buffer, offset, length, position, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L552) +## [`opendirSync(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L860) + +External docs: https://nodejs.org/api/fs.html#fsreaddirpath-options-callback +Synchronously open a directory. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| options | object? \| function(Error?, Dir?) | | true | | +| options.encoding | string? | utf8 | true | | +| options.withFileTypes | boolean? | false | true | | -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | Dir | | + +## [`read(fd, buffer, offset, length, position, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L887) + +External docs: https://nodejs.org/api/fs.html#fsreadfd-buffer-offset-length-position-callback Asynchronously read from an open file descriptor. | Argument | Type | Default | Optional | Description | @@ -963,9 +1116,23 @@ Asynchronously read from an open file descriptor. | position | number \| BigInt \| null | | false | Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged. | | callback | function(Error?, number?, Buffer?) | | false | | -## [`readdir(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L586) +## [`write(fd, buffer, offset, length, position, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L922) + +External docs: https://nodejs.org/api/fs.html#fswritefd-buffer-offset-length-position-callback +Asynchronously write to an open file descriptor. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| fd | number | | false | | +| buffer | object \| Buffer \| TypedArray | | false | The buffer that the data will be written to. | +| offset | number | | false | The position in buffer to write the data to. | +| length | number | | false | The number of bytes to read. | +| position | number \| BigInt \| null | | false | Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged. | +| callback | function(Error?, number?, Buffer?) | | false | | + +## [`readdir(path, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L956) -External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback +External docs: https://nodejs.org/api/fs.html#fsreaddirpath-options-callback Asynchronously read all entries in a directory. | Argument | Type | Default | Optional | Description | @@ -976,7 +1143,19 @@ Asynchronously read all entries in a directory. | options.withFileTypes ? false | boolean? | | true | | | callback | function(Error?, object) | | false | | -## [`readFile(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L637) +## [`readdirSync(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1008) + +External docs: https://nodejs.org/api/fs.html#fsreaddirpath-options-callback +Synchronously read all entries in a directory. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| options | object? \| function(Error?, object) | | true | | +| options.encoding ? utf8 | string? | | true | | +| options.withFileTypes ? false | boolean? | | true | | + +## [`readFile(path, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1038) @@ -989,7 +1168,18 @@ Asynchronously read all entries in a directory. | options.signal | AbortSignal? | | true | | | callback | function(Error?, Buffer?) | | false | | -## [`readlink(path, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L680) +## [`readFileSync(path, } options, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1080) + + + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL \| number | | false | | +| } options | { encoding?: string = 'utf8', flags?: string = 'r' | | false | | +| options | object? \| function(Error?, Buffer?) | | true | | +| options.signal | AbortSignal? | | true | | + +## [`readlink(path, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1143) Reads link at `path` @@ -998,7 +1188,7 @@ Reads link at `path` | path | string | | false | | | callback | function(err, string) | | false | | -## [`realpath(path, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L699) +## [`realpath(path, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1163) Computes real path for `path` @@ -1007,7 +1197,15 @@ Computes real path for `path` | path | string | | false | | | callback | function(err, string) | | false | | -## [`rename(src, dest, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L719) +## [`realpathSync(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1181) + +Computes real path for `path` + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string | | false | | + +## [`rename(src, dest, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1199) Renames file or directory at `src` to `dest`. @@ -1017,7 +1215,16 @@ Renames file or directory at `src` to `dest`. | dest | string | | false | | | callback | function | | false | | -## [`rmdir(path, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L742) +## [`renameSync(src, dest)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1225) + +Renames file or directory at `src` to `dest`, synchronously. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| src | string | | false | | +| dest | string | | false | | + +## [`rmdir(path, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1249) Removes directory at `path`. @@ -1026,9 +1233,41 @@ Removes directory at `path`. | path | string | | false | | | callback | function | | false | | -## [`stat(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L765) +## [`rmdirSync(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1269) + +Removes directory at `path`, synchronously. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string | | false | | +## [`statSync(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1290) +Synchronously get the stats of a file + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL \| number | | false | filename or file descriptor | +| options | object? | | false | | +| options.encoding ? utf8 | string? | | true | | +| options.flag ? r | string? | | true | | + +## [`stat(path, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1310) + +Get the stats of a file + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL \| number | | false | filename or file descriptor | +| options | object? | | false | | +| options.encoding ? utf8 | string? | | true | | +| options.flag ? r | string? | | true | | +| options.signal | AbortSignal? | | true | | +| callback | function(Error?, Stats?) | | false | | + +## [`lstat(path, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1382) + +Get the stats of a symbolic link | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | @@ -1039,7 +1278,7 @@ Removes directory at `path`. | options.signal | AbortSignal? | | true | | | callback | function(Error?, Stats?) | | false | | -## [`symlink(src, dest)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L799) +## [`symlink(src, dest)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1423) Creates a symlink of `src` at `dest`. @@ -1048,7 +1287,7 @@ Creates a symlink of `src` at `dest`. | src | string | | false | | | dest | string | | false | | -## [`unlink(path, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L840) +## [`unlink(path, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1466) Unlinks (removes) file at `path`. @@ -1057,7 +1296,15 @@ Unlinks (removes) file at `path`. | path | string | | false | | | callback | function | | false | | -## [`writeFile(path, data, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L865) +## [`unlinkSync(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1486) + +Unlinks (removes) file at `path`, synchronously. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string | | false | | + +## [`writeFile(path, data, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1511) @@ -1072,7 +1319,22 @@ Unlinks (removes) file at `path`. | options.signal | AbortSignal? | | true | | | callback | function(Error?) | | false | | -## [`watch(, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L910) +## [`writeFileSync(path, data, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1556) + +External docs: https://nodejs.org/api/fs.html#fswritefilesyncfile-data-options +Writes data to a file synchronously. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL \| number | | false | filename or file descriptor | +| data | string \| Buffer \| TypedArray \| DataView \| object | | false | | +| options | object? | | false | | +| options.encoding ? utf8 | string? | | true | | +| options.mode ? 0o666 | string? | | true | | +| options.flag ? w | string? | | true | | +| options.signal | AbortSignal? | | true | | + +## [`watch(, options, callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/index.js#L1591) Watch for changes at `path` calling `callback` @@ -1091,7 +1353,7 @@ Watch for changes at `path` calling `callback` -# [FS.promises](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L25) +# [fs.promises](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L25) * This module enables interacting with the file system in a way modeled on @@ -1115,7 +1377,7 @@ Watch for changes at `path` calling `callback` import fs from 'socket:fs/promises' ``` -## [`access(path, mode, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L86) +## [`access(path, mode, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L111) External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesaccesspath-mode Asynchronously check access a file. @@ -1126,7 +1388,7 @@ Asynchronously check access a file. | mode | string? | | true | | | options | object? | | true | | -## [`chmod(path, mode)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L96) +## [`chmod(path, mode)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L122) External docs: https://nodejs.org/api/fs.html#fspromiseschmodpath-mode @@ -1140,7 +1402,7 @@ External docs: https://nodejs.org/api/fs.html#fspromiseschmodpath-mode | :--- | :--- | :--- | | Not specified | Promise | | -## [`chown(path, uid, gid)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L119) +## [`chown(path, uid, gid)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L147) Changes ownership of file or directory at `path` with `uid` and `gid`. @@ -1154,7 +1416,7 @@ Changes ownership of file or directory at `path` with `uid` and `gid`. | :--- | :--- | :--- | | Not specified | Promise | | -## [`copyFile(src, dest, flags)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L146) +## [`copyFile(src, dest, flags)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L176) Asynchronously copies `src` to `dest` calling `callback` upon success or error. @@ -1168,7 +1430,7 @@ Asynchronously copies `src` to `dest` calling `callback` upon success or error. | :--- | :--- | :--- | | Not specified | Promise | | -## [`lchown(path, uid, gid)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L173) +## [`lchown(path, uid, gid)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L206) Chages ownership of link at `path` with `uid` and `gid. @@ -1182,7 +1444,7 @@ Chages ownership of link at `path` with `uid` and `gid. | :--- | :--- | :--- | | Not specified | Promise | | -## [`link(src, dest)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L199) +## [`link(src, dest)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L234) Creates a link to `dest` from `dest`. @@ -1195,7 +1457,7 @@ Creates a link to `dest` from `dest`. | :--- | :--- | :--- | | Not specified | Promise | | -## [`mkdir(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L224) +## [`mkdir(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L262) Asynchronously creates a directory. @@ -1211,7 +1473,7 @@ Asynchronously creates a directory. | :--- | :--- | :--- | | Not specified | Promise | Upon success, fulfills with undefined if recursive is false, or the first directory path created if recursive is true. | -## [`open(path, flags, mode)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L252) +## [`open(path, flags, mode)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L292) External docs: https://nodejs.org/api/fs.html#fspromisesopenpath-flags-mode Asynchronously open a file. @@ -1227,7 +1489,7 @@ Asynchronously open a file. | :--- | :--- | :--- | | Not specified | Promise | | -## [`opendir(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L264) +## [`opendir(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L305) External docs: https://nodejs.org/api/fs.html#fspromisesopendirpath-options @@ -1243,7 +1505,7 @@ External docs: https://nodejs.org/api/fs.html#fspromisesopendirpath-options | :--- | :--- | :--- | | Not specified | Promise | | -## [`readdir(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L276) +## [`readdir(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L318) External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreaddirpath-options @@ -1255,7 +1517,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesr | options.encoding | string? | utf8 | true | | | options.withFileTypes | boolean? | false | true | | -## [`readFile(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L309) +## [`readFile(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L356) External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreadfilepath-options @@ -1272,7 +1534,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesr | :--- | :--- | :--- | | Not specified | Promise | | -## [`readlink(path)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L326) +## [`readlink(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L374) Reads link at `path` @@ -1284,7 +1546,7 @@ Reads link at `path` | :--- | :--- | :--- | | Not specified | Promise | | -## [`realpath(path)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L345) +## [`realpath(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L395) Computes real path for `path` @@ -1296,7 +1558,7 @@ Computes real path for `path` | :--- | :--- | :--- | | Not specified | Promise | | -## [`rename(src, dest)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L365) +## [`rename(src, dest)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L417) Renames file or directory at `src` to `dest`. @@ -1309,7 +1571,7 @@ Renames file or directory at `src` to `dest`. | :--- | :--- | :--- | | Not specified | Promise | | -## [`rmdir(path)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L386) +## [`rmdir(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L441) Removes directory at `path`. @@ -1321,10 +1583,25 @@ Removes directory at `path`. | :--- | :--- | :--- | | Not specified | Promise | | -## [`stat(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L405) +## [`stat(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L463) External docs: https://nodejs.org/api/fs.html#fspromisesstatpath-options +Get the stats of a file + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| path | string \| Buffer \| URL | | false | | +| options | object? | | true | | +| options.bigint | boolean? | false | true | | +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | Promise | | + +## [`lstat(path, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L478) + +External docs: https://nodejs.org/api/fs.html#fspromiseslstatpath-options +Get the stats of a symbolic link. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | @@ -1336,7 +1613,7 @@ External docs: https://nodejs.org/api/fs.html#fspromisesstatpath-options | :--- | :--- | :--- | | Not specified | Promise | | -## [`symlink(src, dest)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L417) +## [`symlink(src, dest)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L491) Creates a symlink of `src` at `dest`. @@ -1349,7 +1626,7 @@ Creates a symlink of `src` at `dest`. | :--- | :--- | :--- | | Not specified | Promise | | -## [`unlink(path)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L452) +## [`unlink(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L527) Unlinks (removes) file at `path`. @@ -1361,7 +1638,7 @@ Unlinks (removes) file at `path`. | :--- | :--- | :--- | | Not specified | Promise | | -## [`writeFile(path, data, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L475) +## [`writeFile(path, data, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L552) External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromiseswritefilefile-data-options @@ -1380,7 +1657,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw | :--- | :--- | :--- | | Not specified | Promise | | -## [`watch(, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L495) +## [`watch(, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/fs/promises.js#L573) Watch for changes at `path` calling `callback` @@ -1399,7 +1676,7 @@ Watch for changes at `path` calling `callback` -# [IPC](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L37) +# [ipc](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L37) This is a low-level API that you don't need unless you are implementing @@ -1433,12 +1710,33 @@ Watch for changes at `path` calling `callback` import { send } from 'socket:ipc' ``` -## [`maybeMakeError()`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L298) +## [`maybeMakeError()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L281) This is a `FunctionDeclaration` named `maybeMakeError` in `api/ipc.js`, it's exported but undocumented. -## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1100) +### [debug](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L408) + + + +### [undefined](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L408) + + + +### [undefined](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L408) + + + +### [undefined](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L408) + + + +## [`IPCSearchParams` (extends `URLSearchParams`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L1050) + +This is a `ClassDeclaration` named ``IPCSearchParams` (extends `URLSearchParams`)` in `api/ipc.js`, it's exported but undocumented. + + +## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L1219) Emit event to be dispatched on `window` object. @@ -1449,7 +1747,7 @@ Emit event to be dispatched on `window` object. | target | EventTarget | window | true | | | options | Object | | true | | -## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1159) +## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L1278) Sends an async IPC command request with parameters. @@ -1465,22 +1763,47 @@ Sends an async IPC command request with parameters. | :--- | :--- | :--- | | Not specified | Promise | | +## [`inflateIPCMessageTransfers()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L1729) + +This is a `FunctionDeclaration` named `inflateIPCMessageTransfers` in `api/ipc.js`, it's exported but undocumented. + + +## [`findIPCMessageTransfers()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L1761) + +This is a `FunctionDeclaration` named `findIPCMessageTransfers` in `api/ipc.js`, it's exported but undocumented. + + +## [ports](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L1826) + +This is a `VariableDeclaration` named `ports` in `api/ipc.js`, it's exported but undocumented. + + +## [`IPCMessagePort` (extends `MessagePort`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L1828) + +This is a `ClassDeclaration` named ``IPCMessagePort` (extends `MessagePort`)` in `api/ipc.js`, it's exported but undocumented. + + +## [`IPCMessageChannel` (extends `MessageChannel`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/ipc.js#L2054) + +This is a `ClassDeclaration` named ``IPCMessageChannel` (extends `MessageChannel`)` in `api/ipc.js`, it's exported but undocumented. + + -# [Network](https://github.com/socketsupply/socket/blob/master/api/network.js#L9) +# [network](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/network.js#L9) External docs: https://socketsupply.co/guides/#p2p-guide - Provides a higher level API over the stream-relay protocol. + Provides a higher level API over the latica protocol. -# [OS](https://github.com/socketsupply/socket/blob/master/api/os.js#L13) +# [os](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L13) This module provides normalized system information from all the major @@ -1491,7 +1814,7 @@ External docs: https://socketsupply.co/guides/#p2p-guide import { arch, platform } from 'socket:os' ``` -## [`arch()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L56) +## [`arch()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L60) Returns the operating system CPU architecture for which Socket was compiled. @@ -1499,7 +1822,7 @@ Returns the operating system CPU architecture for which Socket was compiled. | :--- | :--- | :--- | | Not specified | string | 'arm64', 'ia32', 'x64', or 'unknown' | -## [`cpus()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L74) +## [`cpus()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L78) External docs: https://nodejs.org/api/os.html#os_os_cpus Returns an array of objects containing information about each CPU/core. @@ -1517,7 +1840,7 @@ Returns an array of objects containing information about each CPU/core. | :--- | :--- | :--- | | cpus | Array | An array of objects containing information about each CPU/core. | -## [`networkInterfaces()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L98) +## [`networkInterfaces()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L102) External docs: https://nodejs.org/api/os.html#os_os_networkinterfaces Returns an object containing network interfaces that have been assigned a network address. @@ -1535,7 +1858,7 @@ Returns an object containing network interfaces that have been assigned a networ | :--- | :--- | :--- | | Not specified | object | An object containing network interfaces that have been assigned a network address. | -## [`platform()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L186) +## [`platform()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L190) External docs: https://nodejs.org/api/os.html#os_os_platform Returns the operating system platform. @@ -1545,7 +1868,7 @@ Returns the operating system platform. | :--- | :--- | :--- | | Not specified | string | 'android', 'cygwin', 'freebsd', 'linux', 'darwin', 'ios', 'openbsd', 'win32', or 'unknown' | -## [`type()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L195) +## [`type()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L199) External docs: https://nodejs.org/api/os.html#os_os_type Returns the operating system name. @@ -1554,7 +1877,7 @@ Returns the operating system name. | :--- | :--- | :--- | | Not specified | string | 'CYGWIN_NT', 'Mac', 'Darwin', 'FreeBSD', 'Linux', 'OpenBSD', 'Windows_NT', 'Win32', or 'Unknown' | -## [`isWindows()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L234) +## [`isWindows()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L238) @@ -1562,7 +1885,7 @@ Returns the operating system name. | :--- | :--- | :--- | | Not specified | boolean | `true` if the operating system is Windows. | -## [`tmpdir()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L246) +## [`tmpdir()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L250) @@ -1570,15 +1893,15 @@ Returns the operating system name. | :--- | :--- | :--- | | Not specified | string | The operating system's default directory for temporary files. | -## [EOL](https://github.com/socketsupply/socket/blob/master/api/os.js#L294) +## [EOL](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L298) The operating system's end-of-line marker. `'\r\n'` on Windows and `'\n'` on POSIX. -## [`rusage()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L306) +## [`rusage()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L310) Get resource usage. -## [`uptime()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L316) +## [`uptime()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L320) Returns the system uptime in seconds. @@ -1586,7 +1909,7 @@ Returns the system uptime in seconds. | :--- | :--- | :--- | | Not specified | number | The system uptime in seconds. | -## [`uname()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L327) +## [`uname()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L331) Returns the operating system name. @@ -1594,11 +1917,19 @@ Returns the operating system name. | :--- | :--- | :--- | | Not specified | string | The operating system name. | +## [`homedir()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/os.js#L389) + +Returns the home directory of the current user. + +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | string | | + -# [Path](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L9) +# [path](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L9) Example usage: @@ -1606,7 +1937,7 @@ Returns the operating system name. import { Path } from 'socket:path' ``` -## [`resolve()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L41) +## [`resolve()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L44) External docs: https://nodejs.org/api/path.html#path_path_resolve_paths The path.resolve() method resolves a sequence of paths or path segments into an absolute path. @@ -1619,7 +1950,7 @@ The path.resolve() method resolves a sequence of paths or path segments into an | :--- | :--- | :--- | | Not specified | string | | -## [`cwd(opts)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L75) +## [`cwd(opts)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L82) Computes current working directory for a path @@ -1632,7 +1963,7 @@ Computes current working directory for a path | :--- | :--- | :--- | | Not specified | string | | -## [`origin()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L99) +## [`origin()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L106) Computed location origin. Defaults to `socket:///` if not available. @@ -1640,7 +1971,7 @@ Computed location origin. Defaults to `socket:///` if not available. | :--- | :--- | :--- | | Not specified | string | | -## [`relative(options, from, to)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L110) +## [`relative(options, from, to)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L117) Computes the relative path from `from` to `to`. @@ -1654,7 +1985,7 @@ Computes the relative path from `from` to `to`. | :--- | :--- | :--- | | Not specified | string | | -## [`join(options, components)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L157) +## [`join(options, components)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L164) Joins path components. This function may not return an absolute path. @@ -1667,7 +1998,7 @@ Joins path components. This function may not return an absolute path. | :--- | :--- | :--- | | Not specified | string | | -## [`dirname(options, components)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L210) +## [`dirname(options, components)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L221) Computes directory name of path. @@ -1680,7 +2011,7 @@ Computes directory name of path. | :--- | :--- | :--- | | Not specified | string | | -## [`basename(options, components)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L246) +## [`basename(options, components)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L263) Computes base name of path. @@ -1693,7 +2024,7 @@ Computes base name of path. | :--- | :--- | :--- | | Not specified | string | | -## [`extname(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L260) +## [`extname(options, path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L277) Computes extension name of path. @@ -1706,7 +2037,7 @@ Computes extension name of path. | :--- | :--- | :--- | | Not specified | string | | -## [`normalize(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L271) +## [`normalize(options, path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L288) Computes normalized path @@ -1719,7 +2050,7 @@ Computes normalized path | :--- | :--- | :--- | | Not specified | string | | -## [`format(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L321) +## [`format(options, path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L338) Formats `Path` object into a string. @@ -1732,7 +2063,7 @@ Formats `Path` object into a string. | :--- | :--- | :--- | | Not specified | string | | -## [`parse(path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L337) +## [`parse(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L354) Parses input `path` into a `Path` instance. @@ -1744,11 +2075,11 @@ Parses input `path` into a `Path` instance. | :--- | :--- | :--- | | Not specified | object | | -## [Path](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L365) +## [Path](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L382) A container for a parsed Path. -### [`from(input, cwd)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L371) +### [`from(input, cwd)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L388) Creates a `Path` instance from `input` and optional `cwd`. @@ -1757,7 +2088,7 @@ Creates a `Path` instance from `input` and optional `cwd`. | input | PathComponent | | false | | | cwd | string | | true | | -### [`constructor(pathname, cwd)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L394) +### [`constructor(pathname, cwd)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L411) `Path` class constructor. @@ -1766,47 +2097,47 @@ Creates a `Path` instance from `input` and optional `cwd`. | pathname | string | | false | | | cwd | string | Path.cwd() | true | | -### [`isRelative()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L463) +### [`isRelative()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L484) `true` if the path is relative, otherwise `false. -### [`value()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L470) +### [`value()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L491) The working value of this path. -### [`source()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L504) +### [`source()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L525) The original source, unresolved. -### [`parent()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L512) +### [`parent()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L533) Computed parent path. -### [`root()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L531) +### [`root()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L552) Computed root in path. -### [`dir()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L552) +### [`dir()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L573) Computed directory name in path. -### [`base()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L587) +### [`base()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L608) Computed base name in path. -### [`name()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L599) +### [`name()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L620) Computed base name in path without path extension. -### [`ext()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L607) +### [`ext()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L628) Computed extension name in path. -### [`drive()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L627) +### [`drive()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L648) The computed drive, if given in the path. -### [`toURL()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L634) +### [`toURL()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L655) @@ -1814,7 +2145,7 @@ The computed drive, if given in the path. | :--- | :--- | :--- | | Not specified | URL | | -### [`toString()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L642) +### [`toString()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/path/path.js#L663) Converts this `Path` instance to a string. @@ -1826,7 +2157,7 @@ Converts this `Path` instance to a string. -# [Process](https://github.com/socketsupply/socket/blob/master/api/process.js#L9) +# [process](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L9) Example usage: @@ -1834,23 +2165,30 @@ Converts this `Path` instance to a string. import process from 'socket:process' ``` -## [`nextTick(callback)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L42) +## [`ProcessEnvironmentEvent` (extends `Event`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L18) -Adds callback to the 'nextTick' queue. +This is a `ClassDeclaration` named ``ProcessEnvironmentEvent` (extends `Event`)` in `api/process.js`, it's exported but undocumented. -| Argument | Type | Default | Optional | Description | -| :--- | :--- | :---: | :---: | :--- | -| callback | Function | | false | | -## [`homedir()`](https://github.com/socketsupply/socket/blob/master/api/process.js#L71) +## [`ProcessEnvironment` (extends `EventTarget`)](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L29) +This is a `ClassDeclaration` named ``ProcessEnvironment` (extends `EventTarget`)` in `api/process.js`, it's exported but undocumented. -| Return Value | Type | Description | -| :--- | :--- | :--- | -| Not specified | string | The home directory of the current user. | +## [env](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L35) -## [`hrtime(time)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L80) +This is a `VariableDeclaration` named `env` in `api/process.js`, it's exported but undocumented. + + +## [`nextTick(callback)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L202) + +Adds callback to the 'nextTick' queue. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| callback | Function | | false | | + +## [`hrtime(time)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L235) Computed high resolution time as a `BigInt`. @@ -1862,7 +2200,7 @@ Computed high resolution time as a `BigInt`. | :--- | :--- | :--- | | Not specified | bigint | | -## [`exit(code)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L104) +## [`exit(code)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L261) @@ -1870,7 +2208,7 @@ Computed high resolution time as a `BigInt`. | :--- | :--- | :---: | :---: | :--- | | code | number | 0 | true | The exit code. Default: 0. | -## [`memoryUsage()`](https://github.com/socketsupply/socket/blob/master/api/process.js#L116) +## [`memoryUsage()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/process.js#L273) Returns an object describing the memory usage of the Node.js process measured in bytes. @@ -1882,7 +2220,7 @@ Returns an object describing the memory usage of the Node.js process measured in -# [Test](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L17) +# [test](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L17) Provides a test runner for Socket Runtime. @@ -1896,7 +2234,7 @@ Returns an object describing the memory usage of the Node.js process measured in }) ``` -## [`getDefaultTestRunnerTimeout()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L54) +## [`getDefaultTestRunnerTimeout()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L54) @@ -1904,11 +2242,11 @@ Returns an object describing the memory usage of the Node.js process measured in | :--- | :--- | :--- | | Not specified | number | The default timeout for tests in milliseconds. | -## [Test](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L69) +## [Test](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L69) -### [`constructor(name, fn, runner)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L127) +### [`constructor(name, fn, runner)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L127) @@ -1918,7 +2256,7 @@ Returns an object describing the memory usage of the Node.js process measured in | fn | TestFn | | false | | | runner | TestRunner | | false | | -### [`comment(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L138) +### [`comment(msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L138) @@ -1926,7 +2264,7 @@ Returns an object describing the memory usage of the Node.js process measured in | :--- | :--- | :---: | :---: | :--- | | msg | string | | false | | -### [`plan(n)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L148) +### [`plan(n)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L148) Plan the number of assertions. @@ -1935,7 +2273,7 @@ Plan the number of assertions. | :--- | :--- | :---: | :---: | :--- | | n | number | | false | | -### [`deepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L159) +### [`deepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L159) @@ -1945,7 +2283,7 @@ Plan the number of assertions. | expected | T | | false | | | msg | string | | true | | -### [`notDeepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L174) +### [`notDeepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L174) @@ -1955,7 +2293,7 @@ Plan the number of assertions. | expected | T | | false | | | msg | string | | true | | -### [`equal(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L189) +### [`equal(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L189) @@ -1965,7 +2303,7 @@ Plan the number of assertions. | expected | T | | false | | | msg | string | | true | | -### [`notEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L204) +### [`notEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L204) @@ -1975,7 +2313,7 @@ Plan the number of assertions. | expected | unknown | | false | | | msg | string | | true | | -### [`fail(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L217) +### [`fail(msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L217) @@ -1983,7 +2321,7 @@ Plan the number of assertions. | :--- | :--- | :---: | :---: | :--- | | msg | string | | true | | -### [`ok(actual, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L230) +### [`ok(actual, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L230) @@ -1992,7 +2330,7 @@ Plan the number of assertions. | actual | unknown | | false | | | msg | string | | true | | -### [`pass(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L242) +### [`pass(msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L242) @@ -2000,7 +2338,7 @@ Plan the number of assertions. | :--- | :--- | :---: | :---: | :--- | | msg | string | | true | | -### [`ifError(err, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L251) +### [`ifError(err, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L251) @@ -2009,7 +2347,7 @@ Plan the number of assertions. | err | Error \| null \| undefined | | false | | | msg | string | | true | | -### [`throws(fn, expected, message)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L264) +### [`throws(fn, expected, message)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L264) @@ -2019,7 +2357,7 @@ Plan the number of assertions. | expected | RegExp \| any | | true | | | message | string | | true | | -### [`sleep(ms, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L313) +### [`sleep(ms, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L313) Sleep for ms with an optional msg @@ -2037,7 +2375,7 @@ Sleep for ms with an optional msg | :--- | :--- | :--- | | Not specified | Promise | | -### [`requestAnimationFrame(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L331) +### [`requestAnimationFrame(msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L331) Request animation frame with an optional msg. Falls back to a 0ms setTimeout when tests are run headlessly. @@ -2055,7 +2393,7 @@ Request animation frame with an optional msg. Falls back to a 0ms setTimeout whe | :--- | :--- | :--- | | Not specified | Promise | | -### [`click(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L354) +### [`click(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L354) Dispatch the `click`` method on an element specified by selector. @@ -2073,7 +2411,7 @@ Dispatch the `click`` method on an element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`eventClick(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L376) +### [`eventClick(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L380) Dispatch the click window.MouseEvent on an element specified by selector. @@ -2091,7 +2429,7 @@ Dispatch the click window.MouseEvent on an element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`dispatchEvent(event, target, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L404) +### [`dispatchEvent(event, target, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L408) Dispatch an event on the target. @@ -2110,7 +2448,7 @@ Dispatch an event on the target. | :--- | :--- | :--- | | Not specified | Promise | | -### [`focus(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L424) +### [`focus(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L428) Call the focus method on element specified by selector. @@ -2128,7 +2466,7 @@ Call the focus method on element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`blur(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L445) +### [`blur(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L452) Call the blur method on element specified by selector. @@ -2146,7 +2484,7 @@ Call the blur method on element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`type(selector, str, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L467) +### [`type(selector, str, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L477) Consecutively set the str value of the element specified by selector to simulate typing. @@ -2165,7 +2503,7 @@ Consecutively set the str value of the element specified by selector to simulate | :--- | :--- | :--- | | Not specified | Promise | | -### [`appendChild(parentSelector, el, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L499) +### [`appendChild(parentSelector, el, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L509) appendChild an element el to a parent selector element. @@ -2185,7 +2523,7 @@ appendChild an element el to a parent selector element. | :--- | :--- | :--- | | Not specified | Promise | | -### [`removeElement(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L519) +### [`removeElement(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L529) Remove an element from the DOM. @@ -2203,7 +2541,7 @@ Remove an element from the DOM. | :--- | :--- | :--- | | Not specified | Promise | | -### [`elementVisible(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L538) +### [`elementVisible(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L548) Test if an element is visible @@ -2221,7 +2559,7 @@ Test if an element is visible | :--- | :--- | :--- | | Not specified | Promise | | -### [`elementInvisible(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L559) +### [`elementInvisible(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L569) Test if an element is invisible @@ -2239,7 +2577,7 @@ Test if an element is invisible | :--- | :--- | :--- | | Not specified | Promise | | -### [`waitFor(querySelectorOrFn, opts, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L583) +### [`waitFor(querySelectorOrFn, opts, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L593) Test if an element is invisible @@ -2260,7 +2598,7 @@ Test if an element is invisible | :--- | :--- | :--- | | Not specified | Promise | | -### [`waitForText(selector, opts, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L645) +### [`waitForText(selector, opts, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L655) Test if an element is invisible @@ -2290,7 +2628,7 @@ Test if an element is invisible | :--- | :--- | :--- | | Not specified | Promise | | -### [`querySelector(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L682) +### [`querySelector(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L692) Run a querySelector as an assert and also get the results @@ -2308,7 +2646,7 @@ Run a querySelector as an assert and also get the results | :--- | :--- | :--- | | Not specified | HTMLElement \| Element | | -### [`querySelectorAll(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L701) +### [`querySelectorAll(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L711) Run a querySelectorAll as an assert and also get the results @@ -2326,7 +2664,7 @@ Run a querySelectorAll as an assert and also get the results | :--- | :--- | :--- | | Not specified | Array | | -### [`getComputedStyle(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L730) +### [`getComputedStyle(selector, msg)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L740) Retrieves the computed styles for a given element. @@ -2351,17 +2689,17 @@ Retrieves the computed styles for a given element. | :--- | :--- | :--- | | Not specified | CSSStyleDeclaration | The computed styles of the element. | -### [`run()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L827) +### [`run()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L837) pass: number, fail: number }>} -## [TestRunner](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L908) +## [TestRunner](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L918) -### [`constructor(report)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L959) +### [`constructor(report)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L969) @@ -2369,7 +2707,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :---: | :---: | :--- | | report | (lines: string) => void | | true | | -### [`nextId()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L968) +### [`nextId()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L978) @@ -2377,11 +2715,11 @@ Retrieves the computed styles for a given element. | :--- | :--- | :--- | | Not specified | string | | -### [`length()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L975) +### [`length()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L985) -### [`add(name, fn, only)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L985) +### [`add(name, fn, only)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L995) @@ -2391,7 +2729,7 @@ Retrieves the computed styles for a given element. | fn | TestFn | | false | | | only | boolean | | false | | -### [`run()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1007) +### [`run()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L1017) @@ -2399,7 +2737,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :--- | | Not specified | Promise | | -### [`onFinish())`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1054) +### [`onFinish())`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L1064) @@ -2407,7 +2745,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :---: | :---: | :--- | | ) | (result: { total: number, success: number, fail: number | > void} callback | false | | -## [`only(name, fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1082) +## [`only(name, fn)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L1092) @@ -2416,7 +2754,7 @@ Retrieves the computed styles for a given element. | name | string | | false | | | fn | TestFn | | true | | -## [`skip(_name, _fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1092) +## [`skip(_name, _fn)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L1102) @@ -2425,7 +2763,7 @@ Retrieves the computed styles for a given element. | _name | string | | false | | | _fn | TestFn | | true | | -## [`setStrict(strict)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1098) +## [`setStrict(strict)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/test/index.js#L1108) @@ -2437,7 +2775,7 @@ Retrieves the computed styles for a given element. -# [Window](https://github.com/socketsupply/socket/blob/master/api/window.js#L12) +# [window](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L12) Provides ApplicationWindow class and methods @@ -2446,11 +2784,15 @@ Retrieves the computed styles for a given element. `socket:application` methods like `getCurrentWindow`, `createWindow`, `getWindow`, and `getWindows`. -## [ApplicationWindow](https://github.com/socketsupply/socket/blob/master/api/window.js#L34) +## [ApplicationWindow](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L35) Represents a window in the application -### [`index()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L63) +### [`id()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L69) + +The unique ID of this window. + +### [`index()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L77) Get the index of the window @@ -2458,11 +2800,15 @@ Get the index of the window | :--- | :--- | :--- | | Not specified | number | the index of the window | -### [`hotkey()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L70) +### [`hotkey()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L84) + +### [`channel()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L92) -### [`getSize()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L78) +The broadcast channel for this window. + +### [`getSize()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L100) Get the size of the window @@ -2470,7 +2816,15 @@ Get the size of the window | :--- | :--- | :--- | | Not specified | { width: number, height: number | } - the size of the window | -### [`getTitle()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L89) +### [`getPosition()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L111) + +Get the position of the window + +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | { x: number, y: number | } - the position of the window | + +### [`getTitle()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L122) Get the title of the window @@ -2478,7 +2832,7 @@ Get the title of the window | :--- | :--- | :--- | | Not specified | string | the title of the window | -### [`getStatus()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L97) +### [`getStatus()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L130) Get the status of the window @@ -2486,7 +2840,7 @@ Get the status of the window | :--- | :--- | :--- | | Not specified | string | the status of the window | -### [`close()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L105) +### [`close()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L138) Close the window @@ -2494,7 +2848,7 @@ Close the window | :--- | :--- | :--- | | Not specified | Promise | the options of the window | -### [`show()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L120) +### [`show()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L153) Shows the window @@ -2502,7 +2856,7 @@ Shows the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`hide()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L129) +### [`hide()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L162) Hides the window @@ -2510,7 +2864,7 @@ Hides the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`maximize()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L138) +### [`maximize()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L171) Maximize the window @@ -2518,7 +2872,7 @@ Maximize the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`minimize()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L147) +### [`minimize()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L180) Minimize the window @@ -2526,7 +2880,7 @@ Minimize the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`restore()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L156) +### [`restore()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L189) Restore the window @@ -2534,7 +2888,7 @@ Restore the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`setTitle(title)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L166) +### [`setTitle(title)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L199) Sets the title of the window @@ -2546,7 +2900,7 @@ Sets the title of the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`setSize(opts)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L179) +### [`setSize(opts)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L212) Sets the size of the window @@ -2560,7 +2914,21 @@ Sets the size of the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`navigate(path)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L219) +### [`setPosition(opts)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L255) + +Sets the position of the window + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| opts | object | | false | an options object | +| opts.x | number \| string | | true | the x position of the window | +| opts.y | number \| string | | true | the y position of the window | + +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | Promise | | + +### [`navigate(path)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L299) Navigate the window to a given path @@ -2572,7 +2940,7 @@ Navigate the window to a given path | :--- | :--- | :--- | | Not specified | Promise | | -### [`showInspector()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L228) +### [`showInspector()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L308) Opens the Web Inspector for the window @@ -2580,7 +2948,7 @@ Opens the Web Inspector for the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`setBackgroundColor(opts)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L245) +### [`setBackgroundColor(opts)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L325) Sets the background color of the window @@ -2596,7 +2964,15 @@ Sets the background color of the window | :--- | :--- | :--- | | Not specified | Promise | | -### [`setContextMenu(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L255) +### [`getBackgroundColor()`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L334) + +Gets the background color of the window + +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | Promise | | + +### [`setContextMenu(options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L343) Opens a native context menu. @@ -2608,7 +2984,7 @@ Opens a native context menu. | :--- | :--- | :--- | | Not specified | Promise | | -### [`showOpenFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L264) +### [`showOpenFilePicker(options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L352) Shows a native open file dialog. @@ -2620,7 +2996,7 @@ Shows a native open file dialog. | :--- | :--- | :--- | | Not specified | Promise | an array of file paths | -### [`showSaveFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L282) +### [`showSaveFilePicker(options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L370) Shows a native save file dialog. @@ -2632,7 +3008,7 @@ Shows a native save file dialog. | :--- | :--- | :--- | | Not specified | Promise | an array of file paths | -### [`showDirectoryFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L300) +### [`showDirectoryFilePicker(options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L388) Shows a native directory dialog. @@ -2644,9 +3020,9 @@ Shows a native directory dialog. | :--- | :--- | :--- | | Not specified | Promise | an array of file paths | -### [`send(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L325) +### [`send(options)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L413) -This is a high-level API that you should use instead of `ipc.send` when +This is a high-level API that you should use instead of `ipc.request` when you want to send a message to another window or to the backend. @@ -2658,7 +3034,7 @@ This is a high-level API that you should use instead of `ipc.send` when | options.event | string | | false | the event to send | | options.value | string \| object | | true | the value to send | -### [`postMessage(message)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L366) +### [`postMessage(message)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L454) Post a message to a window TODO(@jwerle): research using `BroadcastChannel` instead @@ -2671,19 +3047,32 @@ Post a message to a window | :--- | :--- | :--- | | Not specified | Promise | | -### [`openExternal(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L384) +### [`openExternal(value)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L473) -Opens an URL in the default browser. +Opens an URL in the default application associated with the URL protocol, + such as 'https:' for the default web browser. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| options | object | | false | | +| value | string | | false | | | Return Value | Type | Description | | :--- | :--- | :--- | -| Not specified | Promise | | +| Not specified | Promise<{ url: string | >} | + +### [`revealFile(value)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L488) + +Opens a file in the default file explorer. + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| value | string | | false | | + +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | Promise | | -### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L395) +### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L503) Adds a listener to the window. @@ -2692,7 +3081,7 @@ Adds a listener to the window. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L413) +### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L521) Adds a listener to the window. An alias for `addListener`. @@ -2701,7 +3090,7 @@ Adds a listener to the window. An alias for `addListener`. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L430) +### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L538) Adds a listener to the window. The listener is removed after the first call. @@ -2710,7 +3099,7 @@ Adds a listener to the window. The listener is removed after the first call. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L446) +### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L554) Removes a listener from the window. @@ -2719,7 +3108,7 @@ Removes a listener from the window. | event | string | | false | the event to remove the listener from | | cb | function(*): void | | false | the callback to remove | -### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L459) +### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L567) Removes all listeners from the window. @@ -2727,7 +3116,7 @@ Removes all listeners from the window. | :--- | :--- | :---: | :---: | :--- | | event | string | | false | the event to remove the listeners from | -### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L475) +### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/v0.6.0-next/api/window.js#L583) Removes a listener from the window. An alias for `removeListener`. diff --git a/api/ai.js b/api/ai.js new file mode 100644 index 0000000000..e56a3ae4eb --- /dev/null +++ b/api/ai.js @@ -0,0 +1,199 @@ +// @ts-check +/** + * @module ai + * + * Provides high level classes for common AI tasks. + * + * If you download a model like `mistral-7b-openorca.Q4_0.gguf` from Hugging + * Face, you can construct in JavaScript with a prompt. Prompt syntax isn't + * concrete like programming syntax, so you'll usually want to know what the + * author has to say about prompting, for example this might be worth reading... + * + * https://docs.mistral.ai/guides/prompting_capabilities + * + * Example usage: + * + * ```js + * import { LLM } from 'socket:ai' + * + * const llm = new LLM({ + * path: 'model.gguf', + * prompt: '...' // insert your prompt here. + * }) + * + * llm.on('end', () => { + * // end of the token stream. + * }) + * + * llm.on('data', data => { + * // a new token has arrived in the token stream. + * }) + * ``` + */ +import { EventEmitter } from './events.js' +import { rand64 } from './crypto.js' +import process from './process.js' +import ipc from './ipc.js' +import gc from './gc.js' + +import * as exports from './ai.js' + +/** + * A class to interact with large language models (using llama.cpp) + */ +export class LLM extends EventEmitter { + /** + * Constructs an LLM instance. Each parameter is designed to configure and control + * the behavior of the underlying large language model provided by llama.cpp. + * @param {Object} options - Configuration options for the LLM instance. + * @param {string} options.path - The file path to the model in .gguf format. This model file contains + * the weights and configuration necessary for initializing the language model. + * @param {string} options.prompt - The initial input text to the model, setting the context or query + * for generating responses. The model uses this as a starting point for text generation. + * @param {string} [options.id] - An optional unique identifier for this specific instance of the model, + * useful for tracking or referencing the model in multi-model setups. + * @param {number} [options.n_ctx=1024] - Specifies the maximum number of tokens that the model can consider + * for a single query. This is crucial for managing memory and computational + * efficiency. Exceeding the model's configuration may lead to errors or truncated outputs. + * @param {number} [options.n_threads=8] - The number of threads allocated for the model's computation, + * affecting performance and speed of response generation. + * @param {number} [options.temp=1.1] - Sampling temperature controls the randomness of predictions. + * Higher values increase diversity, potentially at the cost of coherence. + * @param {number} [options.max_tokens=512] - The upper limit on the number of tokens that the model can generate + * in response to a single prompt. This prevents runaway generations. + * @param {number} [options.n_gpu_layers=32] - The number of GPU layers dedicated to the model processing. + * More layers can increase accuracy and complexity of the outputs. + * @param {number} [options.n_keep=0] - Determines how many of the top generated responses are retained after + * the initial generation phase. Useful for models that generate multiple outputs. + * @param {number} [options.n_batch=0] - The size of processing batches. Larger batch sizes can reduce + * the time per token generation by parallelizing computations. + * @param {number} [options.n_predict=0] - Specifies how many forward predictions the model should make + * from the current state. This can pre-generate responses or calculate probabilities. + * @param {number} [options.grp_attn_n=0] - Group attention parameter 'N' modifies how attention mechanisms + * within the model are grouped and interact, affecting the model’s focus and accuracy. + * @param {number} [options.grp_attn_w=0] - Group attention parameter 'W' adjusts the width of each attention group, + * influencing the breadth of context considered by each attention group. + * @param {number} [options.seed=0] - A seed for the random number generator used in the model. Setting this ensures + * consistent results in model outputs, important for reproducibility in experiments. + * @param {number} [options.top_k=0] - Limits the model's output choices to the top 'k' most probable next words, + * reducing the risk of less likely, potentially nonsensical outputs. + * @param {number} [options.tok_p=0.0] - Top-p (nucleus) sampling threshold, filtering the token selection pool + * to only those whose cumulative probability exceeds this value, enhancing output relevance. + * @param {number} [options.min_p=0.0] - Sets a minimum probability filter for token generation, ensuring + * that generated tokens have at least this likelihood of being relevant or coherent. + * @param {number} [options.tfs_z=0.0] - Temperature factor scale for zero-shot learning scenarios, adjusting how + * the model weights novel or unseen prompts during generation. + * @throws {Error} Throws an error if the model path is not provided, as the model cannot initialize without it. + */ + + constructor (options = null) { + super() + + options = { ...options } + if (!options.path) { + throw new Error('expected a path to a valid model (.gguf)') + } + + this.path = options.path + this.prompt = options.prompt + this.id = options.id || rand64() + + const opts = { + id: this.id, + path: this.path, + prompt: this.prompt, + // @ts-ignore + antiprompt: options.antiprompt, + // @ts-ignore + conversation: options.conversation === true, + // @ts-ignore + chatml: options.chatml === true, + // @ts-ignore + instruct: options.instruct === true, + n_ctx: options.n_ctx || 1024, // simplified, assuming default value of 1024 if not specified + n_threads: options.n_threads || 8, + temp: options.temp || 1.1, // assuming `temp` should be a number, not a string + max_tokens: options.max_tokens || 512, + n_gpu_layers: options.n_gpu_layers || 32, + n_keep: options.n_keep || 0, + n_batch: options.n_batch || 0, + n_predict: options.n_predict || 0, + grp_attn_n: options.grp_attn_n || 0, + grp_attn_w: options.grp_attn_w || 0, + seed: options.seed || 0, + top_k: options.top_k || 0, + tok_p: options.tok_p || 0.0, + min_p: options.min_p || 0.0, + tfs_z: options.tfs_z || 0.0 + } + + globalThis.addEventListener('data', event => { + // @ts-ignore + const detail = event.detail + const { err, data, source } = detail.params + + if (err && BigInt(err.id) === this.id) { + return this.emit('error', err) + } + + if (!data || BigInt(data.id) !== this.id) return + + if (source === 'ai.llm.log') { + this.emit('log', data.message) + return + } + + if (source === 'ai.llm.chat') { + if (data.complete) { + return this.emit('end') + } + + this.emit('data', decodeURIComponent(data.token)) + } + }) + + ipc.request('ai.llm.create', opts) + .then((result) => { + if (result.err) { + this.emit('error', result.err) + } + }, (err) => { + this.emit('error', err) + }) + } + + /** + * Tell the LLM to stop after the next token. + * @returns {Promise} A promise that resolves when the LLM stops. + */ + async stop () { + return await ipc.request('ai.llm.stop', { id: this.id }) + } + + /** + * @ignore + */ + [gc.finalizer] (options) { + return { + args: [this.id, options], + async handle (id) { + if (process.env.DEBUG) { + console.warn('Closing LLM on garbage collection') + } + + await ipc.request('ai.llm.destroy', { id }, options) + } + } + } + + /** + * Send a message to the chat. + * @param {string} message - The message to send to the chat. + * @returns {Promise} A promise that resolves with the response from the chat. + */ + async chat (message) { + return await ipc.request('ai.llm.chat', { id: this.id, message }) + } +} + +export default exports diff --git a/api/application.js b/api/application.js index 312a4afcfd..66bbf07aed 100644 --- a/api/application.js +++ b/api/application.js @@ -1,6 +1,7 @@ +/* global Event, MessageEvent */ // @ts-check /** - * @module Application + * @module application * * Provides Application level methods * @@ -10,15 +11,195 @@ * ``` */ +import { ApplicationURLEvent } from './internal/events.js' import ApplicationWindow, { formatURL } from './window.js' import { isValidPercentageValue } from './util.js' import ipc, { primordials } from './ipc.js' import menu, { setMenu } from './application/menu.js' +import client from './application/client.js' +import hooks from './hooks.js' import os from './os.js' import * as exports from './application.js' -export { menu } +const eventTarget = new EventTarget() + +let isApplicationPaused = false +hooks.onApplicationResume((event) => { + isApplicationPaused = false + eventTarget.dispatchEvent(new Event(event.type, event)) +}) + +hooks.onApplicationPause((event) => { + isApplicationPaused = true + eventTarget.dispatchEvent(new Event(event.type, event)) +}) + +hooks.onApplicationURL((event) => { + eventTarget.dispatchEvent(new ApplicationURLEvent(event.type, event)) +}) + +hooks.onMessage((event) => { + eventTarget.dispatchEvent(new MessageEvent(event.type, event)) +}) + +function serializeConfig (config) { + if (!config || typeof config !== 'object') { + return '' + } + + const entries = [] + for (const key in config) { + entries.push(`${key} = ${config[key]}`) + } + + return entries.join('\n') +} + +export { client, menu } + +// get this from constant value in runtime +export const MAX_WINDOWS = 32 + +export class ApplicationWindowList { + #list = [] + + static from (...args) { + if (Array.isArray(args[0])) { + return new this(args[0]) + } + + return new this(args) + } + + constructor (items) { + if (Array.isArray(items)) { + for (const item of items) { + this.add(item) + } + } + } + + get length () { + return this.#list.length + } + + get size () { + return this.length + } + + get [Symbol.iterator] () { + return this.#list[Symbol.iterator] + } + + forEach (callback, thisArg) { + this.#list.forEach(callback, thisArg) + } + + item (index) { + return this[index] ?? undefined + } + + entries () { + const entries = [] + + for (const item of this.#list) { + entries.push([item.index, item]) + } + + return entries + } + + keys () { + return this.entries().map((entry) => entry[0]) + } + + values () { + return this.entries().map((entry) => entry[1]) + } + + add (window) { + if (Number.isFinite(window.index) && window.index > -1) { + this[window.index] = window + + for (let i = 0; i < this.#list.length; ++i) { + if (this.#list[i].index === window.index) { + this.#list.splice(i, 1) + break + } + } + + this.#list.push(window) + this.#list.sort((a, b) => a.index - b.index) + } + + return this + } + + remove (windowOrIndex) { + let index = -1 + if (Number.isFinite(windowOrIndex) && windowOrIndex > -1) { + index = windowOrIndex + } else { + index = windowOrIndex?.index ?? -1 + } + + if (index > -1) { + delete this[index] + for (let i = 0; i < this.#list.length; ++i) { + if (this.#list[i].index === index) { + this.#list.splice(i, 1) + return true + } + } + } + + return false + } + + contains (windowOrIndex) { + let index = -1 + if (Number.isFinite(windowOrIndex) && windowOrIndex > -1) { + index = windowOrIndex + } else { + index = windowOrIndex?.index ?? -1 + } + + if (index > -1) { + return Boolean(this[index]) + } + + return false + } + + clear () { + for (const item of this.#list) { + delete this[item.index] + } + + this.#list = [] + return this + } +} + +/** + * Add an application event `type` callback `listener` with `options`. + * @param {string} type + * @param {function(Event|MessageEvent|CustomEvent|ApplicationURLEvent): boolean} listener + * @param {{ once?: boolean }|boolean=} [options] + */ +export function addEventListener (type, listener, options = null) { + return eventTarget.addEventListener(type, listener, options) +} + +/** + * Remove an application event `type` callback `listener` with `options`. + * @param {string} type + * @param {function(Event|MessageEvent|CustomEvent|ApplicationURLEvent): boolean} listener + */ +export function removeEventListener (type, listener) { + return eventTarget.removeEventListener(type, listener) +} /** * Returns the current window index @@ -31,9 +212,19 @@ export function getCurrentWindowIndex () { /** * Creates a new window and returns an instance of ApplicationWindow. * @param {object} opts - an options object - * @param {number} opts.index - the index of the window - * @param {string} opts.path - the path to the HTML file to load into the window - * @param {string=} opts.title - the title of the window + * @param {string=} opts.aspectRatio - a string (split on ':') provides two float values which set the window's aspect ratio. + * @param {boolean=} opts.closable - deterime if the window can be closed. + * @param {boolean=} opts.minimizable - deterime if the window can be minimized. + * @param {boolean=} opts.maximizable - deterime if the window can be maximized. + * @param {number} [opts.margin] - a margin around the webview. (Private) + * @param {number} [opts.radius] - a radius on the webview. (Private) + * @param {number} opts.index - the index of the window. + * @param {string} opts.path - the path to the HTML file to load into the window. + * @param {string=} opts.title - the title of the window. + * @param {string=} opts.titlebarStyle - determines the style of the titlebar (MacOS only). + * @param {string=} opts.windowControlOffsets - a string (split on 'x') provides the x and y position of the traffic lights (MacOS only). + * @param {string=} opts.backgroundColorDark - determines the background color of the window in dark mode. + * @param {string=} opts.backgroundColorLight - determines the background color of the window in light mode. * @param {(number|string)=} opts.width - the width of the window. If undefined, the window will have the main window width. * @param {(number|string)=} opts.height - the height of the window. If undefined, the window will have the main window height. * @param {(number|string)=} [opts.minWidth = 0] - the minimum width of the window @@ -43,8 +234,10 @@ export function getCurrentWindowIndex () { * @param {boolean=} [opts.resizable=true] - whether the window is resizable * @param {boolean=} [opts.frameless=false] - whether the window is frameless * @param {boolean=} [opts.utility=false] - whether the window is utility (macOS only) - * @param {boolean=} [opts.canExit=false] - whether the window can exit the app + * @param {boolean=} [opts.shouldExitApplicationOnClose=false] - whether the window can exit the app * @param {boolean=} [opts.headless=false] - whether the window will be headless or not (no frame) + * @param {string=} [opts.userScript=null] - A user script that will be injected into the window (desktop only) + * @param {string[]=} [opts.protocolHandlers] - An array of protocol handler schemes to register with the new window (requires service worker) * @return {Promise} */ export async function createWindow (opts) { @@ -59,16 +252,71 @@ export async function createWindow (opts) { index: globalThis.__args.index, title: opts.title ?? '', resizable: opts.resizable ?? true, + closable: opts.closable === true, + maximizable: opts.maximizable ?? true, + minimizable: opts.minimizable ?? true, frameless: opts.frameless ?? false, + aspectRatio: opts.aspectRatio ?? '', + titlebarStyle: opts.titlebarStyle ?? '', + windowControlOffsets: opts.windowControlOffsets ?? '', + backgroundColorDark: opts.backgroundColorDark ?? '', + backgroundColorLight: opts.backgroundColorLight ?? '', utility: opts.utility ?? false, - canExit: opts.canExit ?? false, + shouldExitApplicationOnClose: opts.shouldExitApplicationOnClose ?? false, + /** + * @private + * @type {number} + */ + radius: opts.radius ?? 0, + /** + * @private + * @type {number} + */ + margin: opts.margin ?? 0, minWidth: opts.minWidth ?? 0, minHeight: opts.minHeight ?? 0, maxWidth: opts.maxWidth ?? '100%', maxHeight: opts.maxHeight ?? '100%', headless: opts.headless === true, // @ts-ignore - debug: opts.debug === true // internal + debug: opts.debug === true, // internal + userScript: encodeURIComponent(opts.userScript ?? ''), + // @ts-ignore + __runtime_primordial_overrides__: ( + // @ts-ignore + opts.__runtime_primordial_overrides__ && + // @ts-ignore + typeof opts.__runtime_primordial_overrides__ === 'object' + // @ts-ignore + ? JSON.stringify(opts.__runtime_primordial_overrides__) + : '' + ), + // @ts-ignore + config: typeof opts?.config === 'string' + // @ts-ignore + ? opts.config + // @ts-ignore + : (serializeConfig(opts?.config) ?? '') + } + + if (Array.isArray(opts?.protocolHandlers)) { + for (const protocolHandler of opts.protocolHandlers) { + // @ts-ignore + opts.config[`webview_protocol-handlers_${protocolHandler}`] = '' + } + } else if (opts?.protocolHandlers && typeof opts.protocolHandlers === 'object') { + // @ts-ignore + for (const key in opts.protocolHandlers) { + // @ts-ignore + if (opts.protocolHandlers[key] && typeof opts.protocolHandlers[key] === 'object') { + // @ts-ignore + opts.config[`webview_protocol-handlers_${key}`] = JSON.stringify(opts.protocolHandlers[key]) + // @ts-ignore + } else if (typeof opts.protocolHandlers[key] === 'string') { + // @ts-ignore + opts.config[`webview_protocol-handlers_${key}`] = opts.protocolHandlers[key] + } + } } if ((opts.width != null && typeof opts.width !== 'number' && typeof opts.width !== 'string') || @@ -99,7 +347,7 @@ export async function createWindow (opts) { options.height = opts.height.toString() } - const { data, err } = await ipc.send('window.create', options) + const { data, err } = await ipc.request('window.create', options) if (err) { throw err @@ -113,17 +361,22 @@ export async function createWindow (opts) { * @returns {Promise<{ width: number, height: number }>} */ export async function getScreenSize () { - if (os.platform() === 'ios') { + if (os.platform() === 'android' || os.platform() === 'ios') { return { - width: globalThis.screen.availWidth, - height: globalThis.screen.availHeight + width: globalThis.screen?.availWidth ?? 0, + height: globalThis.screen?.availHeight ?? 0 } } - const { data, err } = await ipc.send('application.getScreenSize', { index: globalThis.__args.index }) - if (err) { - throw err + + const result = await ipc.request('application.getScreenSize', { + index: globalThis.__args.index + }) + + if (result.err) { + throw result.err } - return data + + return result.data } function throwOnInvalidIndex (index) { @@ -135,31 +388,55 @@ function throwOnInvalidIndex (index) { /** * Returns the ApplicationWindow instances for the given indices or all windows if no indices are provided. * @param {number[]} [indices] - the indices of the windows - * @return {Promise>} * @throws {Error} - if indices is not an array of integer numbers + * @return {Promise} */ -export async function getWindows (indices) { - if (os.platform() === 'ios' || os.platform() === 'android') { - return { - 0: new ApplicationWindow({ +export async function getWindows (indices, options = null) { + if (globalThis.RUNTIME_APPLICATION_ALLOW_MULTI_WINDOWS === false) { + return new ApplicationWindowList([ + new ApplicationWindow({ index: 0, - width: globalThis.screen.availWidth, - height: globalThis.screen.availHeight, - title: document.title, + id: globalThis.__args?.client?.id ?? null, + width: globalThis.screen?.availWidth ?? 0, + height: globalThis.screen?.availHeight ?? 0, + title: globalThis.document.title, status: 31 }) - } + ]) } + // TODO: create a local registry and return from it when possible const resultIndices = indices ?? [] + if (!Array.isArray(resultIndices)) { throw new Error('Indices list must be an array of integer numbers') } + for (const index of resultIndices) { throwOnInvalidIndex(index) } - const { data: windows } = await ipc.send('application.getWindows', resultIndices) - return Object.fromEntries(windows.map(window => [Number(window.index), new ApplicationWindow(window)])) + + const result = await ipc.request('application.getWindows', resultIndices) + + if (result.err) { + throw result.err + } + + // 0 indexed based key to `ApplicationWindow` object map + const windows = new ApplicationWindowList() + + if (!Array.isArray(result.data)) { + return windows + } + + for (const data of result.data) { + const max = Number.isFinite(options?.max) ? options.max : MAX_WINDOWS + if (options?.max === false || data.index < max) { + windows.add(new ApplicationWindow(data)) + } + } + + return windows } /** @@ -168,9 +445,9 @@ export async function getWindows (indices) { * @throws {Error} - if index is not a valid integer number * @returns {Promise} - the ApplicationWindow instance or null if the window does not exist */ -export async function getWindow (index) { +export async function getWindow (index, options) { throwOnInvalidIndex(index) - const windows = await getWindows([index]) + const windows = await getWindows([index], options) return windows[index] } @@ -179,7 +456,7 @@ export async function getWindow (index) { * @return {Promise} */ export async function getCurrentWindow () { - return getWindow(globalThis.__args.index) + return await getWindow(globalThis.__args.index, { max: false }) } /** @@ -188,7 +465,7 @@ export async function getCurrentWindow () { * @return {Promise} */ export async function exit (code = 0) { - const { data, err } = await ipc.send('application.exit', code) + const { data, err } = await ipc.request('application.exit', code) if (err) { throw err } @@ -301,7 +578,15 @@ export async function setTrayMenu (o) { * @return {Promise} */ export async function setSystemMenuItemEnabled (value) { - return await ipc.send('application.setSystemMenuItemEnabled', value) + return await ipc.request('application.setSystemMenuItemEnabled', value) +} + +/** + * Predicate function to determine if application is in a "paused" state. + * @return {boolean} + */ +export function isPaused () { + return isApplicationPaused } /** diff --git a/api/application/client.js b/api/application/client.js new file mode 100644 index 0000000000..f2086e1607 --- /dev/null +++ b/api/application/client.js @@ -0,0 +1,95 @@ +import location from '../location.js' + +/** + * @typedef {{ + * id?: string | null, + * type?: 'window' | 'worker', + * parent?: object | null, + * top?: object | null, + * frameType?: 'top-level' | 'nested' | 'none' + * }} ClientState + */ + +export class Client { + /** + * @type {ClientState} + */ + #state = null + + /** + * `Client` class constructor + * @private + * @param {ClientState} state + */ + constructor (state) { + this.#state = state + } + + /** + * The unique ID of the client. + * @type {string|null} + */ + get id () { + return this.#state?.id ?? null + } + + /** + * The frame type of the client. + * @type {'top-level'|'nested'|'none'} + */ + get frameType () { + return this.#state?.frameType ?? 'none' + } + + /** + * The type of the client. + * @type {'window'|'worker'} + */ + get type () { + return this.#state?.type ?? '' + } + + /** + * The parent client of the client. + * @type {Client|null} + */ + get parent () { + return this.#state?.parent + ? new Client(this.#state.parent) + : null + } + + /** + * The top client of the client. + * @type {Client|null} + */ + get top () { + return this.#state?.top + ? new Client(this.#state.top) + : null + } + + /** + * A readonly `URL` of the current location of this client. + * @type {URL} + */ + get location () { + return new URL(globalThis.RUNTIME_WORKER_LOCATION ?? location.href) + } + + /** + * Converts this `Client` instance to JSON. + * @return {object} + */ + toJSON () { + return { + id: this.id, + frameType: this.frameType, + type: this.type, + location: this.location.toString() + } + } +} + +// @ts-ignore +export default new Client(globalThis.__args?.client ?? {}) diff --git a/api/application/menu.js b/api/application/menu.js index 3a3ad264a6..0237d58f45 100644 --- a/api/application/menu.js +++ b/api/application/menu.js @@ -1,7 +1,10 @@ /* global ErrorEvent */ import { MenuItemEvent } from '../internal/events.js' +import { Deferred } from '../async.js' import ipc from '../ipc.js' +let contextMenuDeferred = null + /** * Helper for getting the current window index. * @ignore @@ -50,23 +53,19 @@ export class Menu extends EventTarget { constructor (type) { super() this.#type = type - this.#channel = new BroadcastChannel(`application.menu.${type}`) - + this.#channel = new BroadcastChannel(`socket.runtime.application.menu.${type}`) this.#channel.addEventListener('message', (event) => { this.dispatchEvent(new MenuItemEvent('menuitem', event.data, this)) }) + } - // forward selection to other windows - this.addEventListener('menuitem', (event) => { - this.#channel.postMessage({ - ...event.data, - source: { - window: { - index: getCurrentWindowIndex() - } - } - }) - }) + /** + * The broadcast channel for this menu. + * @ignore + * @type {BroadcastChannel} + */ + get channel () { + return this.#channel } /** @@ -264,15 +263,26 @@ export class MenuContainer extends EventTarget { this.#system = options?.system ?? null this.#context = options?.context ?? null - if (sourceEventTarget) { - sourceEventTarget.addEventListener('menuItemSelected', (event) => { - const detail = event.detail ?? {} - const menu = this[detail.type ?? ''] - if (menu) { - menu.dispatchEvent(new MenuItemEvent('menuitem', detail, menu)) - } - }) - } + sourceEventTarget.addEventListener('menuItemSelected', (event) => { + if (contextMenuDeferred) { + contextMenuDeferred.resolve({ data: event.detail.parent }) + } + + const detail = event.detail ?? {} + const menu = this[detail.type ?? ''] + + if (menu) { + menu.dispatchEvent(new MenuItemEvent('menuitem', detail, menu)) + menu.channel.postMessage({ + ...detail, + source: { + window: { + index: getCurrentWindowIndex() + } + } + }) + } + }) if (this.#tray) { this.#tray.addEventListener('menuitem', (event) => { @@ -535,16 +545,45 @@ export async function setMenu (options, type) { * @ignore */ export async function setContextMenu (options) { - const o = Object - .entries(options) - .flatMap(a => a.join(':')) - .join('_') + const lines = options.value.split('\n') + const e = new Error() + const frame = e.stack.split('\n')[2] + const callerLineNo = frame.split(':').reverse()[1] + + let err + let lineText + + for (let i = 0; i < lines.length; i++) { + lineText = lines[i].trim() + + if (!lineText.length) continue + if (lineText.includes('---')) continue + if (!lineText.includes(':')) { + err = 'Expected separator (:)' + } + + const parts = lineText.split(':') + if (!parts[0].trim()) { + err = 'Expected label' + } + + if (!parts[0].trim()) { + err = 'Expected accelerator' + } + + if (err) { + const lineNo = Number(callerLineNo) + i + return ipc.Result.from({ err: new Error(`${err} on line ${lineNo}: "${lineText}"`) }) + } + } + + contextMenuDeferred = new Deferred() - const { data, err } = await ipc.send('window.setContextMenu', o) + const result = await ipc.send('window.setContextMenu', options) - if (err) { - throw err + if (result && result.err) { + return { err: result.err } } - return data + return await contextMenuDeferred } diff --git a/api/assert.js b/api/assert.js new file mode 100644 index 0000000000..207ea63234 --- /dev/null +++ b/api/assert.js @@ -0,0 +1,127 @@ +import fastDeepEqual from './test/fast-deep-equal.js' +import util from './util.js' + +export class AssertionError extends Error { + actual = null + expected = null + operator = null + + constructor (options) { + super(options.message) + } +} + +export function assert (value, message = null) { + if (value === undefined) { + throw new AssertionError({ + operator: '==', + message: 'No value argument passed to `assert()`', + actual: value, + expected: true + }) + } else if (!value) { + throw new AssertionError({ + operator: '==', + message: message || 'The expression evaluated to a falsy value:', + actual: value, + expected: true + }) + } +} + +export function ok (value, message = null) { + if (value === undefined) { + throw new AssertionError({ + operator: '==', + message: 'No value argument passed to `assert.ok()`', + actual: value, + expected: true + }) + } else if (!value) { + throw new AssertionError({ + operator: '==', + message: message || 'The expression evaluated to a falsy value:', + actual: value, + expected: true + }) + } +} + +export function equal (actual, expected, message = null) { + // eslint-disable-next-line + if (actual != expected) { + throw new AssertionError({ + operator: '==', + message: message || `${util.inspect(actual)} == ${util.inspect(expected)}`, + actual, + expected + }) + } +} + +export function notEqual (actual, expected, message = null) { + // eslint-disable-next-line + if (actual == expected) { + throw new AssertionError({ + operator: '!=', + message: message || `${util.inspect(actual)} != ${util.inspect(expected)}`, + actual, + expected + }) + } +} + +export function strictEqual (actual, expected, message = null) { + if (actual !== expected) { + throw new AssertionError({ + operator: '===', + message: message || `${util.inspect(actual)} === ${util.inspect(expected)}`, + actual, + expected + }) + } +} + +export function notStrictEqual (actual, expected, message = null) { + if (actual === expected) { + throw new AssertionError({ + operator: '!==', + message: message || `${util.inspect(actual)} !== ${util.inspect(expected)}`, + actual, + expected + }) + } +} + +export function deepEqual (actual, expected, message = null) { + if (fastDeepEqual(actual, expected)) { + throw new AssertionError({ + operator: '==', + message: message || `${util.inspect(actual)} == ${util.inspect(expected)}`, + actual, + expected + }) + } +} + +export function notDeepEqual (actual, expected, message = null) { + if (!fastDeepEqual(actual, expected)) { + throw new AssertionError({ + operator: '!=', + message: message || `${util.inspect(actual)} != ${util.inspect(expected)}`, + actual, + expected + }) + } +} + +export default Object.assign(assert, { + AssertionError, + ok, + equal, + notEqual, + strictEqual, + notStrictEqual, + deepEqual, + notDeepEqual +}) diff --git a/api/async.js b/api/async.js new file mode 100644 index 0000000000..0c4e8ba6d4 --- /dev/null +++ b/api/async.js @@ -0,0 +1,37 @@ +/** + * @module async + * + * Various primitives for async hooks, storage, resources, and contexts. + */ +import AsyncLocalStorage from './async/storage.js' +import AsyncResource from './async/resource.js' +import AsyncContext from './async/context.js' +import Deferred from './async/deferred.js' + +import { + executionAsyncResource, + executionAsyncId, + triggerAsyncId, + createHook, + AsyncHook +} from './async/hooks.js' + +import * as exports from './async.js' + +export { + // async resources/storages + AsyncLocalStorage, + AsyncResource, + // AsyncContext + AsyncContext, + // deferred + Deferred, + // async hooks + executionAsyncResource, + executionAsyncId, + triggerAsyncId, + createHook, + AsyncHook +} + +export default exports diff --git a/api/async/context.js b/api/async/context.js new file mode 100644 index 0000000000..7add8bbcad --- /dev/null +++ b/api/async/context.js @@ -0,0 +1,524 @@ +/** + * @module async.context + * + * Async Context for JavaScript based on the TC39 proposal. + * + * Example usage: + * ```js + * // `AsyncContext` is also globally available as `globalThis.AsyncContext` + * import AsyncContext from 'socket:async/context' + * + * const var = new AsyncContext.Variable() + * var.run('top', () => { + * console.log(var.get()) // 'top' + * queueMicrotask(() => { + * var.run('nested', () => { + * console.log(var.get()) // 'nested' + * }) + * }) + * }) + * ``` + * + * @see {@link https://tc39.es/proposal-async-context} + * @see {@link https://github.com/tc39/proposal-async-context} + */ + +/** + * @template T + * @typedef {{ + * name?: string, + * defaultValue?: T + * }} VariableOptions + */ + +/** + * @callback AnyFunc + * @template T + * @this T + * @param {...any} args + * @returns {any} + */ + +/** + * `FrozenRevert` holds a frozen Mapping that will be simply restored + * when the revert is run. + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/fork.ts} + */ +export class FrozenRevert { + /** + * The (unchanged) mapping for this `FrozenRevert`. + * @type {Mapping} + */ + #mapping + + /** + * `FrozenRevert` class constructor. + * @param {Mapping} mapping + */ + constructor (mapping) { + this.#mapping = mapping + } + + /** + * Restores (unchaged) mapping from this `FrozenRevert`. This function is + * called by `AsyncContext.Storage` when it reverts a current mapping to the + * previous state before a "fork". + * @param {Mapping=} [unused] + * @return {Mapping} + */ + restore (unused = null) { + // eslint-disable-next-line + void unused; + return this.#mapping + } +} + +/** + * Revert holds the state on how to revert a change to the + * `AsyncContext.Storage` current `Mapping` + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/fork.ts} + * @template T + */ +export class Revert { + /** + * @type {Variable} + */ + #key + + /** + * @type {boolean} + */ + #hasVariable = false + + /** + * @type {T|undefined} + */ + #previousVariable = undefined + + /** + * `Revert` class constructor. + * @param {Mapping} mapping + * @param {Variable} key + */ + constructor (mapping, key) { + this.#key = key + this.#hasVariable = mapping.has(key) + this.#previousVariable = mapping.get(key) + } + + /** + * @type {T|undefined} + */ + get previousVariable () { + return this.#previousVariable + } + + /** + * Restores a mapping from this `Revert`. This function is called by + * `AsyncContext.Storage` when it reverts a current mapping to the + * previous state before a "fork". + * @param {Mapping} current + * @return {Mapping} + */ + restore (current) { + if (this.#hasVariable) { + return current.set(this.#key, this.#previousVariable) + } + + return current.delete(this.#key) + } +} + +/** + * A container for all `AsyncContext.Variable` instances and snapshot state. + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/mapping.ts} + */ +export class Mapping { + /** + * The internal mapping data. + * @type {Map, any>} + */ + #data = null + + /** + * `true` if the `Mapping` is frozen, otherwise `false`. + * @type {boolean} + */ + #frozen = false + + /** + * `Mapping` class constructor. + * @param {Map, any>} data + */ + constructor (data) { + this.#data = data + } + + /** + * Freezes the `Mapping` preventing `AsyncContext.Variable` modifications with + * `set()` and `delete()`. + */ + freeze () { + this.#frozen = true + } + + /** + * Returns `true` if the `Mapping` is frozen, otherwise `false`. + * @return {boolean} + */ + isFrozen () { + return this.#frozen + } + + /** + * Optionally returns a new `Mapping` if the current one is "frozen", + * otherwise it just returns the current instance. + * @return {Mapping} + */ + fork () { + if (this.#frozen) { + return new this.constructor(new Map(this.#data)) + } + + return this + } + + /** + * Returns `true` if the `Mapping` has a `AsyncContext.Variable` at `key`, + * otherwise `false. + * @template T + * @param {Variable} key + * @return {boolean} + */ + has (key) { + return this.#data.has(key) + } + + /** + * Gets an `AsyncContext.Variable` value at `key`. If not set, this function + * returns `undefined`. + * @template T + * @param {Variable} key + * @return {boolean} + */ + get (key) { + return this.#data.get(key) + } + + /** + * Sets an `AsyncContext.Variable` value at `key`. If the `Mapping` is frozen, + * then a "forked" (new) instance with the value set on it is returned, + * otherwise the current instance. + * @template T + * @param {Variable} key + * @param {T} value + * @return {Mapping} + */ + set (key, value) { + const mapping = this.fork() + mapping.#data.set(key, value) + return mapping + } + + /** + * Delete an `AsyncContext.Variable` value at `key`. + * If the `Mapping` is frozen, then a "forked" (new) instance is returned, + * otherwise the current instance. + * @template T + * @param {Variable} key + * @param {T} value + * @return {Mapping} + */ + delete (key) { + const mapping = this.fork() + mapping.#data.delete(key) + return mapping + } +} + +/** + * A container of all `AsyncContext.Variable` data. + * @ignore + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/storage.ts} + */ +export class Storage { + /** + * The current `Mapping` for this `AsyncContext`. + * @type {Mapping} + */ + static #current = new Mapping(new Map()) + + /** + * Returns `true` if the current `Mapping` has a + * `AsyncContext.Variable` at `key`, + * otherwise `false. + * @template T + * @param {Variable} key + * @return {boolean} + */ + static has (key) { + return this.#current.has(key) + } + + /** + * Gets an `AsyncContext.Variable` value at `key` for the current `Mapping`. + * If not set, this function returns `undefined`. + * @template T + * @param {Variable} key + * @return {T|undefined} + */ + static get (key) { + return this.#current.get(key) + } + + /** + * Set updates the `AsyncContext.Variable` with a new value and returns a + * revert action that allows the modification to be reversed in the future. + * @template T + * @param {Variable} key + * @param {T} value + * @return {Revert|FrozenRevert} + */ + static set (key, value) { + const revert = this.#current.isFrozen() + ? new FrozenRevert(this.#current) + : /** @type {Revert} */ (new Revert(this.#current, key)) + this.#current = this.#current.set(key, value) + return revert + } + + /** + * "Freezes" the current storage `Mapping`, and returns a new `FrozenRevert` + * or `Revert` which can restore the storage state to the state at + * the time of the snapshot. + * @return {FrozenRevert} + */ + static snapshot () { + this.#current.freeze() + return new FrozenRevert(this.#current) + } + + /** + * Restores the storage `Mapping` state to state at the time the + * "revert" (`FrozenRevert` or `Revert`) was created. + * @template T + * @param {Revert|FrozenRevert} revert + */ + static restore (revert) { + this.#current = revert.restore(this.#current) + } + + /** + * Switches storage `Mapping` state to the state at the time of a + * "snapshot". + * @param {FrozenRevert} snapshot + * @return {FrozenRevert} + */ + static switch (snapshot) { + const previous = this.#current + this.#current = snapshot.restore(previous) + return new FrozenRevert(previous) + } +} + +/** + * `AsyncContext.Variable` is a container for a value that is associated with + * the current execution flow. The value is propagated through async execution + * flows, and can be snapshot and restored with Snapshot. + * @template T + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextvariable} + */ +export class Variable { + /** + * The name of this async context variable. + * @ignore + * @type {string} + */ + #name = '' + + /** + * The default value of this async context variable. + * @ignore + * @type {T|undefined} + */ + #defaultValue = undefined + + /** + * @type {FrozenRevert|Revert|undefined} + */ + #revert = undefined + + /** + * `Variable` class constructor. + * @param {VariableOptions=} [options] + */ + constructor (options = null) { + if (options?.name && typeof options?.name === 'string') { + this.#name = options.name + } + + this.#defaultValue = options?.defaultValue + } + + /** + * @ignore + */ + get defaultValue () { return this.#defaultValue } + set defaultValue (defaultValue) { + this.#defaultValue = defaultValue + } + + /** + * @ignore + */ + get revert () { + return this.#revert + } + + /** + * The name of this async context variable. + * @type {string} + */ + get name () { + return this.#name + } + + /** + * Executes a function `fn` with specified arguments, + * setting a new value to the current context before the call, + * and ensuring the environment is reverted back afterwards. + * The function allows for the modification of a specific context's + * state in a controlled manner, ensuring that any changes can be undone. + * @template T, F extends AnyFunc + * @param {T} value + * @param {F} fn + * @param {...Parameters} args + * @returns {ReturnType} + */ + run (value, fn, ...args) { + const revert = Storage.set(this, value) + this.#revert = revert + try { + return Reflect.apply(fn, null, args) + } finally { + Storage.restore(revert) + if (this.#revert === revert) { + this.#revert = null + } + } + } + + /** + * Get the `AsyncContext.Variable` value. + * @template T + * @return {T|undefined} + */ + get () { + return Storage.has(this) ? Storage.get(this) : this.#defaultValue + } +} + +/** + * `AsyncContext.Snapshot` allows you to opaquely capture the current values of + * all `AsyncContext.Variable` instances and execute a function at a later time + * as if those values were still the current values (a snapshot and restore). + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextsnapshot} + */ +export class Snapshot { + #snapshot = Storage.snapshot() + + /** + * Wraps a given function `fn` with additional logic to take a snapshot of + * `Storage` before invoking `fn`. Returns a new function with the same + * signature as `fn` that when called, will invoke `fn` with the current + * `this` context and provided arguments, after restoring the `Storage` + * snapshot. + * + * `AsyncContext.Snapshot.wrap` is a helper which captures the current values + * of all Variables and returns a wrapped function. When invoked, this + * wrapped function restores the state of all Variables and executes the + * inner function. + * + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextsnapshotwrap} + * + * @template F + * @param {F} fn + * @returns {F} + */ + static wrap (fn) { + const name = fn.name || 'anonymous' + const snapshot = Storage.snapshot() + const container = { + /** + * @this {ThisType} + * @param {...any} args + * @returns {ReturnType} + */ + [name] (...args) { + return run(fn, this, args, snapshot) + } + } + + return /** @type {F} */ (container[name]) + } + + /** + * Runs the given function `fn` with arguments `args`, using a `null` + * context and the current snapshot. + * + * @template F extends AnyFunc + * @param {F} fn + * @param {...Parameters} args + * @returns {ReturnType} + */ + run (fn, ...args) { + return run(fn, null, args, this.#snapshot) + } +} + +/** + * Runs the given function `fn` with the provided `context` and `args`, + * ensuring the environment is set to a specific `snapshot` before execution, + * and reverted back afterwards. + * + * @template F extends AnyFunc + * @param {F} fn + * @param {ThisType} context + * @param {any[]} args + * @param {FrozenRevert} snapshot + * @returns {ReturnType} + */ +function run (fn, context, args, snapshot) { + const revert = Storage.switch(snapshot) + + try { + return Reflect.apply(fn, context, args) + } finally { + Storage.restore(revert) + } +} + +/** + * `AsyncContext` container. + */ +export class AsyncContext { + /** + * `AsyncContext.Variable` is a container for a value that is associated with + * the current execution flow. The value is propagated through async execution + * flows, and can be snapshot and restored with Snapshot. + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextvariable} + * @type {typeof Variable} + */ + static Variable = Variable + + /** + * `AsyncContext.Snapshot` allows you to opaquely capture the current values of + * all `AsyncContext.Variable` instances and execute a function at a later time + * as if those values were still the current values (a snapshot and restore). + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextsnapshot} + * @type {typeof Snapshot} + */ + static Snapshot = Snapshot +} + +export default AsyncContext diff --git a/api/async/deferred.js b/api/async/deferred.js new file mode 100644 index 0000000000..e9b52b64f7 --- /dev/null +++ b/api/async/deferred.js @@ -0,0 +1,151 @@ +import { ErrorEvent } from '../events.js' + +/** + * Dispatched when a `Deferred` internal promise is resolved. + */ +export class DeferredResolveEvent extends Event { + /** + * The `Deferred` promise result value. + * @type {any?} + */ + result = null + + /** + * `DeferredResolveEvent` class constructor + * @ignore + * @param {string=} [type] + * @param {any=} [result] + */ + constructor (type = 'resolve', result = null) { + super(type) + this.result = result + } +} + +/** + * Dispatched when a `Deferred` internal promise is rejected. + */ +export class DeferredRejectEvent extends ErrorEvent { + /** + * `DeferredRejectEvent` class constructor + * @ignore + * @param {string=} [type] + * @param {Error=} [error] + */ + constructor (type = 'reject', error = null) { + super(type, { error }) + } +} + +/** + * A utility class for creating deferred promises. + */ +export class Deferred extends EventTarget { + /** + * @type {Promise?} + */ + #promise = null + + /** + * Function to resolve the associated promise. + * @type {function} + */ + resolve = null + + /** + * Function to reject the associated promise. + * @type {function} + */ + reject = null + + /** + * `Deferred` class constructor. + * @param {Deferred|Promise?} [promise] + */ + constructor (promise) { + super() + + this.#promise = new Promise((resolve, reject) => { + this.resolve = (value) => { + try { + resolve(value) + return this.promise + } finally { + this.dispatchEvent(new DeferredResolveEvent('resolve', value)) + } + } + + this.reject = (error) => { + try { + reject(error) + return this.promise + } finally { + this.dispatchEvent(new DeferredRejectEvent('reject', error)) + } + } + }) + + if (typeof promise?.then === 'function') { + const p = this.#promise + this.#promise = promise.then(() => p) + } + + this.then = this.then.bind(this) + this.catch = this.catch.bind(this) + this.finally = this.finally.bind(this) + } + + /** + * A string representation of this Deferred instance. + * @type {string} + * @ignore + */ + get [Symbol.toStringTag] () { + return 'Promise' + } + + /** + * The promise associated with this Deferred instance. + * @type {Promise} + */ + get promise () { + return this.#promise + } + + /** + * Attaches a fulfillment callback and a rejection callback to the promise, + * and returns a new promise resolving to the return value of the called + * callback. + * @param {function(any)=} [resolve] + * @param {function(Error)=} [reject] + */ + then (resolve, reject) { + if (resolve && reject) { + return this.promise.then(resolve, reject) + } else if (resolve) { + return this.promise.then(resolve) + } else { + return this.promise.then() + } + } + + /** + * Attaches a rejection callback to the promise, and returns a new promise + * resolving to the return value of the callback if it is called, or to its + * original fulfillment value if the promise is instead fulfilled. + * @param {function(Error)=} [callback] + */ + catch (callback) { + return this.promise.catch(callback) + } + + /** + * Attaches a callback for when the promise is settled (fulfilled or rejected). + * @param {function(any?)} [callback] + */ + finally (callback) { + return this.promise.finally(callback) + } +} + +export default Deferred diff --git a/api/async/hooks.js b/api/async/hooks.js new file mode 100644 index 0000000000..77cbda3a84 --- /dev/null +++ b/api/async/hooks.js @@ -0,0 +1,178 @@ +/** + * @module async.hooks + * + * Primitives for hooks into async lifecycles such as `queueMicrotask`, + * `setTimeout`, `setInterval`, and `Promise` built-ins as well as user defined + * async resources. + */ +import { + executionAsyncResource, + executionAsyncId, + triggerAsyncId, + hooks +} from '../internal/async/hooks.js' + +export { + executionAsyncResource, + executionAsyncId, + triggerAsyncId +} + +/** + * A container for `AsyncHooks` callbacks. + * @ignore + */ +export class AsyncHookCallbacks { + /** + * `AsyncHookCallbacks` class constructor. + * @ignore + * @param {AsyncHookCallbackOptions} [options] + */ + constructor (options = null) { + if (typeof options?.init === 'function') { + this.init = options.init + } + + if (typeof options?.before === 'function') { + this.before = options.before + } + + if (typeof options?.after === 'function') { + this.after = options.after + } + + if (typeof options?.destroy === 'function') { + this.destroy = options.destroy + } + } + + init (asyncId, type, triggerAsyncId, resource) { + // noop + } + + before (asyncId) { + // noop + } + + after (asyncId) { + // noop + } + + destroy (asyncId) { + // noop + } + + promiseResolve (asyncId) { + // noop + } +} + +/** + * A container for registering various callbacks for async resource hooks. + */ +export class AsyncHook { + /** + * @type {AsyncHookCallbacks} + */ + #callbacks + + /** + * @type {boolean} + */ + #enabled = false + + /** + * @param {AsyncHookCallbackOptions|AsyncHookCallbacks=} [options] + */ + constructor (callbacks = null) { + this.#callbacks = new AsyncHookCallbacks(callbacks) + } + + /** + * @type {boolean} + */ + get enabled () { + return this.#enabled + } + + /** + * Enable the async hook. + * @return {AsyncHook} + */ + enable () { + if (this.#enabled) { + return this + } + + const { init, before, after, destroy, promiseResolve } = this.#callbacks + + if (!hooks.init.includes(init)) { + hooks.init.push(init) + } + + if (!hooks.before.includes(before)) { + hooks.before.push(before) + } + + if (!hooks.after.includes(after)) { + hooks.after.push(after) + } + + if (!hooks.destroy.includes(destroy)) { + hooks.destroy.push(destroy) + } + + if (!hooks.promiseResolve.includes(promiseResolve)) { + hooks.promiseResolve.push(promiseResolve) + } + + this.#enabled = true + return this + } + + /** + * Disables the async hook + * @return {AsyncHook} + */ + disable () { + if (!this.#enabled) { + return this + } + + const { init, before, after, destroy, promiseResolve } = this.#callbacks + + if (hooks.init.includes(init)) { + hooks.init.splice(hooks.init.indexOf(init), 1) + } + + if (hooks.before.includes(before)) { + hooks.before.splice(hooks.before.indexOf(before), 1) + } + + if (hooks.after.includes(after)) { + hooks.after.splice(hooks.after.indexOf(after), 1) + } + + if (hooks.destroy.includes(destroy)) { + hooks.destroy.splice(hooks.destroy.indexOf(destroy), 1) + } + + if (hooks.promiseResolve.includes(promiseResolve)) { + hooks.promiseResolve.splice(hooks.promiseResolve.indexOf(promiseResolve), 1) + } + + this.#enabled = false + return this + } +} + +/** + * Factory for creating a `AsyncHook` instance. + * @param {AsyncHookCallbackOptions|AsyncHookCallbacks=} [callbacks] + * @return {AsyncHook} + */ +export function createHook (callbacks) { + return new AsyncHook(callbacks) +} + +export default createHook diff --git a/api/async/resource.js b/api/async/resource.js new file mode 100644 index 0000000000..7bc30bbe93 --- /dev/null +++ b/api/async/resource.js @@ -0,0 +1,126 @@ +/** + * @module async.resource + * + * Primitives for creating user defined async resources that implement + * async hooks. + */ +import { + CoreAsyncResource, + executionAsyncResource, + executionAsyncId, + triggerAsyncId +} from '../internal/async/hooks.js' + +export { + executionAsyncResource, + executionAsyncId, + triggerAsyncId +} + +/** + * @typedef {{ + * triggerAsyncId?: number, + * requireManualDestroy?: boolean + * }} AsyncResourceOptions + */ + +/** + * A container that should be extended that represents a resource with + * an asynchronous execution context. + */ +export class AsyncResource extends CoreAsyncResource { + /** + * Binds function `fn` with an optional this `thisArg` binding to run + * in the execution context of an anonymous `AsyncResource`. + * @param {function} fn + * @param {object|string=} [type] + * @param {object=} [thisArg] + * @return {function} + */ + static bind (fn, type, thisArg) { + if (typeof type === 'object') { + thisArg = type + type = fn.name + } + + type = type || fn.name || 'bound-anonymous-function' + const resource = new AsyncResource(type) + return resource.bind(fn, thisArg) + } + + /** + * `AsyncResource` class constructor. + * @param {string} type + * @param {AsyncResourceOptions|number=} [options] + */ + constructor (type, options = null) { + super(type, options) + } + + /** + * The `AsyncResource` type. + * @type {string} + */ + get type () { + return super.type + } + + /** + *`true` if the `AsyncResource` was destroyed, otherwise `false`. This + * value is only set to `true` if `emitDestroy()` was called, likely from + * d + * @type {boolean} + * @ignore + */ + get destroyed () { + return super.destroyed + } + + /** + * The unique async resource ID. + * @return {number} + */ + asyncId () { + return super.asyncId() + } + + /** + * The trigger async resource ID. + * @return {number} + */ + triggerAsyncId () { + return super.triggerAsyncId() + } + + /** + * Manually emits destroy hook for the resource. + * @return {AsyncResource} + */ + emitDestroy () { + return super.emitDestroy() + } + + /** + * Binds function `fn` with an optional this `thisArg` binding to run + * in the execution context of this `AsyncResource`. + * @param {function} fn + * @param {object=} [thisArg] + * @return {function} + */ + bind (fn, thisArg = undefined) { + return super.bind(fn, thisArg) + } + + /** + * Runs function `fn` in the execution context of this `AsyncResource`. + * @param {function} fn + * @param {object=} [thisArg] + * @param {...any} [args] + * @return {any} + */ + runInAsyncScope (fn, thisArg, ...args) { + return super.runInAsyncScope(fn, thisArg, ...args) + } +} + +export default AsyncResource diff --git a/api/async/storage.js b/api/async/storage.js new file mode 100644 index 0000000000..4541287fed --- /dev/null +++ b/api/async/storage.js @@ -0,0 +1,138 @@ +/** + * @module async.storage + * + * Primitives for creating user defined async storage contexts. + */ +import { Snapshot, Variable } from './context.js' +import { AsyncResource } from './resource.js' +import { createHook } from './hooks.js' +import { executionAsyncResource } from '../internal/async/hooks.js' + +const storages = [] + +// eslint-disable-next-line +const asyncLocalStorageHooks = createHook({ + init (asyncId, type, triggerAsyncId, resource) { + const currentAsyncResource = executionAsyncResource() + // eslint-disable-next-line + for (var i = 0; i < storages.length; ++i) { + storages[i].propagateTriggerResourceStore( + resource, + currentAsyncResource, + type + ) + } + } +}) + +/** + * A container for storing values that remain present during + * asynchronous operations. + */ +export class AsyncLocalStorage { + #store = null + #enabled = false + #variable = new Variable() + + /** + * Binds function `fn` to run in the execution context of an + * anonymous `AsyncResource`. + * @param {function} fn + * @return {function} + */ + static bind (fn) { + return AsyncResource.bind(fn) + } + + /** + * Captures the current async context and returns a function that runs + * a function in that execution context. + * @return {function} + */ + static snapshot () { + // eslint-disable-next-line + return AsyncResource.bind(Snapshot.wrap((cb, ...args) => cb(...args))) + } + + /** + * @type {boolean} + */ + get enabled () { + return this.#enabled + } + + /** + * Disables the `AsyncLocalStorage` instance. When disabled, + * `getStore()` will always return `undefined`. + */ + disable () { + if (this.#enabled) { + this.#enabled = false + const index = storages.indexOf(this) + if (index > -1) { + storages.splice(index, 1) + } + } + } + + /** + * Enables the `AsyncLocalStorage` instance. + */ + enable () { + if (!this.#enabled) { + this.#enabled = true + storages.push(this) + } + } + + /** + * Enables and sets the `AsyncLocalStorage` instance default store value. + * @param {any} store + */ + enterWith (store) { + this.enable() + this.#variable = new Variable({ defaultValue: store }) + } + + /** + * Runs function `fn` in the current asynchronous execution context with + * a given `store` value and arguments given to `fn`. + * @param {any} store + * @param {function} fn + * @param {...any} args + * @return {any} + */ + run (store, fn, ...args) { + this.enable() + return this.#variable.run(store, () => { + return Reflect.apply(fn, null, args) + }) + } + + exit (fn, ...args) { + if (!this.#enabled) { + return Reflect.apply(fn, null, args) + } + + try { + this.disable() + return Reflect.apply(fn, null, args) + } finally { + // revert + this.enable() + } + } + + /** + * If the `AsyncLocalStorage` instance is enabled, it returns the current + * store value for this asynchronous execution context. + * @return {any|undefined} + */ + getStore () { + if (this.#enabled) { + return this.#variable.get() + } + } +} + +export default AsyncLocalStorage diff --git a/api/async/wrap.js b/api/async/wrap.js new file mode 100644 index 0000000000..02a62cd166 --- /dev/null +++ b/api/async/wrap.js @@ -0,0 +1,67 @@ +import { Snapshot } from './context.js' + +export const symbol = Symbol.for('socket.runtime.async') + +/** + * Returns `true` if a given function `fn` has the "async" wrapped tag, + * meaning it was "tagged" in a `wrap(fn)` call before, otherwise this + * function will return `false`. + * @ignore + * @param {function} fn + * @param {boolean} + */ +export function isTagged (fn) { + if (typeof fn === 'function' && fn[symbol] === true) { + return true + } + + return false +} + +/** + * Tags a function `fn` as being "async wrapped" so subsequent calls to + * `wrap(fn)` do not wrap an already wrapped function. + * @ignore + * @param {function} fn + * @return {function} + */ +export function tag (fn) { + if (typeof fn !== 'function') { + return fn + } + + if (fn[symbol] === true) { + return fn + } + + Object.defineProperty(fn, symbol, { + configurable: false, + enumerable: false, + writable: false, + value: true + }) + + return fn +} + +/** + * Wraps a function `fn` that captures a snapshot of the current async + * context. This function is idempotent and will not wrap a function more + * than once. + * @ignore + * @param {function} fn + * @return {function} + */ +export function wrap (fn) { + if (typeof fn === 'function') { + if (isTagged(fn)) { + return fn + } + + return tag(Snapshot.wrap(fn)) + } + + return fn +} + +export default wrap diff --git a/api/async_hooks.js b/api/async_hooks.js new file mode 100644 index 0000000000..0ea7d3a5e2 --- /dev/null +++ b/api/async_hooks.js @@ -0,0 +1,21 @@ +import { + executionAsyncResource, + executionAsyncId, + triggerAsyncId, + createHook +} from './async/hooks.js' + +import { AsyncLocalStorage } from './async/storage.js' +import { AsyncResource } from './async/resource.js' +import * as exports from './async_hooks.js' + +export { + AsyncLocalStorage, + AsyncResource, + executionAsyncResource, + executionAsyncId, + triggerAsyncId, + createHook +} + +export default exports diff --git a/api/bluetooth.js b/api/bluetooth.js index a762728844..4406282a84 100644 --- a/api/bluetooth.js +++ b/api/bluetooth.js @@ -1,5 +1,5 @@ /** - * @module Bluetooth + * @module bluetooth * * A high-level, cross-platform API for Bluetooth Pub-Sub * diff --git a/api/bootstrap.js b/api/bootstrap.js index e96f9a8632..79568b5491 100644 --- a/api/bootstrap.js +++ b/api/bootstrap.js @@ -1,5 +1,5 @@ /** - * @module Bootstrap + * @module bootstrap * * This module is responsible for downloading the bootstrap file and writing it to disk. * It also provides a function to check the hash of the file on disk. This is used to diff --git a/api/buffer.js b/api/buffer.js index 5c118c21dd..4a1ed1a736 100644 --- a/api/buffer.js +++ b/api/buffer.js @@ -1,5 +1,5 @@ /** - * @module Buffer + * @module buffer * * The buffer module from node.js, for the browser. * @@ -276,6 +276,15 @@ const K_MAX_LENGTH = 0x7fffffff const kMaxLength = K_MAX_LENGTH export default Buffer +export const File = globalThis.File +export const Blob = globalThis.Blob +export const constants = { + MAX_LENGTH: kMaxLength, + MAX_STRING_LENGTH: kMaxLength +} + +export const btoa = globalThis.btoa.bind(globalThis) +export const atob = globalThis.atob.bind(globalThis) export { Buffer, diff --git a/api/child_process.js b/api/child_process.js new file mode 100644 index 0000000000..9470b50e20 --- /dev/null +++ b/api/child_process.js @@ -0,0 +1,704 @@ +/* global ErrorEvent */ +import { AsyncResource } from './async/resource.js' +import { EventEmitter } from './events.js' +import diagnostics from './diagnostics.js' +import { Worker } from './worker_threads.js' +import { Buffer } from './buffer.js' +import { rand64 } from './crypto.js' +import process from './process.js' +import signal from './process/signal.js' +import ipc from './ipc.js' +import gc from './gc.js' +import os from './os.js' + +const dc = diagnostics.channels.group('child_process', [ + 'spawn', + 'close', + 'exit', + 'kill' +]) + +export class Pipe extends AsyncResource { + #process = null + #reading = true + + /** + * `Pipe` class constructor. + * @param {ChildProcess} process + * @ignore + */ + constructor (process) { + super('Pipe') + + this.#process = process + + if (process.stdout) { + const { emit } = process.stdout + process.stdout.emit = (...args) => { + if (!this.reading) return false + return this.runInAsyncScope(() => { + return emit.call(process.stdout, ...args) + }) + } + } + + if (process.stderr) { + const { emit } = process.stderr + process.stderr.emit = (...args) => { + if (!this.reading) return false + return this.runInAsyncScope(() => { + return emit.call(process.stderr, ...args) + }) + } + } + + process.once('close', () => this.destroy()) + process.once('exit', () => this.destroy()) + } + + /** + * `true` if the pipe is still reading, otherwise `false`. + * @type {boolean} + */ + get reading () { + return this.#reading + } + + /** + * @type {import('./process')} + */ + get process () { + return this.#process + } + + /** + * Destroys the pipe + */ + destroy () { + this.#reading = false + } +} + +export class ChildProcess extends EventEmitter { + #id = rand64() + #worker = null + #signal = null + #timeout = null + #resource = null + #env = { ...process.env } + #pipe = null + + #state = { + killed: false, + signalCode: null, + exitCode: null, + spawnfile: null, + spawnargs: [], + lifecycle: 'init', + pid: 0 + } + + /** + * `ChildProcess` class constructor. + * @param {{ + * env?: object, + * stdin?: boolean, + * stdout?: boolean, + * stderr?: boolean, + * signal?: AbortSigal, + * }=} [options] + */ + constructor (options = null) { + super() + + // this does not implement disconnect or message because this is not node + // @ts-ignore + const workerLocation = new URL('./child_process/worker.js', import.meta.url) + + // TODO(@jwerle): support environment variable inject + if (options?.env && typeof options?.env === 'object') { + this.#env = options.env + } + + this.#resource = new AsyncResource('ChildProcess') + this.#resource.handle = this + + this.#resource.runInAsyncScope(() => { + this.#worker = new Worker(workerLocation.toString(), { + env: options?.env ?? {}, + stdin: options?.stdin !== false, + stdout: options?.stdout !== false, + stderr: options?.stderr !== false, + workerData: { id: this.#id } + }) + + this.#pipe = new Pipe(this) + }) + + if (options?.signal) { + this.#signal = options.signal + this.#signal.addEventListener('abort', () => { + this.#resource.runInAsyncScope(() => { + this.emit('error', new Error(this.#signal.reason)) + this.kill(options?.killSignal ?? 'SIGKILL') + }) + }) + } + + if (options?.timeout) { + this.#timeout = setTimeout(() => { + this.#resource.runInAsyncScope(() => { + this.emit('error', new Error('Child process timed out')) + this.kill(options?.killSignal ?? 'SIGKILL') + }) + }, options.timeout) + + this.once('exit', () => { + clearTimeout(this.#timeout) + }) + } + + this.#worker.on('message', data => { + if (data.method === 'kill' && data.args[0] === true) { + this.#state.killed = true + } + + if (data.method === 'state') { + if (this.#state.pid !== data.args[0].pid) { + this.#resource.runInAsyncScope(() => { + this.emit('spawn') + }) + } + + Object.assign(this.#state, data.args[0]) + + switch (this.#state.lifecycle) { + case 'spawn': { + gc.ref(this) + dc.channel('spawn').publish({ child_process: this }) + break + } + + case 'exit': { + this.#resource.runInAsyncScope(() => { + this.emit('exit', this.#state.exitCode) + }) + dc.channel('exit').publish({ child_process: this }) + break + } + + case 'close': { + this.#resource.runInAsyncScope(() => { + this.emit('close', this.#state.exitCode) + }) + + dc.channel('close').publish({ child_process: this }) + break + } + + case 'kill': { + this.#state.killed = true + dc.channel('kill').publish({ child_process: this }) + break + } + } + } + + if (data.method === 'exit') { + this.#resource.runInAsyncScope(() => { + this.emit('exit', data.args[0]) + }) + } + }) + + this.#worker.on('error', err => { + this.#resource.runInAsyncScope(() => { + this.emit('error', err) + }) + }) + } + + /** + * @ignore + * @type {Pipe} + */ + get pipe () { + return this.#pipe + } + + /** + * `true` if the child process was killed with kill()`, + * otherwise `false`. + * @type {boolean} + */ + get killed () { + return this.#state.killed + } + + /** + * The process identifier for the child process. This value is + * `> 0` if the process was spawned successfully, otherwise `0`. + * @type {number} + */ + get pid () { + return this.#state.pid + } + + /** + * The executable file name of the child process that is launched. This + * value is `null` until the child process has successfully been spawned. + * @type {string?} + */ + get spawnfile () { + return this.#state.spawnfile ?? null + } + + /** + * The full list of command-line arguments the child process was spawned with. + * This value is an empty array until the child process has successfully been + * spawned. + * @type {string[]} + */ + get spawnargs () { + return this.#state.spawnargs + } + + /** + * Always `false` as the IPC messaging is not supported. + * @type {boolean} + */ + get connected () { + return false + } + + /** + * The child process exit code. This value is `null` if the child process + * is still running, otherwise it is a positive integer. + * @type {number?} + */ + get exitCode () { + return this.#state.exitCode ?? null + } + + /** + * If available, the underlying `stdin` writable stream for + * the child process. + * @type {import('./stream').Writable?} + */ + get stdin () { + return this.#worker.stdin ?? null + } + + /** + * If available, the underlying `stdout` readable stream for + * the child process. + * @type {import('./stream').Readable?} + */ + get stdout () { + return this.#worker.stdout ?? null + } + + /** + * If available, the underlying `stderr` readable stream for + * the child process. + * @type {import('./stream').Readable?} + */ + get stderr () { + return this.#worker.stderr ?? null + } + + /** + * The underlying worker thread. + * @ignore + * @type {import('./worker_threads').Worker} + */ + get worker () { + return this.#worker + } + + /** + * This function does nothing, but is present for nodejs compat. + */ + disconnect () { + return false + } + + /** + * This function does nothing, but is present for nodejs compat. + * @return {boolean} + */ + send () { + return false + } + + /** + * This function does nothing, but is present for nodejs compat. + */ + ref () { + return false + } + + /** + * This function does nothing, but is present for nodejs compat. + */ + unref () { + return false + } + + /** + * Kills the child process. This function throws an error if the child + * process has not been spawned or is already killed. + * @param {number|string} signal + */ + kill (...args) { + if (!/spawn/.test(this.#state.lifecycle)) { + throw new Error('Cannot kill a child process that has not been spawned') + } + + if (this.killed) { + throw new Error('Cannot kill an already killed child process') + } + + this.#worker.postMessage({ id: this.#id, method: 'kill', args }) + return this + } + + /** + * Spawns the child process. This function will thrown an error if the process + * is already spawned. + * @param {string} command + * @param {string[]=} [args] + * @return {ChildProcess} + */ + spawn (...args) { + if (/spawning|spawn/.test(this.#state.lifecycle)) { + throw new Error('Cannot spawn an already spawned ChildProcess') + } + + if (!args[0] || typeof args[0] !== 'string') { + throw new TypeError('Expecting command to be a string.') + } + + this.#state.lifecycle = 'spawning' + this.#worker.postMessage({ + id: this.#id, + env: this.#env, + method: 'spawn', + args + }) + + return this + } + + /** + * `EventTarget` based `addEventListener` method. + * @param {string} event + * @param {function(Event)} callback + * @param {{ once?: false }} [options] + */ + addEventListener (event, callback, options = null) { + callback.listener = (...args) => { + if (event === 'error') { + callback(new ErrorEvent('error', { + // @ts-ignore + target: this, + error: args[0] + })) + } else { + callback(new Event(event, args[0])) + } + } + + if (options?.once === true) { + this.once(event, callback.listener) + } else { + this.on(event, callback.listener) + } + } + + /** + * `EventTarget` based `removeEventListener` method. + * @param {string} event + * @param {function(Event)} callback + * @param {{ once?: false }} [options] + */ + removeEventListener (event, callback) { + this.off(event, callback.listener ?? callback) + } + + /** + * Implements `gc.finalizer` for gc'd resource cleanup. + * @return {gc.Finalizer} + * @ignore + */ + [gc.finalizer] () { + return { + args: [this.#id], + async handle (id) { + const result = await ipc.send('child_process.kill', { + id, + signal: 'SIGTERM' + }) + + if (result.err) { + console.warn(result.err) + } + } + } + } +} + +/** + * Spawns a child process exeucting `command` with `args` + * @param {string} command + * @param {string[]|object=} [args] + * @param {object=} [options + * @return {ChildProcess} + */ +export function spawn (command, args = [], options = null) { + if (args && typeof args === 'object' && !Array.isArray(args)) { + options = args + args = [] + } + + if (!command || typeof command !== 'string') { + throw new TypeError('Expecting command to be a string.') + } + + if (args && typeof args === 'string') { + // @ts-ignore + args = args.split(' ') + } + + const child = new ChildProcess(options) + child.worker.on('online', () => child.spawn(command, args, options)) + return child +} + +export function exec (command, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + + const child = spawn(command, options) + const stdout = [] + const stderr = [] + let closed = false + let hasError = false + + if (child.stdout) { + child.stdout.on('data', (data) => { + if (hasError || closed) { + return + } + + stdout.push(Buffer.from(data)) + stdout.push(Buffer.from('\n')) + }) + } + + if (child.stderr) { + child.stderr.on('data', (data) => { + if (hasError || closed) { + return + } + + stderr.push(Buffer.from(data)) + stderr.push(Buffer.from('\n')) + }) + } + + child.once('error', (err) => { + hasError = true + stdout.splice(0, stdout.length) + stderr.splice(0, stderr.length) + if (typeof callback === 'function') { + callback(err, null, null) + } + }) + + if (typeof callback === 'function') { + child.once('close', () => { + closed = true + + if (hasError) { + return + } + + if (options?.encoding === 'buffer') { + callback( + null, + Buffer.concat(stdout), + Buffer.concat(stderr) + ) + } else { + const encoding = options?.encoding ?? 'utf8' + callback( + null, + // @ts-ignore + Buffer.concat(stdout).toString(encoding), + // @ts-ignore + Buffer.concat(stderr).toString(encoding) + ) + } + + stdout.splice(0, stdout.length) + stderr.splice(0, stderr.length) + }) + } + + return Object.assign(child, { + then (resolve, reject) { + const promise = new Promise((resolve, reject) => { + child.once('error', (err) => { + hasError = true + stdout.splice(0, stdout.length) + stderr.splice(0, stderr.length) + reject(err) + }) + + child.once('close', () => { + closed = true + + if (options?.encoding === 'buffer') { + resolve({ + stdout: Buffer.concat(stdout), + stderr: Buffer.concat(stderr) + }) + } else { + const encoding = options?.encoding ?? 'utf8' + resolve({ + // @ts-ignore + stdout: Buffer.concat(stdout).toString(encoding), + // @ts-ignore + stderr: Buffer.concat(stderr).toString(encoding) + }) + } + + stdout.splice(0, stdout.length) + stderr.splice(0, stderr.length) + }) + }) + + if (resolve && reject) { + return promise.then(resolve, reject) + } else if (resolve) { + return promise.then(resolve) + } + + return promise + }, + + catch (reject) { + return this.then().catch(reject) + }, + + finally (next) { + return this.then().finally(next) + } + }) +} + +export function execSync (command, options) { + const result = ipc.sendSync('child_process.exec', { + id: rand64(), + args: command, + cwd: options?.cwd ?? '', + stdin: options?.stdin !== false, + stdout: options?.stdout !== false, + stderr: options?.stderr !== false, + timeout: Number.isFinite(options?.timeout) ? options.timeout : 0, + killSignal: options?.killSignal ?? signal.SIGTERM + }) + + if (result.err) { + // @ts-ignore + if (!result.err.code) { + throw result.err + } + + // @ts-ignore + const { stdout, stderr, signal: errorSignal, code, pid } = result.err + const message = code === 'ETIMEDOUT' + ? 'execSync ETIMEDOUT' + : stderr.join('\n') + + const error = Object.assign(new Error(message), { + pid, + stdout, + stderr, + code: typeof code === 'string' ? code : null, + signal: errorSignal || signal.toString(options?.killSignal) || null, + status: Number.isFinite(code) ? code : null, + output: [null, stdout.join('\n'), stderr.join('\n')] + }) + + // @ts-ignore + error.error = error + + if (typeof code === 'string') { + // @ts-ignore + error.errno = -os.constants.errno[code] + } + + throw error + } + + const { stdout, stderr, signal: errorSignal, code, pid } = result.data + + if (code) { + const message = code === 'ETIMEDOUT' + ? 'execSync ETIMEDOUT' + : stderr.join('\n') + + const error = Object.assign(new Error(message), { + pid, + stdout, + stderr, + code: typeof code === 'string' ? code : null, + signal: errorSignal || null, + status: Number.isFinite(code) ? code : null, + output: [null, stdout, stderr] + }) + + // @ts-ignore + error.error = error + + if (typeof code === 'string') { + // @ts-ignore + error.errno = -os.constants.errno[code] + } + + throw error + } + + const output = stdout && options?.encoding === 'utf8' + ? stdout + : Buffer.from(stdout) + + return output +} + +export const execFile = exec + +exec[Symbol.for('nodejs.util.promisify.custom')] = +exec[Symbol.for('socket.runtime.util.promisify.custom')] = + async function execPromisify (command, options) { + return await new Promise((resolve, reject) => { + exec(command, options, (err, stdout, stderr) => { + if (err) { + reject(err) + } else { + resolve({ stdout, stderr }) + } + }) + }) + } + +export default { + ChildProcess, + spawn, + execFile, + exec +} diff --git a/api/child_process/worker.js b/api/child_process/worker.js new file mode 100644 index 0000000000..9fcea0d413 --- /dev/null +++ b/api/child_process/worker.js @@ -0,0 +1,108 @@ +import { parentPort } from '../worker_threads.js' +import process from '../process.js' +import signal from '../process/signal.js' +import ipc from '../ipc.js' + +const state = {} + +const propagateWorkerError = err => parentPort.postMessage({ + worker_threads: { + error: { + name: err.name, + message: err.message, + stack: err.stack, + type: err.name + } + } +}) + +if (process.stdin) { + process.stdin.on('data', async (data) => { + const { id } = state + const result = await ipc.write('child_process.spawn', { id }, data) + + if (result.err) { + propagateWorkerError(result.err) + } + }) +} + +parentPort.onmessage = async ({ data: { id, method, args } }) => { + if (method === 'spawn') { + const command = args[0] + const argv = args[1] + const opts = args[2] + + const params = { + args: [command, ...Array.from(argv ?? [])].join('\u0001'), + id, + cwd: opts?.cwd ?? '', + stdin: opts?.stdin !== false, + stdout: opts?.stdout !== false, + stderr: opts?.stderr !== false + } + + const result = await ipc.send('child_process.spawn', params) + + if (result.err) { + return propagateWorkerError(result.err) + } + + state.id = BigInt(result.data.id) + state.pid = result.data.pid + state.spawnfile = command + state.spawnargs = argv + state.lifecycle = 'spawn' + + parentPort.postMessage({ method: 'state', args: [state] }) + + globalThis.addEventListener('data', ({ detail }) => { + const { err, data, source } = detail.params + const buffer = detail.data + + if (err && err.id === state.id) { + return propagateWorkerError(err) + } + + if (!data || BigInt(data.id) !== state.id) return + + if (source === 'child_process.spawn' && data.source === 'stdout') { + if (process.stdout) { + process.stdout.write(buffer) + } + } + + if (source === 'child_process.spawn' && data.source === 'stderr') { + if (process.stderr) { + process.stderr.write(buffer) + } + } + + if (source === 'child_process.spawn' && data.status === 'close') { + state.exitCode = data.code + state.lifecycle = 'close' + parentPort.postMessage({ method: 'state', args: [state] }) + } + + if (source === 'child_process.spawn' && data.status === 'exit') { + state.exitCode = data.code + state.lifecycle = 'exit' + parentPort.postMessage({ method: 'state', args: [state] }) + } + }) + } + + if (method === 'kill') { + const result = await ipc.send('child_process.kill', { + id: state.id, + signal: signal.getCode(args[0]) + }) + + if (result.err) { + return propagateWorkerError(result.err) + } + + state.lifecycle = 'kill' + parentPort.postMessage({ method: 'state', args: [state] }) + } +} diff --git a/api/commonjs.js b/api/commonjs.js new file mode 100644 index 0000000000..fb3da723db --- /dev/null +++ b/api/commonjs.js @@ -0,0 +1,23 @@ +/** + * @module commonjs + */ +import builtins, { defineBuiltin } from './commonjs/builtins.js' +import createRequire from './commonjs/require.js' +import Package from './commonjs/package.js' +import Module from './commonjs/module.js' +import Loader from './commonjs/loader.js' +import Cache from './commonjs/cache.js' + +import * as exports from './commonjs.js' + +export default exports +export { + builtins, + Cache, + createRequire, + Loader, + Module, + Package +} + +defineBuiltin('commonjs', exports) diff --git a/api/commonjs/builtins.js b/api/commonjs/builtins.js new file mode 100644 index 0000000000..a3351c8b39 --- /dev/null +++ b/api/commonjs/builtins.js @@ -0,0 +1,247 @@ +import { ModuleNotFoundError } from '../errors.js' + +// eslint-disable-next-line +import _async, { + AsyncLocalStorage, + AsyncResource, + executionAsyncResource, + executionAsyncId, + triggerAsyncId, + createHook, + AsyncHook +} from '../async.js' + +// eslint-disable-next-line +import * as ai from '../ai.js' +import * as application from '../application.js' +import assert from '../assert.js' +import * as buffer from '../buffer.js' +// eslint-disable-next-line +import * as child_process from '../child_process.js' +import console from '../console.js' +import * as constants from '../constants.js' +import * as crypto from '../crypto.js' +import * as dgram from '../dgram.js' +import * as diagnostics from '../diagnostics.js' +import * as dns from '../dns.js' +import events from '../events.js' +import errno from '../errno.js' +import * as extension from '../extension.js' +import * as fs from '../fs.js' +import * as gc from '../gc.js' +import * as http from '../http.js' +import * as https from '../https.js' +import * as ipc from '../ipc.js' +import * as language from '../language.js' +import * as location from '../location.js' +import * as mime from '../mime.js' +import * as network from '../network.js' +import * as os from '../os.js' +import { posix as path } from '../path.js' +import process from '../process.js' +import * as querystring from '../querystring.js' +import * as serviceWorker from '../service-worker.js' +import stream from '../stream.js' +// eslint-disable-next-line +import * as string_decoder from '../string_decoder.js' +import test from '../test.js' +import * as timers from '../timers.js' +import * as tty from '../tty.js' +import * as url from '../url.js' +import util from '../util.js' +import vm from '../vm.js' +import * as window from '../window.js' +// eslint-disable-next-line +import * as worker_threads from '../worker_threads.js' + +/** + * A mapping of builtin modules + * @type {object} + */ +export const builtins = {} + +/** + * Defines a builtin module by name making a shallow copy of the + * module exports. + * @param {string} + * @param {object} exports + */ +export function defineBuiltin (name, exports, copy = true) { + if (exports && typeof exports === 'object') { + if (copy) { + builtins[name] = { ...exports } + delete builtins[name].default + } else { + builtins[name] = exports + } + } else if (typeof exports === 'string' && exports in builtins) { + // alias + Object.defineProperty(builtins, name, { + configurable: true, + enumerable: true, + get: () => builtins[exports] + }) + } else { + builtins[name] = exports + } +} + +// node.js compat modules +defineBuiltin('async_context', { + AsyncLocalStorage, + AsyncResource +}) +defineBuiltin('async_hooks', { + AsyncLocalStorage, + AsyncResource, + executionAsyncResource, + executionAsyncId, + triggerAsyncId, + createHook, + AsyncHook +}) +defineBuiltin('ai', ai) +defineBuiltin('assert', assert, false) +defineBuiltin('buffer', buffer, false) +defineBuiltin('console', console, false) +defineBuiltin('constants', constants) +// eslint-disable-next-line +defineBuiltin('child_process', child_process) +defineBuiltin('crypto', crypto) +defineBuiltin('dgram', dgram) +defineBuiltin('diagnostics_channel', diagnostics) +defineBuiltin('dns', dns) +defineBuiltin('dns/promises', dns.promises) +defineBuiltin('events', events, false) +defineBuiltin('fs', fs) +defineBuiltin('fs/promises', fs.promises) +defineBuiltin('http', http) +defineBuiltin('https', https) +defineBuiltin('net', {}) +defineBuiltin('os', os) +defineBuiltin('path', path) +defineBuiltin('perf_hooks', { performance: globalThis.performance }) +defineBuiltin('process', process, false) +defineBuiltin('querystring', querystring) +defineBuiltin('stream', stream, false) +defineBuiltin('stream/web', stream.web) +// eslint-disable-next-line +defineBuiltin('string_decoder', string_decoder) +defineBuiltin('sys', util) +defineBuiltin('test', test, false) +defineBuiltin('timers', timers) +defineBuiltin('timers/promises', timers.promises) +defineBuiltin('tty', tty) +defineBuiltin('util', util) +defineBuiltin('util/types', util.types) +defineBuiltin('url', url) +defineBuiltin('vm', vm) +// eslint-disable-next-line +defineBuiltin('worker_threads', worker_threads) +// unsupported, but stubbed as entries +defineBuiltin('v8', {}) +defineBuiltin('zlib', {}) + +// runtime modules +// eslint-disable-next-line +defineBuiltin('async', _async) +defineBuiltin('application', application) +defineBuiltin('commonjs/builtins', builtins, false) +defineBuiltin('errno', errno) +defineBuiltin('extension', extension) +defineBuiltin('gc', gc) +defineBuiltin('ipc', ipc) +defineBuiltin('language', language) +defineBuiltin('location', location) +defineBuiltin('mime', mime) +defineBuiltin('network', network) +defineBuiltin('service-worker', serviceWorker) +defineBuiltin('window', window) + +/** + * Known runtime specific builtin modules. + * @type {Set} + */ +export const runtimeModules = new Set([ + 'async', + 'application', + 'commonjs', + 'commonjs/builtins', + 'commonjs/cache', + 'commonjs/loader', + 'commonjs/module', + 'commonjs/package', + 'commonjs/require', + 'extension', + 'errno', + 'gc', + 'ipc', + 'language', + 'location', + 'mime', + 'network', + 'service-worker', + 'window' +]) + +/** + * Predicate to determine if a given module name is a builtin module. + * @param {string} name + * @param {{ builtins?: object }} + * @return {boolean} + */ +export function isBuiltin (name, options = null) { + const originalName = name + name = name.replace(/^(socket|node):/, '') + + if ( + runtimeModules.has(name) && + !originalName.startsWith('socket:') + ) { + return false + } + + if (options?.builtins && typeof options.builtins === 'object') { + return name in builtins + } else if (name in builtins) { + return true + } + + return false +} + +/** + * Gets a builtin module by name. + * @param {string} name + * @param {{ builtins?: object }} [options] + * @return {any} + */ +export function getBuiltin (name, options = null) { + const originalName = name + name = name.replace(/^(socket|node):/, '') + + if ( + runtimeModules.has(name) && + !originalName.startsWith('socket:') + ) { + throw new ModuleNotFoundError( + `Cannot find builtin module '${originalName}` + ) + } + + if (options?.builtins && typeof options.builtins === 'object') { + if (name in options.builtins) { + return options.builtins[name] + } + } + + if (name in builtins) { + return builtins[name] + } + + throw new ModuleNotFoundError( + `Cannot find builtin module '${originalName}` + ) +} + +export default builtins diff --git a/api/commonjs/cache.js b/api/commonjs/cache.js new file mode 100644 index 0000000000..c96acecb44 --- /dev/null +++ b/api/commonjs/cache.js @@ -0,0 +1,871 @@ +/* global ErrorEvent */ +import { defineBuiltin } from './builtins.js' +import { Deferred } from '../async/deferred.js' +import serialize from '../internal/serialize.js' +import database from '../internal/database.js' +import gc from '../gc.js' + +/** + * @typedef {{ + * types?: object, + * loader?: import('./loader.js').Loader + * }} CacheOptions + */ + +export const CACHE_CHANNEL_MESSAGE_ID = 'id' +export const CACHE_CHANNEL_MESSAGE_REPLICATE = 'replicate' + +/** + * @typedef {{ + * name: string + * }} StorageOptions + */ + +/** + * An storage context object with persistence and durability + * for service worker storages. + */ +export class Storage extends EventTarget { + /** + * Maximum entries that will be restored from storage into the context object. + * @type {number} + */ + static MAX_CONTEXT_ENTRIES = 16 * 1024 + + /** + * A mapping of known `Storage` instances. + * @type {Map} + */ + static instances = new Map() + + /** + * Opens an storage for a particular name. + * @param {StorageOptions} options + * @return {Promise} + */ + static async open (options) { + if (Storage.instances.has(options?.name)) { + const storage = Storage.instances.get(options.name) + await storage.ready + return storage + } + + const storage = new this(options) + Storage.instances.set(storage.name, storage) + await storage.open() + return storage + } + + #database = null + #opening = false + #context = {} + #proxy = null + #ready = null + #name = null + + /** + * `Storage` class constructor + * @ignore + * @param {StorageOptions} options + */ + constructor (options) { + super() + + this.#name = options.name + this.#proxy = new Proxy(this.#context, { + get: (_, property) => { + return this.#context[property] + }, + + set: (_, property, value) => { + this.#context[property] = value + if (this.database && this.database.opened) { + this.forwardRequest(this.database.put(property, value, { durability: 'relaxed' })) + } + return true + }, + + deleteProperty: (_, property) => { + if (this.database && this.database.opened) { + this.forwardRequest(this.database.delete(property)) + } + + return Reflect.deleteProperty(this.#context, property) + }, + + getOwnPropertyDescriptor: (_, property) => { + if (property in this.#context) { + return { + configurable: true, + enumerable: true, + writable: true, + value: this.#context[property] + } + } + }, + + has: (_, property) => { + return Reflect.has(this.#context, property) + }, + + ownKeys: (_) => { + return Reflect.ownKeys(this.#context) + } + }) + } + + /** + * A reference to the currently opened storage database. + * @type {import('../internal/database.js').Database} + */ + get database () { + return this.#database + } + + /** + * `true` if the storage is opened, otherwise `false`. + * @type {boolean} + */ + get opened () { + return this.#database?.opened === true + } + + /** + * `true` if the storage is opening, otherwise `false`. + * @type {boolean} + */ + get opening () { + return this.#opening + } + + /** + * A proxied object for reading and writing storage state. + * Values written to this object must be cloneable with respect to the + * structured clone algorithm. + * @see {https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm} + * @type {Proxy} + */ + get context () { + return this.#proxy + } + + /** + * The current storage name. This value is also used as the + * internal database name. + * @type {string} + */ + get name () { + return `socket.runtime.commonjs.cache.storage(${this.#name})` + } + + /** + * A promise that resolves when the storage is opened. + * @type {Promise?} + */ + get ready () { + return this.#ready?.promise + } + + /** + * @ignore + * @param {Promise} promise + */ + async forwardRequest (promise) { + try { + return await promise + } catch (error) { + this.dispatchEvent(new ErrorEvent('error', { error })) + } + } + + /** + * Resets the current storage to an empty state. + */ + async reset () { + await this.close() + await database.drop(this.name) + this.#database = null + await this.open() + } + + /** + * Synchronizes database entries into the storage context. + */ + async sync (options = null) { + const entries = await this.#database.get(options?.query ?? undefined, { + count: Storage.MAX_CONTEXT_ENTRIES + }) + + const promises = [] + const snapshot = Object.keys(this.#context) + const delta = [] + + for (const [key, value] of entries) { + if (!snapshot.includes(key)) { + delta.push(key) + } + + this.#context[key] = value + } + + for (const key of delta) { + const value = this.#context[key] + promises.push(this.forwardRequest(this.database.put(key, value, { durability: 'relaxed' }))) + } + + await Promise.all(promises) + } + + /** + * Opens the storage. + * @ignore + */ + async open (options = null) { + if (!this.opening && !this.#database) { + this.#opening = true + this.#ready = new Deferred() + this.#database = await database.open(this.name) + await this.sync(options?.sync ?? null) + this.#opening = false + this.#ready.resolve() + } + + return this.#ready.promise + } + + /** + * Closes the storage database, purging existing state. + * @ignore + */ + async close () { + await this.#database.close() + for (const key in this.#context) { + Reflect.deleteProperty(this.#context, key) + } + } +} + +/** + * A container for `Snapshot` data storage. + */ +export class SnapshotData { + /** + * `SnapshotData` class constructor. + * @param {object=} [data] + */ + constructor (data = null) { + // make the `prototype` a `null` value + Object.setPrototypeOf(this, null) + + if (data && typeof data === 'object') { + Object.assign(this, data) + } + + this[Symbol.toStringTag] = 'Snapshot.Data' + + this.toJSON = () => { + return { ...this } + } + } +} + +/** + * A container for storing a snapshot of the cache data. + */ +export class Snapshot { + /** + * @type {typeof SnapshotData} + */ + static Data = SnapshotData + + // @ts-ignore + #data = new Snapshot.Data() + + /** + * `Snapshot` class constructor. + */ + constructor () { + for (const key in Cache.shared) { + // @ts-ignore + this.#data[key] = new Snapshot.Data(Object.fromEntries(Cache.shared[key].entries())) + } + } + + /** + * A reference to the snapshot data. + * @type {Snapshot.Data} + */ + get data () { + return this.#data + } + + /** + * @ignore + */ + [Symbol.for('socket.runtime.serialize')] () { + return { ...serialize(this.#data) } + } + + /** + * @ignore + * @return {object} + */ + toJSON () { + return { ...serialize(this.#data) } + } +} + +/** + * An interface for managing and performing operations on a collection + * of `Cache` objects. + */ +export class CacheCollection { + /** + * `CacheCollection` class constructor. + * @ignore + * @param {Cache[]|Record=} [collection] + */ + constructor (collection = null) { + if (collection && typeof collection === 'object') { + if (Array.isArray(collection)) { + for (const value of collection) { + if (value instanceof Cache) { + this.add(value) + } + } + } else { + for (const key in collection) { + const value = collection[key] + this.add(key, value) + } + } + } + } + + /** + * Adds a `Cache` instance to the collection. + * @param {string|Cache} name + * @param {Cache=} [cache] + * @param {boolean} + */ + add (name, cache = null) { + if (name instanceof Cache) { + return this.add(name.name, name) + } + + if (typeof name === 'string' && cache instanceof Cache) { + if (name in Object.getPrototypeOf(this)) { + return false + } + + Object.defineProperty(this, name, { + configurable: false, + enumerable: true, + writable: false, + value: cache + }) + + return true + } + + return false + } + + /** + * Calls a method on each `Cache` object in the collection. + * @param {string} method + * @param {...any} args + * @return {Promise>} + */ + async call (method, ...args) { + const results = {} + + for (const key in this) { + const value = this[key] + if (value instanceof Cache) { + if (typeof value[method] === 'function') { + results[key] = await value[method](...args) + } + } + } + + return results + } + + async restore () { + return await this.call('restore') + } + + async reset () { + return await this.call('reset') + } + + async snapshot () { + return await this.call('snapshot') + } + + async get (key) { + return await this.call('get', key) + } + + async delete (key) { + return await this.call('delete', key) + } + + async keys (key) { + return await this.call('keys', key) + } + + async values (key) { + return await this.call('values', key) + } + + async clear (key) { + return await this.call('clear', key) + } +} + +/** + * A container for a shared cache that lives for the life time of + * application execution. Updates to this storage are replicated to other + * instances in the application context, including windows and workers. + */ +export class Cache { + /** + * A globally shared type mapping for the cache to use when + * derserializing a value. + * @type {Map} + */ + static types = new Map() + + /** + * A globally shared cache store keyed by cache name. This is useful so + * when multiple instances of a `Cache` are created, they can share the + * same data store, reducing duplications. + * @type {Record} + */ + static shared = Object.create(null) + + /** + * A mapping of opened `Storage` instances. + * @type {Map} + */ + static storages = Storage.instances + + /** + * The `Cache.Snapshot` class. + * @type {typeof Snapshot} + */ + static Snapshot = Snapshot + + /** + * The `Cache.Storage` class + * @type {typeof Storage} + */ + static Storage = Storage + + /** + * Creates a snapshot of the current cache which can be serialized and + * stored in persistent storage. + * @return {Snapshot} + */ + static snapshot () { + return new Snapshot() + } + + /** + * Restore caches from persistent storage. + * @param {string[]} names + * @return {Promise} + */ + static async restore (names) { + const promises = [] + + for (const name of names) { + promises.push(Storage.open({ name }).then((storage) => { + this.storages.set(name, storage) + + Cache.shared[name] ||= new Map() + for (const key in storage.context) { + const value = storage.context[key] + Cache.shared[name].set(key, value) + } + })) + } + + await Promise.all(promises) + } + + #onmessage = null + #storage = null + #channel = null + #loader = null + #name = '' + #types = null + #data = null + #id = null + + /** + * `Cache` class constructor. + * @param {string} name + * @param {CacheOptions=} [options] + */ + constructor (name, options) { + if (!name || typeof name !== 'string') { + throw new TypeError(`Expecting 'name' to be a string. Received: ${name}`) + } + + if (!Cache.shared[name]) { + Cache.shared[name] = new Map() + } + + this.#id = Math.random().toString(16).slice(2) + this.#name = name + this.#data = Cache.shared[name] + this.#types = new Map(Cache.types.entries()) + this.#loader = options?.loader ?? null + this.#storage = Cache.storages.get(name) ?? new Storage({ name }) + this.#channel = new BroadcastChannel(`socket.runtime.commonjs.cache.${name}`) + + if (!Cache.storages.has(name)) { + Cache.storages.set(name, this.#storage) + } + + if (options?.types && typeof options.types === 'object') { + for (const key in options.types) { + const value = options.types[key] + if (typeof value === 'function') { + this.#types.set(key, value) + } + } + } + + this.#onmessage = (event) => { + const { data } = event + if (!data || typeof data !== 'object') { + return + } + + if (data[CACHE_CHANNEL_MESSAGE_ID] === this.#id) { + return + } + + // recv 'replicate' + if (Array.isArray(data[CACHE_CHANNEL_MESSAGE_REPLICATE])) { + for (const entry of data[CACHE_CHANNEL_MESSAGE_REPLICATE]) { + if (!this.has(...entry)) { + const [key, value] = entry + if (value?.__type__) { + this.#data.set(key, this.#types.get(value.__type__).from(value, { + loader: this.#loader + })) + } else { + this.#data.set(key, value) + } + } + } + } + } + + this.#channel.addEventListener('message', this.#onmessage) + this.#storage.open() + + gc.ref(this) + } + + /** + * The unique ID for this cache. + * @type {string} + */ + get id () { + return this.#id + } + + /** + * The loader associated with this cache. + * @type {import('./loader.js').Loader} + */ + get loader () { + return this.#loader + } + + /** + * A reference to the persisted storage. + * @type {Storage} + */ + get storage () { + return this.#storage + } + + /** + * The cache name + * @type {string} + */ + get name () { + return this.#name + } + + /** + * The underlying cache data map. + * @type {Map} + */ + get data () { + return this.#data + } + + /** + * The broadcast channel associated with this cach. + * @type {BroadcastChannel} + */ + get channel () { + return this.#channel + } + + /** + * The size of the cache. + * @type {number} + */ + get size () { + return this.#data.size + } + + /** + * @type {Map} + */ + get types () { + return this.#types + } + + /** + * Resets the cache map and persisted storage. + */ + async reset () { + this.#data.clear() + await this.#storage.reset() + } + + /** + * Restores cache data from storage. + */ + async restore () { + if (!this.#storage.opened) { + await this.#storage.open() + } + + for (const key in this.#storage.context) { + const value = this.#storage.context[key] + + if (value && !this.#data.has(key)) { + if (value?.__type__) { + this.#data.set(key, this.#types.get(value.__type__).from(value, { + loader: this.loader + })) + } else { + this.#data.set(key, value) + } + } + } + } + + /** + * Creates a snapshot of the current cache which can be serialized and + * stored in persistent storage. + * @return {Snapshot.Data} + */ + snapshot () { + const snapshot = new Snapshot() + return snapshot.data[this.name] + } + + /** + * Get a value at `key`. + * @param {string} key + * @return {object|undefined} + */ + get (key) { + const types = Array.from(this.types.values()) + let value = null + + if (this.#data.has(key)) { + value = this.#data.get(key) + } else if (key in this.#storage.context) { + value = this.#storage.context[key] + } + + if (!value) { + return + } + + // late init from type + if (value?.__type__ && this.#types.has(value.__type__)) { + value = this.#types.get(value.__type__).from(value, { + loader: this.#loader + }) + } else if (types.length === 1) { + // if there is only 1 type in this cache types mapping, it most likely is the + // general type used for this cache, so try to use it + const [Type] = types + if (typeof Type === 'function' && !(value instanceof Type)) { + if (typeof Type.from === 'function') { + value = Type.from(value, { + loader: this.#loader + }) + } + } + } + + // reset the value + this.#data.set(key, value) + + return value + } + + /** + * Set `value` at `key`. + * @param {string} key + * @param {object} value + * @return {Cache} + */ + set (key, value) { + this.#data.set(key, value) + this.#storage.context[key] = serialize(value) + return this + } + + /** + * Returns `true` if `key` is in cache, otherwise `false`. + * @param {string} + * @return {boolean} + */ + has (key) { + return this.#data.has(key) + } + + /** + * Delete a value at `key`. + * This does not replicate to shared caches. + * @param {string} key + * @return {boolean} + */ + delete (key) { + delete this.#storage.context[key] + if (this.#data.delete(key)) { + return true + } + + return false + } + + /** + * Returns an iterator for all cache keys. + * @return {object} + */ + keys () { + return this.#data.keys() + } + + /** + * Returns an iterator for all cache values. + * @return {object} + */ + values () { + return this.#data.values() + } + + /** + * Returns an iterator for all cache entries. + * @return {object} + */ + entries () { + return this.#data.entries() + } + + /** + * Clears all entries in the cache. + * This does not replicate to shared caches. + * @return {undefined} + */ + clear () { + this.#data.clear() + } + + /** + * Enumerates entries in map calling `callback(value, key + * @param {function(object, string, Cache): any} callback + */ + forEach (callback) { + if (!callback || typeof callback !== 'function') { + throw new TypeError(`${callback} is not a function`) + } + + this.#data.forEach((value, key) => { + callback(value, key, this) + }) + } + + /** + * Broadcasts a replication to other shared caches. + */ + replicate () { + const entries = Array.from(this.#data.entries()) + + if (entries.length === 0) { + return this + } + + const message = { + [CACHE_CHANNEL_MESSAGE_ID]: this.#id, + [CACHE_CHANNEL_MESSAGE_REPLICATE]: entries + } + + this.#channel.postMessage(message) + + return this + } + + /** + * Destroys the cache. This function stops the broadcast channel and removes + * and listeners + */ + destroy () { + this.#channel.removeEventListener('message', this.#onmessage) + this.#data.clear() + this.#data = null + this.#channel = null + gc.unref(this) + } + + /** + * @ignore + */ + [Symbol.iterator] () { + return this.#data.entries() + } + + /** + * Implements `gc.finalizer` for gc'd resource cleanup. + * @return {gc.Finalizer} + * @ignore + */ + [gc.finalizer] () { + return { + args: [this.#data, this.#channel, this.#onmessage], + async handle (data, channel, onmessage) { + data.clear() + channel.removeEventListener('message', onmessage) + } + } + } +} + +export default Cache + +defineBuiltin('commonjs/cache', { + CACHE_CHANNEL_MESSAGE_ID, + CACHE_CHANNEL_MESSAGE_REPLICATE, + CacheCollection, + SnapshotData, + Snapshot, + Storage, + Cache +}) diff --git a/api/commonjs/loader.js b/api/commonjs/loader.js new file mode 100644 index 0000000000..05404da8ee --- /dev/null +++ b/api/commonjs/loader.js @@ -0,0 +1,955 @@ +/* global XMLHttpRequest */ +/** + * @module commonjs.loader + */ +import { CacheCollection, Cache } from './cache.js' +import { defineBuiltin } from './builtins.js' +import InternalSymbols from '../internal/symbols.js' +import application from '../application.js' +import { Headers } from '../ipc.js' +import location from '../location.js' +import path from '../path.js' +import URL from '../url.js' +import os from '../os.js' +import fs from '../fs.js' + +const RUNTIME_SERVICE_WORKER_FETCH_MODE = 'Runtime-ServiceWorker-Fetch-Mode' +const RUNTIME_REQUEST_SOURCE_HEADER = 'Runtime-Request-Source' +const textDecoder = new TextDecoder() + +/** + * @typedef {{ + * extensions?: string[] | Set + * origin?: URL | string, + * statuses?: Cache + * cache?: { response?: Cache, status?: Cache }, + * headers?: Headers | Map | object | string[][] + * }} LoaderOptions + */ + +/** + * @typedef {{ + * loader?: Loader, + * origin?: URL | string + * }} RequestOptions + */ + +/** + * @typedef {{ + * headers?: Headers | object | array[], + * status?: number + * }} RequestStatusOptions + */ + +/** + * @typedef {{ + * headers?: Headers | object + * }} RequestLoadOptions + */ + +/** + * @typedef {{ + * request?: Request, + * headers?: Headers, + * status?: number, + * buffer?: ArrayBuffer, + * text?: string + * }} ResponseOptions + */ + +/** + * A container for the status of a CommonJS resource. A `RequestStatus` object + * represents meta data for a `Request` that comes from a preflight + * HTTP HEAD request. + */ +export class RequestStatus { + /** + * Creates a `RequestStatus` from JSON input. + * @param {object} json + * @return {RequestStatus} + */ + static from (json, options) { + const status = new this(null, json) + status.request = Request.from({ url: json.id }, { ...options, status }) + return status + } + + #status = undefined + #request = null + #headers = new Headers() + + /** + * `RequestStatus` class constructor. + * @param {Request} request + * @param {RequestStatusOptions} [options] + */ + constructor (request, options = null) { + if (!options && request && !(request instanceof Request)) { + options = request + request = options.requesst + } + + if (request && !(request instanceof Request)) { + throw new TypeError( + `Expecting 'request' to be a Request object. Received: ${request}` + ) + } + + this.#headers = options?.headers ? Headers.from(options.headers) : this.#headers + this.#status = options?.status ? options.status : undefined + + if (request) { + this.request = request + } + } + + /** + * The `Request` object associated with this `RequestStatus` object. + * @type {Request} + */ + get request () { return this.#request } + set request (request) { + this.#request = request + + if ( + !this.#status && + request?.loader?.cache?.status?.has?.(request?.id) + ) { + this.#status = ( + request.status?.value ?? + request.loader.cache.status.get(request.id)?.value ?? + null + ) + } + } + + /** + * The unique ID of this `RequestStatus`, which is the absolute URL as a string. + * @type {string} + */ + get id () { + return this.#request?.id ?? null + } + + /** + * The origin for this `RequestStatus` object. + * @type {string} + */ + get origin () { + return this.#request?.origin ?? null + } + + /** + * A HTTP status code for this `RequestStatus` object. + * @type {number|undefined} + */ + get status () { + return this.#status + } + + /** + * An alias for `status`. + * @type {number|undefined} + */ + get value () { + return this.#status + } + + /** + * @ignore + */ + get valueOf () { + return this.status + } + + /** + * The HTTP headers for this `RequestStatus` object. + * @type {Headers} + */ + get headers () { + return this.#headers + } + + /** + * The resource location for this `RequestStatus` object. This value is + * determined from the 'Content-Location' header, if available, otherwise + * it is derived from the request URL pathname (including the query string). + * @type {string} + */ + get location () { + const contentLocation = this.#headers.get('content-location') + if (contentLocation) { + return contentLocation + } + + if (this.#request) { + return this.#request.url.pathname + this.#request.url.search + } + + return '' + } + + /** + * `true` if the response status is considered OK, otherwise `false`. + * @type {boolean} + */ + get ok () { + return this.#status >= 200 && this.#status < 400 + } + + /** + * Loads the internal state for this `RequestStatus` object. + * @param {RequestLoadOptions|boolean} [options] + * @return {RequestStatus} + */ + load (options = null) { + // allow `load(true)` to force a reload of the state + if (this.#status && options !== true) { + return this + } + + if ( + this.#request.id.includes(`://${application.config.meta_bundle_identifier}`) + ) { + try { + const id = this.#request.id.replace('https:', 'socket:') + fs.accessSync(id) + } catch { + this.#request.loader.cache.status.set(this.id, this) + this.#headers = new Headers() + this.#status = 404 + return this + } + } + + const request = new XMLHttpRequest() + request.open('HEAD', this.#request.id, false) + request.setRequestHeader(RUNTIME_REQUEST_SOURCE_HEADER, 'module') + + if (os.platform() !== 'android') { + request.withCredentials = true + } + + if (globalThis.isServiceWorkerScope) { + request.setRequestHeader(RUNTIME_SERVICE_WORKER_FETCH_MODE, 'ignore') + } + + if (this.#request?.loader) { + const entries = this.#request.loader.headers.entries() + for (const entry of entries) { + // @ts-ignore + request.setRequestHeader(...entry) + } + } + + if (options?.headers && typeof options?.headers === 'object') { + const entries = typeof options.headers.entries === 'function' + ? options.headers.entries() + : Object.entries(options.headers) + + for (const entry of entries) { + // @ts-ignore + request.setRequestHeader(...entry) + } + } + + request.send(null) + + this.#headers = Headers.from(request) + this.#status = request.status + + const contentLocation = this.#headers.get('content-location') + + if (this.#request) { + this.#request.loader.cache.status.set(this.id, this) + } + + // verify 'Content-Location' header if given in response + // @ts-ignore + if (this.#request && contentLocation && URL.canParse(contentLocation, this.origin)) { + const url = new URL(contentLocation, this.origin) + const extension = path.extname(url.pathname) + + if (!this.#request.loader.extensions.has(extension)) { + this.#status = 404 + return this + } + } + + return this + } + + /** + * Converts this `RequestStatus` to JSON. + * @ignore + * @return {{ + * id: string, + * origin: string | null, + * status: number, + * headers: Array + * request: object | null | undefined + * }} + */ + toJSON (includeRequest = true) { + if (includeRequest) { + return { + id: this.id, + origin: this.origin, + status: this.status, + headers: Array.from(this.headers.entries()), + request: this.#request ? this.#request.toJSON(false) : null + } + } else { + return { + id: this.id, + origin: this.origin, + status: this.status, + headers: Array.from(this.headers.entries()) + } + } + } + + /** + * Serializes this `Response`, suitable for `postMessage()` transfers. + * @ignore + * @return {{ + * __type__: 'RequestStatus', + * id: string, + * origin: string | null, + * status: number, + * headers: Array + * request: object | null + * }} + */ + [InternalSymbols.serialize] () { + return { __type__: 'RequestStatus', ...this.toJSON() } + } +} + +/** + * A container for a synchronous CommonJS request to local resource or + * over the network. + */ +export class Request { + /** + * Creates a `Request` instance from JSON input + * @param {object} json + * @param {RequestOptions=} [options] + * @return {Request} + */ + static from (json, options) { + return new this(json.url, { + status: ( + json.status && + typeof json.status === 'object' && + !(json.status instanceof RequestStatus) + // @ts-ignore + ? RequestStatus.from(json.status) + : options?.status + ), + ...options + }) + } + + #url = null + #loader = null + #status = null + + /** + * `Request` class constructor. + * @param {URL|string} url + * @param {URL|string=} [origin] + * @param {RequestOptions=} [options] + */ + constructor (url, origin, options = null) { + if (origin && typeof origin === 'object' && !(origin instanceof URL)) { + options = origin + origin = options.origin ?? null + } + + if (!origin) { + origin = location.origin + } + + if (String(origin).startsWith('blob:')) { + origin = new URL(origin).pathname + } + + this.#url = new URL(url, origin) + this.#loader = options?.loader ?? null + this.#status = options?.status instanceof RequestStatus + ? options.status + : new RequestStatus(options.status) + + this.#status.request = this + } + + /** + * The unique ID of this `Request`, which is the absolute URL as a string. + * @type {string} + */ + get id () { + return this.url.href + } + + /** + * The absolute `URL` of this `Request` object. + * @type {URL} + */ + get url () { + return this.#url + } + + /** + * The origin for this `Request`. + * @type {string} + */ + get origin () { + return this.url.origin + } + + /** + * The `Loader` for this `Request` object. + * @type {Loader?} + */ + get loader () { + return this.#loader + } + + /** + * The `RequestStatus` for this `Request` + * @type {RequestStatus} + */ + get status () { + return this.#status.load() + } + + /** + * Loads the CommonJS source file, optionally checking the `Loader` cache + * first, unless ignored when `options.cache` is `false`. + * @param {RequestLoadOptions=} [options] + * @return {Response} + */ + load (options = null) { + // check loader cache first + if (options?.cache !== false && this.#loader !== null) { + if (this.#loader.cache.response.has(this.id)) { + return this.#loader.cache.response.get(this.id) + } + } + + if (this.status.value >= 400) { + return new Response(this, { + status: this.status.value + }) + } + + if ( + /^(socket:|https:)/.test(this.id) && + this.id.includes(`//${application.config.meta_bundle_identifier}/`) + ) { + try { + const id = this.id.replace('https:', 'socket:') + fs.accessSync(id) + } catch { + return new Response(this, { + status: 404 + }) + } + } + + const request = new XMLHttpRequest() + request.open('GET', this.id, false) + request.setRequestHeader(RUNTIME_REQUEST_SOURCE_HEADER, 'module') + + if (os.platform() !== 'android') { + request.withCredentials = true + } + + if (globalThis.isServiceWorkerScope) { + request.setRequestHeader(RUNTIME_SERVICE_WORKER_FETCH_MODE, 'ignore') + } + + if (typeof options?.responseType === 'string') { + request.responseType = options.responseType + } + + if (this.#loader) { + const entries = this.#loader.headers.entries() + for (const entry of entries) { + // @ts-ignore + request.setRequestHeader(...entry) + } + } + + if (options?.headers && typeof options?.headers === 'object') { + const entries = typeof options.headers.entries === 'function' + ? options.headers.entries() + : Object.entries(options.headers) + + for (const entry of entries) { + // @ts-ignore + request.setRequestHeader(...entry) + } + } + + request.send(null) + + let responseText = null + + try { + // @ts-ignore + responseText = request.responseText // can throw `InvalidStateError` error + } catch { + if (typeof request.response === 'string') { + responseText = request.response + } + } + + return new Response(this, { + headers: Headers.from(request), + status: request.status, + buffer: request.response, + text: responseText ?? null + }) + } + + /** + * Converts this `Request` to JSON. + * @ignore + * @return {{ + * url: string, + * status: object | undefined + * }} + */ + toJSON (includeStatus = true) { + if (includeStatus) { + return { + url: this.url.href, + status: this.status.toJSON(false) + } + } else { + return { + url: this.url.href + } + } + } + + /** + * Serializes this `Response`, suitable for `postMessage()` transfers. + * @ignore + * @return {{ + * __type__: 'Request', + * url: string, + * status: object | undefined + * }} + */ + [InternalSymbols.serialize] () { + return { __type__: 'Request', ...this.toJSON() } + } +} + +/** + * A container for a synchronous CommonJS request response for a local resource + * or over the network. + */ +export class Response { + /** + * Creates a `Response` from JSON input + * @param {obejct} json + * @param {ResponseOptions=} [options] + * @return {Response} + */ + static from (json, options) { + return new this({ + ...json, + request: Request.from({ url: json.id }, options) + }, options) + } + + #request = null + #headers = null + #status = 404 + #buffer = null + #text = '' + + /** + * `Response` class constructor. + * @param {Request|ResponseOptions} request + * @param {ResponseOptions=} [options] + */ + constructor (request, options = null) { + options = { ...options } + + if (typeof request === 'object' && !(request instanceof Request)) { + options = request + request = options.request + } + + if (!request || !(request instanceof Request)) { + throw new TypeError( + `Expecting 'request' to be a Request object. Received: ${request}` + ) + } + + this.#request = request + this.#headers = Headers.from(options.headers) + this.#status = options.status || 404 + this.#buffer = options.buffer ? new Uint8Array(options.buffer).buffer : null + this.#text = options.text || '' + + if (request.loader) { + // cache request response in the loader + request.loader.cache.response.set(request.id, this) + } + } + + /** + * The unique ID of this `Response`, which is the absolute + * URL of the request as a string. + * @type {string} + */ + get id () { + return this.#request.id + } + + /** + * The `Request` object associated with this `Response` object. + * @type {Request} + */ + get request () { + return this.#request + } + + /** + * The response headers from the associated request. + * @type {Headers} + */ + get headers () { + return this.#headers + } + + /** + * The `Loader` associated with this `Response` object. + * @type {Loader?} + */ + get loader () { + return this.request.loader + } + + /** + * The `Response` status code from the associated `Request` object. + * @type {number} + */ + get status () { + return this.#status + } + + /** + * The `Response` string from the associated `Request` + * @type {string} + */ + get text () { + if (this.#text) { + return this.#text + } else if (this.#buffer) { + return textDecoder.decode(this.#buffer) + } + + return '' + } + + /** + * The `Response` array buffer from the associated `Request` + * @type {ArrayBuffer?} + */ + get buffer () { + return this.#buffer ?? null + } + + /** + * `true` if the response is considered OK, otherwise `false`. + * @type {boolean} + */ + get ok () { + return this.id && this.status >= 200 && this.status < 400 + } + + /** + * Converts this `Response` to JSON. + * @ignore + * @return {{ + * id: string, + * text: string, + * status: number, + * buffer: number[] | null, + * headers: Array + * }} + */ + toJSON () { + return { + id: this.id, + text: this.text, + status: this.status, + buffer: this.#buffer ? Array.from(new Uint8Array(this.#buffer)) : null, + headers: Array.from(this.#headers.entries()) + } + } + + /** + * Serializes this `Response`, suitable for `postMessage()` transfers. + * @ignore + * @return {{ + * __type__: 'Response', + * id: string, + * text: string, + * status: number, + * buffer: number[] | null, + * headers: Array + * }} + */ + [InternalSymbols.serialize] () { + return { __type__: 'Response', ...this.toJSON() } + } +} + +/** + * A container for loading CommonJS module sources + */ +export class Loader { + /** + * A request class used by `Loader` objects. + * @type {typeof Request} + */ + static Request = Request + + /** + * A response class used by `Loader` objects. + * @type {typeof Request} + */ + static Response = Response + + /** + * Resolves a given module URL to an absolute URL with an optional `origin`. + * @param {URL|string} url + * @param {URL|string} [origin] + * @return {string} + */ + static resolve (url, origin = null) { + if (!origin) { + origin = location.origin + } + + if (String(origin).startsWith('blob:')) { + origin = new URL(origin).pathname + } + + if (String(url).startsWith('blob:')) { + url = new URL(url).pathname + } + + return String(new URL(url, origin)) + } + + /** + * Default extensions for a loader. + * @type {Set} + */ + static defaultExtensions = new Set([ + '.js', + '.json', + '.mjs', + '.cjs', + '.jsx', + '.ts', + '.tsx', + '.wasm' + ]) + + #cache = new CacheCollection() + + #origin = null + #headers = new Headers() + #extensions = Loader.defaultExtensions + + /** + * `Loader` class constructor. + * @param {string|URL|LoaderOptions} origin + * @param {LoaderOptions=} [options] + */ + constructor (origin, options = null) { + if (origin && typeof origin === 'object' && !(origin instanceof URL)) { + options = origin + origin = options.origin + } + + this.#origin = Loader.resolve('.', origin) + + if (options?.headers && typeof options.headers === 'object') { + if (Array.isArray(options.headers)) { + for (const entry of options.headers) { + // @ts-ignore + this.#headers.set(...entry) + } + } else if (typeof options.headers.entries === 'function') { + for (const entry of options.headers.entries()) { + // @ts-ignore + this.#headers.set(...entry) + } + } else { + for (const key in options.headers) { + this.#headers.set(key, options.headers[key]) + } + } + } + + if (options?.extensions && typeof options.extensions === 'object') { + if (Array.isArray(options.extensions) || options instanceof Set) { + for (const value of options.extensions) { + const extension = (!value.startsWith('.') ? `.${value}` : value).trim() + if (extension) { + this.#extensions.add(extension.trim()) + } + } + } + } + + this.#cache.add( + 'status', + options?.cache?.status instanceof Cache + ? options.cache.status + : new Cache('loader.status', { loader: this, types: { RequestStatus } }) + ) + + this.#cache.add( + 'response', + options?.cache?.response instanceof Cache + ? options.cache.response + : new Cache('loader.response', { loader: this, types: { Response } }) + ) + + this.#cache.restore() + } + + /** + * The internal caches for this `Loader` object. + * @type {{ response: Cache, status: Cache }} + */ + get cache () { + return this.#cache + } + + /** + * Headers used in too loader requests. + * @type {Headers} + */ + get headers () { + return this.#headers + } + + /** + * A set of supported `Loader` extensions. + * @type {Set} + */ + get extensions () { + return this.#extensions + } + + /** + * The origin of this `Loader` object. + * @type {string} + */ + get origin () { return this.#origin } + set origin (origin) { + this.#origin = Loader.resolve(origin, location.origin) + } + + /** + * Loads a CommonJS module source file at `url` with an optional `origin`, which + * defaults to the application origin. + * @param {URL|string} url + * @param {URL|string|object} [origin] + * @param {RequestOptions=} [options] + * @return {Response} + */ + load (url, origin, options) { + if (origin && typeof origin === 'object' && !(origin instanceof URL)) { + options = origin + origin = options.origin ?? this.origin + } + + if (!origin) { + origin = this.origin + } + + const request = new Request(url, { + loader: this, + origin + }) + + return request.load(options) + } + + /** + * Queries the status of a CommonJS module source file at `url` with an + * optional `origin`, which defaults to the application origin. + * @param {URL|string} url + * @param {URL|string|object} [origin] + * @param {RequestOptions=} [options] + * @return {RequestStatus} + */ + status (url, origin, options = null) { + if (origin && typeof origin === 'object' && !(origin instanceof URL)) { + options = origin + origin = options.origin ?? this.origin + } + + if (!origin) { + origin = this.origin + } + + url = this.resolve(url, origin) + + // @ts-ignore + if (this.#cache.status.has(url)) { + // @ts-ignore + return this.#cache.status.get(url) + } + + const request = new Request(url, { + loader: this, + origin, + ...options + }) + + // @ts-ignore + this.#cache.status.set(url, request.status) + return request.status + } + + /** + * Resolves a given module URL to an absolute URL based on the loader origin. + * @param {URL|string} url + * @param {URL|string} [origin] + * @return {string} + */ + resolve (url, origin) { + return Loader.resolve(url, origin || this.origin) + } + + /** + * @ignore + */ + [Symbol.for('socket.runtime.util.inspect.custom')] () { + return `Loader ('${this.origin}') { }` + } +} + +export default Loader + +defineBuiltin('commonjs/loader', { + RequestStatus, + Response, + Request, + Loader +}) diff --git a/api/commonjs/module.js b/api/commonjs/module.js new file mode 100644 index 0000000000..d31c75e14a --- /dev/null +++ b/api/commonjs/module.js @@ -0,0 +1,923 @@ +/* global ErrorEvent */ +/* eslint-disable no-void, no-sequences */ +import { globalPaths, createRequire as createRequireImplementation } from './require.js' +import { DEFAULT_PACKAGE_PREFIX, Package } from './package.js' +import builtins, { defineBuiltin } from './builtins.js' +import application from '../application.js' +import { Loader } from './loader.js' +import location from '../location.js' +import process from '../process.js' +import path from '../path.js' +import fs from '../fs.js' + +/** + * @typedef {function(string, Module, function(string): any): any} ModuleResolver + */ + +/** + * @typedef {import('./require.js').RequireFunction} RequireFunction + */ + +/** + * @typedef {import('./package.js').PackageOptions} PackageOptions + */ + +/** + * @typedef {{ + * prefix?: string, + * request?: import('./loader.js').RequestOptions, + * builtins?: object + * } CreateRequireOptions + */ + +/** + * @typedef {{ + * resolvers?: ModuleResolver[], + * importmap?: ImportMap, + * loader?: Loader | object, + * loaders?: object, + * package?: Package | PackageOptions + * parent?: Module, + * state?: State + * }} ModuleOptions + */ + +/** + * @typedef {{ + * extensions?: object + * }} ModuleLoadOptions + */ + +export const builtinModules = builtins + +/** + * CommonJS module scope with module scoped globals. + * @ignore + * @param {object} exports + * @param {function(string): any} require + * @param {Module} module + * @param {string} __filename + * @param {string} __dirname + * @param {typeof process} process + * @param {object} global + */ +export function CommonJSModuleScope ( + exports, + require, + module, + __filename, + __dirname, + process, + global +) { + // eslint-disable-next-line no-unused-vars + const crypto = require('socket:crypto') + // eslint-disable-next-line no-unused-vars + const { Buffer } = require('socket:buffer') + + // eslint-disable-next-line no-unused-expressions + void exports, require, module, __filename, __dirname + // eslint-disable-next-line no-unused-expressions + void process, console, global, crypto, Buffer + + return (function () { + 'module code' + })() +} + +/** + * CommonJS module scope source wrapper. + * @type {string} + */ +export const COMMONJS_WRAPPER = CommonJSModuleScope + .toString() + .split(/'module code'/) + +/** + * A container for imports. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} + */ +export class ImportMap { + #imports = {} + + /** + * The imports object for the importmap. + * @type {object} + */ + get imports () { return this.#imports } + set imports (imports) { + if (imports && typeof imports === 'object' && !Array.isArray(imports)) { + this.#imports = {} + for (const key in imports) { + this.#imports[key] = imports[key] + } + } + } + + /** + * Extends the current imports object. + * @param {object} imports + * @return {ImportMap} + */ + extend (importmap) { + this.imports = { ...this.#imports, ...importmap?.imports ?? null } + return this + } +} + +/** + * A container for `Module` instance state. + */ +export class State { + loading = false + loaded = false + error = null + + /** + * `State` class constructor. + * @ignore + * @param {object|State=} [state] + */ + constructor (state = null) { + if (state && typeof state === 'object') { + for (const key in state) { + if (key in this && typeof state[key] === typeof this[key]) { + this[key] = state[key] + } + } + } + } +} + +/** + * The module scope for a loaded module. + * This is a special object that is seal, frozen, and only exposes an + * accessor the 'exports' field. + * @ignore + */ +export class ModuleScope { + #module = null + #exports = Object.create(null) + + /** + * `ModuleScope` class constructor. + * @param {Module} module + */ + constructor (module) { + this.#module = module + Object.freeze(this) + } + + get id () { + return this.#module.id + } + + get filename () { + return this.#module.filename + } + + get loaded () { + return this.#module.loaded + } + + get children () { + return this.#module.children + } + + get exports () { + return this.#exports + } + + set exports (exports) { + this.#exports = exports + } + + toJSON () { + return { + id: this.id, + filename: this.filename, + children: this.children, + exports: this.exports + } + } +} + +/** + * An abstract base class for loading a module. + */ +export class ModuleLoader { + /** + * Creates a `ModuleLoader` instance from the `module` currently being loaded. + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {ModuleLoader} + */ + static from (module, options = null) { + const loader = new this(module, options) + return loader + } + + /** + * Creates a new `ModuleLoader` instance from the `module` currently + * being loaded with the `source` string to parse and load with optional + * `ModuleLoadOptions` options. + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + static load (module, options = null) { + return this.from(module, options).load(module, options) + } + + /** + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + load (module, options = null) { + // eslint-disable-next-line + void module + // eslint-disable-next-line + void options + return false + } +} + +/** + * A JavaScript module loader + */ +export class JavaScriptModuleLoader extends ModuleLoader { + /** + * Loads the JavaScript module. + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + load (module, options = null) { + const response = module.loader.load(module.id, options) + const compiled = Module.compile(response.text, { url: response.id }) + const __filename = module.id + const __dirname = path.dirname(__filename) + + // eslint-disable-next-line no-useless-call + const result = compiled.call(null, + module.scope.exports, + module.createRequire(options), + module.scope, + __filename, + __dirname, + process, + globalThis + ) + + if (typeof result?.catch === 'function') { + result.catch((error) => { + error.module = module + module.dispatchEvent(new ErrorEvent('error', { error })) + }) + } + + return true + } +} + +/** + * A JSON module loader. + */ +export class JSONModuleLoader extends ModuleLoader { + /** + * Loads the JSON module. + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + load (module, options = null) { + const response = module.loader.load(module.id, options) + if (response.text) { + module.scope.exports = JSON.parse(response.text) + } + return true + } +} +/** + * A WASM module loader + + */ +export class WASMModuleLoader extends ModuleLoader { + /** + * Loads the WASM module. + * @param {string} + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + load (module, options = null) { + const response = module.loader.load(module.id, { + ...options, + responseType: 'arraybuffer' + }) + + const instance = new WebAssembly.Instance( + new WebAssembly.Module(response.buffer), + options || undefined + ) + + module.scope.exports = instance.exports + return true + } +} + +/** + * A container for a loaded CommonJS module. All errors bubble + * to the "main" module and global object (if possible). + */ +export class Module extends EventTarget { + /** + * A reference to the currently scoped module. + * @type {Module?} + */ + static current = null + + /** + * A reference to the previously scoped module. + * @type {Module?} + */ + static previous = null + + /** + * A cache of loaded modules + * @type {Map} + */ + static cache = new Map() + + /** + * An array of globally available module loader resolvers. + * @type {ModuleResolver[]} + */ + static resolvers = [] + + /** + * Globally available 'importmap' for all loaded modules. + * @type {ImportMap} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} + */ + static importmap = new ImportMap() + + /** + * A limited set of builtins exposed to CommonJS modules. + * @type {object} + */ + static builtins = builtins + + /** + * A limited set of builtins exposed to CommonJS modules. + * @type {object} + */ + static builtinModules = builtinModules + + /** + * CommonJS module scope source wrapper components. + * @type {string[]} + */ + static wrapper = COMMONJS_WRAPPER + + /** + * An array of global require paths, relative to the origin. + * @type {string[]} + */ + static globalPaths = globalPaths + + /** + * Globabl module loaders + * @type {object} + */ + static loaders = Object.assign(Object.create(null), { + '.js' (module, options = null) { + return JavaScriptModuleLoader.load(module, options) + }, + + '.cjs' (module, options = null) { + return JavaScriptModuleLoader.load(module, options) + }, + + '.json' (module, options = null) { + return JSONModuleLoader.load(module, options) + }, + + '.wasm' (source, module, options = null) { + return WASMModuleLoader.load(module, options) + } + }) + + /** + * The main entry module, lazily created. + * @type {Module} + */ + static get main () { + if (this.cache.has(location.origin)) { + return this.cache.get(location.origin) + } + + let packageInfo = '' + + try { + packageInfo = fs.readFileSync('package.json', 'utf8') + } catch (err) { + } + + if (packageInfo.length) { + try { + packageInfo = JSON.parse(packageInfo) + } catch (err) { + console.warn(err) + packageInfo = null + } + } + + const main = new Module(location.origin, { + state: new State({ loaded: true }), + parent: null, + package: new Package(location.origin, { + id: location.href, + type: 'commonjs', + info: packageInfo || application.config, + index: '', + prefix: '', + manifest: '', + + // eslint-disable-next-line + name: packageInfo?.name || application.config.build_name, + // eslint-disable-next-line + version: packageInfo?.version || application.config.meta_version, + // eslint-disable-next-line + description: packageInfo?.description || application.config.meta_description, + imports: packageInfo?.imports || {}, + exports: { + '.': { + default: location.pathname + } + } + }) + }) + + this.cache.set(location.origin, main) + + if (globalThis.window && globalThis === globalThis.window) { + try { + const importmapScriptElement = globalThis.document.querySelector('script[type=importmap]') + if (importmapScriptElement && importmapScriptElement.textContent) { + const importmap = JSON.parse(importmapScriptElement.textContent) + Module.importmap.extend(importmap) + } + } catch (err) { + globalThis.reportError(err) + } + } + + return main + } + + /** + * Wraps source in a CommonJS module scope. + * @param {string} source + */ + static wrap (source) { + const [head, tail] = this.wrapper + const body = String(source || '') + return [head, body, tail].join('\n') + } + + /** + * Compiles given JavaScript module source. + * @param {string} source + * @param {{ url?: URL | string }=} [options] + * @return {function( + * object, + * function(string): any, + * Module, + * string, + * string, + * typeof process, + * object + * ): any} + */ + static compile (source, options = null) { + const wrapped = Module.wrap(source) + .replace('function CommonJSModuleScope', `"Module (${options?.url ?? ''})"`) + // eslint-disable-next-line + const compiled = new Function(` + const __commonjs_module_scope_container__ = {${wrapped}}; + return __commonjs_module_scope_container__[Object.keys(__commonjs_module_scope_container__)[0]]; + // # sourceURL=${options?.url ?? ''} + `)() + + return compiled + } + + /** + * Creates a `Module` from source URL and optionally a parent module. + * @param {string|URL|Module} url + * @param {ModuleOptions=} [options] + */ + static from (url, options = null) { + if (typeof url === 'object' && url instanceof Module) { + return this.from(url.id, options) + } + + if (options instanceof Module) { + options = { parent: options } + } else { + options = { ...options } + } + + if (!options.parent) { + options.parent = Module.current + } + + url = Loader.resolve(url, options.parent?.id) + + if (this.cache.has(url)) { + return this.cache.get(url) + } + + const module = new Module(url, options) + return module + } + + /** + * Creates a `require` function from a given module URL. + * @param {string|URL} url + * @param {ModuleOptions=} [options] + */ + static createRequire (url, options = null) { + const module = Module.from(url, { + package: { info: Module.main.package.info }, + ...options + }) + + return module.createRequire(options) + } + + #id = null + #scope = null + #state = new State() + #cache = Object.create(null) + #loader = null + #parent = null + #package = null + #children = [] + #resolvers = [] + #importmap = new ImportMap() + #loaders = Object.create(null) + + /** + * `Module` class constructor. + * @param {string|URL} url + * @param {ModuleOptions=} [options] + */ + constructor (url, options = null) { + super() + + options = { ...options } + + if (options.parent && options.parent instanceof Module) { + this.#parent = options.parent + } else if (options.parent === undefined) { + this.#parent = Module.main + } + + if (this.#parent !== null) { + this.#id = Loader.resolve(url, this.#parent.id) + this.#cache = this.#parent.cache + } else { + this.#id = Loader.resolve(url) + } + + if (options.state && options.state instanceof State) { + this.#state = options.state + } + + this.#scope = new ModuleScope(this) + this.#loader = new Loader(this.#id, options?.loader) + this.#package = options.package instanceof Package + ? options.package + : this.#parent?.package ?? new Package(options.name ?? this.#id, options.package) + + this.addEventListener('error', (event) => { + if (event.error) { + this.#state.error = event.error + } + + if (Module.main === this) { + // bubble error to globalThis, if possible + if (typeof globalThis.dispatchEvent === 'function') { + // @ts-ignore + globalThis.dispatchEvent(new ErrorEvent('error', event)) + } + } else { + // bubble errors to main module + Module.main.dispatchEvent(new ErrorEvent('error', event)) + } + }) + + this.#importmap.extend(Module.importmap) + Object.assign(this.#loaders, Module.loaders) + + if (options.importmap) { + this.#importmap.extend(options.importmap) + } + + if (Array.isArray(options.resolvers)) { + for (const resolver of options.resolvers) { + if (typeof resolver === 'function') { + this.#resolvers.push(resolver) + } + } + } + + // includes `.browser` field mapping + for (const key in this.package.imports) { + const value = this.package.imports[key] + if (!value) continue + + this.#resolvers.push((specifier, ctx, next) => { + if (specifier === key) { + return (typeof value === 'string' + ? value + : value.default ?? value.browser ?? next(specifier) + ) + } + + return next(specifier) + }) + } + + if (this.#parent) { + Object.assign(this.#loaders, this.#parent.loaders) + + if (Array.isArray(options?.resolvers)) { + for (const resolver of options.resolvers) { + if (typeof resolver === 'function') { + this.#resolvers.push(resolver) + } + } + } + + this.#importmap.extend(this.#parent.importmap) + } + + if (options.loaders && typeof options.loaders === 'object') { + Object.assign(this.#loaders, options.loaders) + } + + Module.cache.set(this.id, this) + } + + /** + * A unique ID for this module. + * @type {string} + */ + get id () { + return this.#id + } + + /** + * A reference to the "main" module. + * @type {Module} + */ + get main () { + return Module.main + } + + /** + * Child modules of this module. + * @type {Module[]} + */ + get children () { + return this.#children + } + + /** + * A reference to the module cache. Possibly shared with all + * children modules. + * @type {object} + */ + get cache () { + return this.#cache + } + + /** + * A reference to the module package. + * @type {Package} + */ + get package () { + return this.#package + } + + /** + * The `ImportMap` for this module. + * @type {ImportMap} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} + */ + get importmap () { + return this.#importmap + } + + /** + * The module level resolvers. + * @type {ModuleResolver[]} + */ + get resolvers () { + const resolvers = Array.from(Module.resolvers).concat(this.#resolvers) + return Array.from(new Set(resolvers)) + } + + /** + * `true` if the module is currently loading, otherwise `false`. + * @type {boolean} + */ + get loading () { + return this.#state.loading + } + + /** + * `true` if the module is currently loaded, otherwise `false`. + * @type {boolean} + */ + get loaded () { + return this.#state.loaded + } + + /** + * An error associated with the module if it failed to load. + * @type {Error?} + */ + get error () { + return this.#state.error + } + + /** + * The exports of the module + * @type {object} + */ + get exports () { + return this.#scope.exports + } + + /** + * The scope of the module given to parsed modules. + * @type {ModuleScope} + */ + get scope () { + return this.#scope + } + + /** + * The origin of the loaded module. + * @type {string} + */ + get origin () { + return this.#loader.origin + } + + /** + * The parent module for this module. + * @type {Module?} + */ + get parent () { + return this.#parent + } + + /** + * The `Loader` for this module. + * @type {Loader} + */ + get loader () { + return this.#loader + } + + /** + * The filename of the module. + * @type {string} + */ + get filename () { + return this.#id + } + + /** + * Known source loaders for this module keyed by file extension. + * @type {object} + */ + get loaders () { + return this.#loaders + } + + /** + * Factory for creating a `require()` function based on a module context. + * @param {CreateRequireOptions=} [options] + * @return {RequireFunction} + */ + createRequire (options = null) { + return createRequireImplementation({ + builtins, + // TODO(@jwerle): make the 'prefix' value configurable somehow + prefix: DEFAULT_PACKAGE_PREFIX, + ...options, + module: this + }) + } + + /** + * Creates a `Module` from source the URL with this module as + * the parent. + * @param {string|URL|Module} url + * @param {ModuleOptions=} [options] + */ + createModule (url, options = null) { + return Module.from(url, { + parent: this, + ...options + }) + } + + /** + * Requires a module at for a given `input` which can be a relative file, + * named module, or an absolute URL within the context of this odule. + * @param {string|URL} input + * @param {RequireOptions=} [options] + * @throws ModuleNotFoundError + * @throws ReferenceError + * @throws SyntaxError + * @throws TypeError + * @return {any} + */ + require (url, options = null) { + const require = this.createRequire(options) + return require(url, options) + } + + /** + * Loads the module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + load (options = null) { + const extension = path.extname(this.id) + + if (this.#state.loaded) { + return true + } + + if (typeof this.#loaders[extension] !== 'function') { + return false + } + + try { + this.#state.loading = true + + if (this.#loaders[extension](this, options)) { + if (this.#parent) { + this.#parent.children.push(this) + } + + this.#state.loaded = true + } + } catch (error) { + error.module = this + this.#state.error = error + this.dispatchEvent(new ErrorEvent('error', { error })) + throw error + } finally { + this.#state.loading = false + } + + return this.#state.loaded + } + + resolve (input) { + return this.package.resolve(input, { load: false, origin: this.id }) + } + + /** + * @ignore + */ + [Symbol.toStringTag] () { + return 'Module' + } +} + +/** + * Creates a `require` function from a given module URL. + * @param {string|URL} url + * @param {ModuleOptions=} [options] + * @return {RequireFunction} + */ +export function createRequire (url, options = null) { + return Module.createRequire(url, options) +} + +Module.Module = Module + +export default Module + +defineBuiltin('commonjs/module', Module, false) diff --git a/api/commonjs/package.js b/api/commonjs/package.js new file mode 100644 index 0000000000..e0b3a51ae1 --- /dev/null +++ b/api/commonjs/package.js @@ -0,0 +1,1301 @@ +/** + * @module commonjs.package + */ +import { ModuleNotFoundError } from '../errors.js' +import { defineBuiltin } from './builtins.js' +import { isESMSource } from '../util.js' +import { Loader } from './loader.js' +import location from '../location.js' +import path from '../path.js' +import URL from '../url.js' + +/** + * `true` if in a worker scope. + * @type {boolean} + * @ignore + */ +const isWorkerScope = globalThis.self === globalThis && !globalThis.window + +/** + * @ignore + * @param {string} source + * @return {boolean} + */ +export function detectESMSource (source) { + return isESMSource(source) +} + +/** + * @typedef {{ + * manifest?: string, + * index?: string, + * description?: string, + * version?: string, + * license?: string, + * exports?: object, + * type?: 'commonjs' | 'module', + * info?: object, + * origin?: string, + * dependencies?: Dependencies | object | Map + * }} PackageOptions + */ + +/** + * @typedef {import('./loader.js').RequestOptions & { + * type?: 'commonjs' | 'module' + * prefix?: string + * }} PackageLoadOptions + */ + +/** + * {import('./loader.js').RequestOptions & { + * load?: boolean, + * type?: 'commonjs' | 'module', + * browser?: boolean, + * children?: string[] + * extensions?: string[] | Set + * }} PackageResolveOptions + */ + +/** + * @typedef {{ + * organization: string | null, + * name: string, + * version: string | null, + * pathname: string, + * url: URL, + * isRelative: boolean, + * hasManifest: boolean + * }} ParsedPackageName + */ + +/** + * @typedef {{ + * require?: string | string[], + * import?: string | string[], + * default?: string | string[], + * default?: string | string[], + * worker?: string | string[], + * browser?: string | string[] + * }} PackageExports + +/** + * The default package index file such as 'index.js' + * @type {string} + */ +export const DEFAULT_PACKAGE_INDEX = 'index.js' + +/** + * The default package manifest file name such as 'package.json' + * @type {string} + */ +export const DEFAULT_PACKAGE_MANIFEST_FILE_NAME = 'package.json' + +/** + * The default package path prefix such as 'node_modules/' + * @type {string} + */ +export const DEFAULT_PACKAGE_PREFIX = 'node_modules/' + +/** + * The default package version, when one is not provided + * @type {string} + */ +export const DEFAULT_PACKAGE_VERSION = '0.0.1' + +/** + * The default license for a package' + * @type {string} + */ +export const DEFAULT_LICENSE = 'Unlicensed' + +/** + * A container for a package name that includes a package organization identifier, + * its fully qualified name, or for relative package names, its pathname + */ +export class Name { + /** + * Parses a package name input resolving the actual module name, including an + * organization name given. If a path includes a manifest file + * ('package.json'), then the directory containing that file is considered a + * valid package and it will be included in the returned value. If a relative + * path is given, then the path is returned if it is a valid pathname. This + * function returns `null` for bad input. + * @param {string|URL} input + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @return {ParsedPackageName?} + */ + static parse (input, options = null) { + if (typeof input === 'string') { + input = input.trim() + } + + if (!input) { + return null + } + + const origin = options?.origin ?? location.origin + const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME + // relative or absolute path given, which is still relative to the origin + const isRelative = ( + typeof input === 'string' && + (input.startsWith('.') || input.startsWith('/')) + ) + + let hasManifest = false + let url = null + + // URL already given, ignore the origin + // @ts-ignore + if (URL.canParse(input) || input instanceof URL) { + url = new URL(input) + } else { + url = new URL(input, origin) + } + + // invalid input if a URL was unable to be determined + if (!url) { + return null + } + + let pathname = url.pathname.replace(new URL(origin).pathname, '') + + if (isRelative && pathname.startsWith('/') && input.startsWith('.')) { + pathname = `./${pathname.slice(1)}` + } else if (pathname.startsWith('/')) { + pathname = pathname.slice(1) + } + + // manifest was given in name, just use the directory name + if (pathname.endsWith(`/${manifest}`)) { + hasManifest = true + pathname = pathname.split('/').slice(0, -1).join('/') + } + + // name included organization + if (pathname.startsWith('@')) { + const components = pathname.split('/') + const organization = components[0] + let [name, version = null] = components[1].split('@') + pathname = [organization || '', name, ...components.slice(2)].filter(Boolean).join('/') + + // manifest was given, this could be a nested package + if (hasManifest) { + name = [organization, name].filter(Boolean).concat(components.slice(2)).join('/') + const r = { + name, + version, + organization, + pathname, + url, + isRelative, + hasManifest + } + return r + } + + // only a organization was given, return `null` + if (components.length === 1) { + return null + } + + name = `${organization}/${name}` + + // only `@/` was given + if (components.length === 2) { + return { + name, + version, + organization, + pathname, + url, + isRelative, + hasManifest + } + } + + // `@//...` was given + return { + name, + version, + organization, + pathname, + url, + isRelative, + hasManifest + } + } + + // a valid relative path was given, just return it normalized + if (isRelative) { + if (input.startsWith('/')) { + pathname = `/${pathname}` + } else { + pathname = `./${pathname}` + } + + return { + organization: null, + version: null, + name: pathname, + pathname, + url, + isRelative, + hasManifest + } + } + + // at this point, a named module was given + const components = pathname.split('/') + const [name, version = null] = components[0].split('@') + pathname = [name, ...components.slice(1)].filter(Boolean).join('/') + + // manifest was given, this could be a nested package + if (hasManifest) { + return { + organization: null, + name: pathname, + pathname, + url, + isRelative, + hasManifest + } + } + + return { + organization: null, + name: name || pathname, + version, + pathname: name && pathname ? pathname : null, + url, + isRelative, + hasManifest + } + } + + /** + * Returns `true` if the given `input` can be parsed by `Name.parse` or given + * as input to the `Name` class constructor. + * @param {string|URL} input + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @return {boolean} + */ + static canParse (input, options = null) { + const origin = options?.origin ?? location.origin + + if (typeof input === 'string') { + input = input.trim() + } + + if (!input) { + return null + } + + // URL already given, ignore the origin + // @ts-ignore + return input instanceof URL || URL.canParse(input, origin) + } + + /** + * Creates a new `Name` from input. + * @param {string|URL} input + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @return {Name} + */ + static from (input, options = null) { + if (!Name.canParse(input, options)) { + throw new TypeError( + `Cannot create new 'Name'. Invalid 'input' given. Received: ${input}` + ) + } + + return new Name(Name.parse(input, options)) + } + + #name = null + #origin = null + #version = null + #pathname = null + #organization = null + + #isRelative = false + + /** + * `Name` class constructor. + * @param {string|URL|NameOptions|Name} name + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @throws TypeError + */ + constructor (name, options = null) { + /** @type {ParsedPackageName?} */ + let parsed = null + if (typeof name === 'string' || name instanceof URL) { + parsed = Name.parse(name, options) + } else if (name && typeof name === 'object') { + parsed = { + organization: name.organization || null, + name: name.name || null, + pathname: name.pathname || null, + version: name.version || null, + // @ts-ignore + url: name.url instanceof URL || URL.canParse(name.url) + ? new URL(name.url) + : null, + isRelative: name.isRelative || false, + hasManifest: name.hasManifest || false + } + } + + if (parsed === null) { + throw new TypeError(`Invalid 'name' given. Received: ${name}`) + } + + this.#name = parsed.name + this.#origin = parsed.url?.origin ?? location.origin + this.#version = parsed.version ?? null + this.#pathname = parsed.pathname + this.#organization = parsed.organization + + this.#isRelative = parsed.isRelative + } + + /** + * The id of this package name. + * @type {string} + */ + get id () { + return this.#pathname + } + + /** + * The actual package name. + * @type {string} + */ + get name () { return this.#name } + + /** + * An alias for 'name'. + * @type {string} + */ + get value () { return this.#name } + + /** + * The origin of the package, if available. + * This value may be `null`. + * @type {string?} + */ + get origin () { return this.#origin } + + /** + * The package version if available. + * This value may be `null`. + * @type {string?} + */ + get version () { return this.#version } + + /** + * The actual package pathname, if given in name string. + * This value is always a string defaulting to '.' if no path + * was given in name string. + * @type {string} + */ + get pathname () { return this.#pathname || '.' } + + /** + * The organization name. + * This value may be `null`. + * @type {string?} + */ + get organization () { return this.#organization } + + /** + * `true` if the package name was relative, otherwise `false`. + * @type {boolean} + */ + get isRelative () { return this.#isRelative } + + /** + * Converts this package name to a string. + * @ignore + * @return {string} + */ + toString () { + const { organization, name } = this + + let pathname = this.#pathname !== name ? this.#pathname : '' + + if (pathname && pathname.startsWith('/')) { + pathname = pathname.slice(1) + } + + if (organization) { + if (pathname) { + return `@${organization}/${name}/${pathname}` + } + + return `@${organization}/${name}` + } else if (this.isRelative) { + return `./${pathname}` + } else if (pathname) { + return `${name}/${pathname}` + } + + return name + } + + /** + * Converts this `Name` instance to JSON. + * @ignore + * @return {object} + */ + toJSON () { + return { + name: this.name, + origin: this.origin, + version: this.version, + pathname: this.pathname, + organization: this.organization + } + } +} + +/** + * A container for package dependencies that map a package name to a `Package` instance. + */ +export class Dependencies { + #map = new Map() + #origin = null + #package = null + + constructor (parent, options = null) { + this.#package = parent + this.#origin = options?.origin ?? parent?.origin + } + + get map () { + return this.#map + } + + get origin () { + return this.#origin ?? null + } + + add (name, info = null) { + if (info instanceof Package) { + this.#map.set(name, info) + } else { + this.#map.set(name, new Package(name, { + loader: this.#package.loader, + origin: this.#origin, + info + })) + } + + this.#map.get(name) + } + + get (name, options = null) { + const dependency = this.#map.get(name) + if (dependency) { + // try to load + dependency.load(options) + return dependency + } + } + + entries () { + return this.#map.entries() + } + + keys () { + return this.#map.keys() + } + + values () { + return this.#map.values() + } + + load (options = null) { + for (const dependency of this.values()) { + dependency.load(options) + } + } + + [Symbol.iterator] () { + return this.#map.entries() + } +} + +/** + * A container for CommonJS module metadata, often in a `package.json` file. + */ +export class Package { + /** + * A high level class for a package name. + * @type {typeof Name} + */ + static Name = Name + + /** + * A high level container for package dependencies. + * @type {typeof Dependencies} + */ + static Dependencies = Dependencies + + /** + * Creates and loads a package + * @param {string|URL|NameOptions|Name} name + * @param {PackageOptions & PackageLoadOptions=} [options] + * @return {Package} + */ + static load (name, options = null) { + const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME + const pkg = new this(name, options) + if (name.endsWith(manifest) || !path.extname(name)) { + pkg.load(options) + } + return pkg + } + + #id = null + #name = null + #type = 'commonjs' + #license = null + #version = null + #description = null + #dependencies = null + + #info = null + #loader = null + + #imports = {} + + /** + * @type {Record} + */ + #exports = {} + + /** + * `Package` class constructor. + * @param {string|URL|NameOptions|Name} name + * @param {PackageOptions=} [options] + */ + constructor (name, options = null) { + options = /** @type {PackageOptions} */ ({ ...options }) + + if (typeof name !== 'string' && !(name instanceof Name) && !(name instanceof URL)) { + throw new TypeError(`Expecting 'name' to be a string or URL. Received: ${name}`) + } + + // the module loader + this.#loader = new Loader(options.loader) + + this.#id = options.id ?? null + this.#name = Name.from(name, { + origin: options.origin ?? options.loader?.origin ?? this.#loader.origin ?? null, + manifest: options.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME + }) + + // early meta data + this.#info = options.info ?? null + this.#type = options.type && /(commonjs|module)/.test(options.type) + ? options.type + : this.#type + this.#exports = options.exports ?? this.#exports + this.#imports = options.imports ?? this.#imports + this.#license = options.license ?? DEFAULT_LICENSE + this.#version = options.version ?? this.#name.version ?? DEFAULT_PACKAGE_VERSION + this.#description = options.description ?? '' + this.#dependencies = new Dependencies(this) + + if (options.dependencies && typeof options.dependencies === 'object') { + if (options.dependencies instanceof Dependencies || typeof options.dependencies.entries === 'function') { + for (const [key, value] of options.dependencies.entries()) { + this.#dependencies.add(key, value) + } + } else { + for (const key in options.dependencies) { + const value = options.dependencies[key] + this.#dependencies.add(key, value) + } + } + } + + if (!this.#exports || typeof this.#exports !== 'object') { + this.#exports = { '.': null } + } + + if (!this.#exports['.']) { + this.#exports = { + '.': { + require: options.index ?? DEFAULT_PACKAGE_INDEX, + import: options.index ?? DEFAULT_PACKAGE_INDEX, + default: options.index ?? DEFAULT_PACKAGE_INDEX + } + } + } + } + + /** + * The unique ID of this `Package`, which is the absolute + * URL of the directory that contains its manifest file. + * @type {string} + */ + get id () { + return this.#id + } + + /** + * The absolute URL to the package manifest file + * @type {string} + */ + get url () { + return new URL(this.#id).href + } + + /** + * A reference to the package subpath imports and browser mappings. + * These values are typically used with its corresponding `Module` + * instance require resolvers. + * @type {object} + */ + get imports () { + return this.#imports + } + + /** + * A loader for this package, if available. This value may be `null`. + * @type {Loader} + */ + get loader () { + return this.#loader + } + + /** + * `true` if the package was actually "loaded", otherwise `false`. + * @type {boolean} + */ + get loaded () { + return this.#info !== null + } + + /** + * The name of the package. + * @type {string} + */ + get name () { + return this.#name?.id ?? '' + } + + /** + * The description of the package. + * @type {string} + */ + get description () { + return this.#description ?? '' + } + + /** + * The organization of the package. This value may be `null`. + * @type {string?} + */ + get organization () { + return this.#name.organization ?? null + } + + /** + * The license of the package. + * @type {string} + */ + get license () { + return this.#license ?? DEFAULT_LICENSE + } + + /** + * The version of the package. + * @type {string} + */ + get version () { + return this.#version ?? DEFAULT_PACKAGE_VERSION + } + + /** + * The origin for this package. + * @type {string} + */ + get origin () { + return this.loader.origin + } + + /** + * The exports mappings for the package + * @type {object} + */ + get exports () { + return this.#exports + } + + /** + * The package type. + * @type {'commonjs'|'module'} + */ + get type () { + return this.#type + } + + /** + * The raw package metadata object. + * @type {object?} + */ + get info () { + return this.#info ?? null + } + + /** + * @type {Dependencies} + */ + get dependencies () { + return this.#dependencies + } + + /** + * An alias for `entry` + * @type {string?} + */ + get main () { + return this.entry + } + + /** + * The entry to the package + * @type {string?} + */ + get entry () { + let entry = null + + if (Array.isArray(this.#exports['.'])) { + for (const exports of this.#exports['.']) { + if (typeof exports === 'string') { + entry = exports + } else if (exports && typeof exports === 'object') { + if (isWorkerScope && exports.worker) { + entry = exports.worker + } else if (this.type === 'commonjs') { + entry = exports.require || exports.default + } else if (this.type === 'module') { + entry = exports.import || exports.default + } + } + + if (entry) { + break + } + } + } + + if (this.type === 'commonjs') { + entry = this.#exports['.'].require + } else if (this.type === 'module') { + entry = this.#exports['.'].import + } + + if (!entry && this.#exports['.'].default) { + entry = this.#exports['.'].default + } + + if (isWorkerScope) { + if (!entry && this.#exports['.'].worker) { + entry = this.#exports['.'].worker + } + } + + if (!entry && this.#exports['.'].browser) { + entry = this.#exports['.'].browser + } + + if (entry) { + if (!entry.startsWith('./')) { + entry = `./${entry}` + } else if (!entry.startsWith('.')) { + entry = `.${entry}` + } + + // @ts-ignore + if (URL.canParse(entry, this.id)) { + return new URL(entry, this.id).href + } + } + + return null + } + + /** + * Load the package information at an optional `origin` with + * optional request `options`. + * @param {PackageLoadOptions=} [options] + * @throws SyntaxError + * @return {boolean} + */ + load (origin = null, options = null) { + if (origin && typeof origin === 'object' && !(origin instanceof URL)) { + options = origin + origin = options.origin + } + + if (!this.origin || origin === this.origin) { + if (options?.force !== true && this.#info) { + return true + } + } + + if (!origin) { + origin = this.origin + } + + const prefix = options?.prefix ?? '' + const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME + const pathname = `${prefix}${this.name}/${manifest}` + const response = this.loader.load(pathname, origin, options) + + if (!response.text) { + const entry = path.basename(this.entry ?? './index.js') + const pathname = `${prefix}${this.name}/${entry}` + const response = this.loader.load(pathname, origin, options) + + if (response.text) { + this.#id = response.id + this.#info = { + name: this.name.value, + main: entry, + module: entry + } + + if (detectESMSource(response.text)) { + this.#info.type = 'module' + } else { + this.#info.type = 'commonjs' + } + + this.#type = this.#info.type + return true + } + + return false + } + + const info = JSON.parse(response.text) + + if (!info || typeof info !== 'object') { + return false + } + + const type = options?.type ?? info.type ?? this.#type + + this.#info = info + this.#type = type + + this.#id = new URL('./', response.id).href + this.#name = info.name + ? Name.from(info.name, { origin }) + : Name.from(this.#name.value, { origin }) + this.#license = info.license ?? 'Unlicensed' + this.#version = info.version + this.#description = info.description + + this.#loader.origin = origin + + if (info.dependencies && typeof info.dependencies === 'object') { + for (const name in info.dependencies) { + const version = info.dependencies[name] + if (typeof version === 'string') { + this.#dependencies.add(name, { + version + }) + } + } + } + + if (info.main && !info.module && !info.type) { + this.#type = 'commonjs' + } + + if (info.main) { + if (info.type === 'module') { + this.#exports['.'].import = info.main + } else { + this.#exports['.'].require = info.main + } + } + + if (info.module && !info.main) { + this.#exports['.'].import = info.module + this.#type = 'module' + } + + if (!this.#exports['.']) { + this.#exports['.'] = {} + } + + if (typeof info.exports === 'string') { + if (this.#type === 'commonjs') { + this.#exports['.'].require = info.exports + } else if (this.#type === 'module') { + this.#exports['.'].import = info.exports + } + } + + if (info.exports && typeof info.exports === 'object') { + if (info.exports.import || info.exports.require || info.exports.default) { + this.#exports['.'] = info.exports + } else { + for (const key in info.exports) { + const exports = info.exports[key] + if (!exports) { + continue + } + + if (typeof exports === 'string') { + this.#exports[key] = {} + if (this.#type === 'commonjs') { + this.#exports[key].require = exports + } else if (this.#type === 'module') { + this.#exports[key].import = exports + } + } else if (typeof exports === 'object') { + this.#exports[key] = exports + } + } + } + } + + for (const key in this.#exports) { + const exports = this.#exports[key] + if (Array.isArray(exports)) { + for (let i = 0; i < exports.length; ++i) { + const value = exports[i] + if (typeof value === 'string') { + if (value.startsWith('/')) { + exports[i] = `.${value}` + } else if (!value.startsWith('.')) { + exports[i] = `./${value}` + } + } + } + } else { + for (const condition in exports) { + const value = exports[condition] + if (Array.isArray(value)) { + for (let i = 0; i < value.length; ++i) { + if (typeof value[i] === 'string') { + if (value[i].startsWith('/')) { + value[i] = `.${value[i]}` + } else if (!value[i].startsWith('.')) { + value[i] = `./${value[i]}` + } + } + } + } else if (typeof value === 'string') { + if (value.startsWith('/')) { + exports[condition] = `.${value}` + } else if (!value.startsWith('.')) { + exports[condition] = `./${value}` + } + } + } + } + } + + if ( + this.#info.imports && + !Array.isArray(this.#info.imports) && + typeof this.#info.imports === 'object' + ) { + for (const key in this.#info.imports) { + const value = this.#info.imports[key] + if (typeof value === 'string') { + this.#imports[key] = { default: value } + } else if (value && typeof value === 'object') { + this.#imports[key] = {} + if (value.default) { + this.#imports[key].default = value.default + } + + if (value.browser) { + this.#imports[key].browser = value.browser + } + } + } + } + + if ( + this.#info.browser && + !Array.isArray(this.#info.browser) && + typeof this.#info.browser === 'object' + ) { + for (const key in this.#info.browser) { + const value = this.#info.browser[key] + + if (typeof value === 'string') { + if (key.startsWith('.')) { + if (this.#exports[key]) { + this.#exports[key].browser = value + } + } else { + this.#imports[key] ??= { } + this.#imports[key].browser = value + } + } else if (value && typeof value === 'object') { + this.#imports[key] ??= {} + if (value.default) { + this.#imports[key].default = value.default + } + + if (value.browser) { + this.#imports[key].browser = value.browser + } + } + } + } + + if (this.#type === 'module') { + if (this.#info.type !== 'module' && this.entry) { + const source = this.loader.load(this.entry, origin, options).text + if (!detectESMSource(source)) { + this.#type = 'commonjs' + } + } + } + + return true + } + + /** + * Resolve a file's `pathname` within the package. + * @param {string|URL} pathname + * @param {PackageResolveOptions=} [options] + * @return {string} + */ + resolve (pathname, options = null) { + if (options?.load !== false) { + this.load(options) + } + + const { info } = this + const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME + const type = options?.type ?? this.type + + if (info?.addon === true) { + throw new ModuleNotFoundError( + `Cannot find module '${pathname}' (requested module is a Node.js addon)`, + options?.children?.map?.((mod) => mod.id) + ) + } + + let origin = this.id + + // an absolute URL was given, just try to resolve it + // @ts-ignore + if (pathname instanceof URL || URL.canParse(pathname)) { + const url = new URL(pathname) + const response = this.loader.status(url.href, options) + + if (response.ok) { + return interpolateBrowserResolution(response.id) + } + + pathname = url.pathname + origin = url.origin + } + + if (pathname === '.') { + pathname = './' + } else if (pathname.startsWith('/')) { + pathname = `.${pathname}` + } else if (!pathname.startsWith('.')) { + pathname = `./${pathname}` + } + + if (options?.origin) { + origin = options.origin + } else if (!origin) { + origin = new URL(this.name, this.origin).href + if (!origin.endsWith('/')) { + origin += '/' + } + } + + // if the pathname ends with the manifest file ('package.json'), then + // construct a new `Package` with the this packages loader and resolve it + if (pathname.endsWith(`/${manifest}`)) { + const url = new URL(`${this.name}/${pathname}`, origin) + const childPackage = new Package(url, { + loader: this.loader, + origin + }) + + // if it loaded, then just return the URL + if (childPackage.load()) { + return url.href + } + } + + const extname = path.extname(pathname) + const extensions = extname !== '' && this.loader.extensions.has(extname) + ? new Set([extname]) + : new Set(Array + .from(options?.extensions ?? []) + .concat('') + .concat(Array.from(this.loader.extensions)) + .filter((e) => typeof e === 'string') + ) + + if (pathname.endsWith('/')) { + pathname += 'index.js' + } + + for (const extension of extensions) { + for (const key in this.#exports) { + const query = pathname !== '.' && pathname !== './' + ? pathname + extension + : pathname + + let exports = this.#exports[key] + let filename = null + + if ( + key === query || + key === pathname.replace(extname, '') || + (pathname === './' && key === '.') || + (pathname === './index' && key === '.') || + (pathname === './index.js' && key === '.') + ) { + if (Array.isArray(exports)) { + for (const entry of exports) { + if (typeof entry === 'string') { + const response = this.loader.load(entry, origin, options) + if (response.ok) { + return interpolateBrowserResolution(response.id) + } + } else if (entry && typeof entry === 'object') { + exports = entry + break + } + } + } + + if (exports && !Array.isArray(exports) && typeof exports === 'object') { + if (isWorkerScope && exports.worker) { + filename = exports.worker + } if (type === 'commonjs' && exports.require) { + filename = exports.require + } else if (type === 'module' && exports.import) { + filename = exports.import + } else if (exports.browser) { + filename = exports.browser + } else if (exports.default) { + filename = exports.default + } else { + filename = ( + exports.require || + exports.import || + exports.browser || + exports.default + ) + } + } + + if (filename) { + const response = this.loader.load(filename, origin, options) + if (response.ok) { + return interpolateBrowserResolution(response.id) + } + } + } + } + + if (extension && (!extname || !this.loader.extensions.has(extname))) { + let response = this.loader.load(pathname + extension, origin, options) + if (response.ok) { + return interpolateBrowserResolution(response.id) + } + + response = this.loader.load(`${pathname}/index${extension}`, origin, options) + if (response.ok) { + return interpolateBrowserResolution(response.id) + } + } + } + + const response = this.loader.load(pathname, origin, options) + + if (response.ok) { + return interpolateBrowserResolution(response.id) + } + + // try to load 'package.json' + const url = new URL(`${this.name}/${pathname}/${manifest}`, origin) + const childPackage = new Package(url, { + loader: this.loader, + origin + }) + + // if it loaded, then return the package entry + if (childPackage.load()) { + return childPackage.entry + } + + throw new ModuleNotFoundError( + `Cannot find module '${pathname}'`, + options?.children?.map?.((mod) => mod.id) + ) + + function interpolateBrowserResolution (id) { + if (!info || options?.browser === false) { + return id + } + + const url = new URL(id) + const prefix = new URL('.', origin || location.origin).href + const pathname = `./${url.href.replace(prefix, '')}` + + if (info.browser && typeof info.browser === 'object') { + for (const key in info.browser) { + const value = info.browser[key] + const filename = !key.startsWith('./') ? `./${key}` : key + if (filename === pathname) { + return new URL(value, prefix).href + } + } + } + + return id + } + } + + /** + * @ignore + */ + [Symbol.for('socket.runtime.util.inspect.custom')] () { + if (this.name && this.version) { + return `Package '(${this.name}@${this.version}') { }` + } else if (this.name) { + return `Package ('${this.name}') { }` + } else { + return 'Package { }' + } + } +} + +export default Package + +defineBuiltin('commonjs/package', { + DEFAULT_PACKAGE_MANIFEST_FILE_NAME, + DEFAULT_PACKAGE_VERSION, + DEFAULT_PACKAGE_PREFIX, + DEFAULT_PACKAGE_INDEX, + DEFAULT_LICENSE, + detectESMSource, + Dependencies, + Package, + Name +}) diff --git a/api/commonjs/require.js b/api/commonjs/require.js new file mode 100644 index 0000000000..bd86e33a10 --- /dev/null +++ b/api/commonjs/require.js @@ -0,0 +1,388 @@ +import { defineBuiltin, getBuiltin, isBuiltin } from './builtins.js' +import { DEFAULT_PACKAGE_PREFIX, Package } from './package.js' +import { ModuleNotFoundError } from '../errors.js' +import { isFunction } from '../util/types.js' +import location from '../location.js' +import URL from '../url.js' + +/** + * @typedef {function(string, import('./module.js').Module, function(string): any): any} RequireResolver + */ + +/** + * @typedef {{ + * module: import('./module.js').Module, + * prefix?: string, + * request?: import('./loader.js').RequestOptions, + * builtins?: object, + * resolvers?: RequireFunction[] + * }} CreateRequireOptions + */ + +/** + * @typedef {function(string): any} RequireFunction + */ + +/** + * @typedef {import('./package.js').PackageOptions} PackageOptions + */ + +/** + * @typedef {import('./package.js').PackageResolveOptions} PackageResolveOptions + */ + +/** + * @typedef { + * PackageResolveOptions & + * PackageOptions & + * { origins?: string[] | URL[] } + * } ResolveOptions + */ + +/** + * @typedef {ResolveOptions & { + * resolvers?: RequireResolver[], + * importmap?: import('./module.js').ImportMap, + * cache?: boolean + * }} RequireOptions + */ + +/** + * An array of global require paths, relative to the origin. + * @type {string[]} + */ +export const globalPaths = [ + new URL(DEFAULT_PACKAGE_PREFIX, location.origin).href +] + +/** + * An object attached to a `require()` function that contains metadata + * about the current module context. + */ +export class Meta { + #referrer = null + #url = null + + /** + * `Meta` class constructor. + * @param {import('./module.js').Module} module + */ + constructor (module) { + this.#referrer = (module.parent ?? module.main).id ?? location.origin + this.#url = module.id + } + + /** + * The referrer (parent) of this module. + * @type {string} + */ + get referrer () { + return this.#referrer + } + + /** + * The referrer (parent) of this module. + * @type {string} + */ + get url () { + return this.#url + } +} + +/** + * Factory for creating a `require()` function based on a module context. + * @param {CreateRequireOptions} options + * @return {RequireFunction} + */ +export function createRequire (options) { + const { builtins, headers, resolvers, module, prefix } = options + const { cache, loaders, main } = module + + // non-standard 'require.meta' object + const meta = new Meta(module) + + Object.assign(resolve, { + paths + }) + + const allResolvers = module.resolvers + .concat(resolvers) + .concat(main.resolvers) + .filter(isFunction) + + return Object.assign(require, { + extensions: loaders, + resolvers: allResolvers, + resolve, + loaders, + module, + cache, + meta, + main + }) + + /** + * Gets an ESM default export, if requested and applicable. + * @ignore + * @param {object} exports + */ + function getDefaultExports (exports) { + if (options?.default !== true) { + return exports + } + + if (exports && typeof exports === 'object') { + if ( + Object.keys(exports).length === 2 && + exports.__esModule === true && + 'default' in exports + ) { + return exports.default + } + } + + return exports + } + + /** + * @param {string} input + * @param {ResolveOptions & RequireOptions=} [options + * @ignore + */ + function applyResolvers (input, options = null) { + if (typeof input === 'string' && input.startsWith('npm:')) { + input = input.slice(4) + } + + const resolvers = Array + .from([]) + .concat(options?.resolvers) + .concat(allResolvers) + .filter(Boolean) + + return next(input) + + function next (specifier) { + if (resolvers.length === 0) return specifier + const resolver = resolvers.shift() + return resolver(specifier, module, next) + } + } + + /** + * Requires a module at for a given `input` which can be a relative file, + * named module, or an absolute URL. + * @param {string|URL} input + * @param {RequireOptions=} [options] + * @throws ModuleNotFoundError + * @throws ReferenceError + * @throws SyntaxError + * @throws TypeError + * @return {any} + */ + function require (input, options = null) { + if (input instanceof URL) { + input = input.href + } + + const resolvedInput = applyResolvers(input, options) + + if (resolvedInput && typeof resolvedInput !== 'string') { + return resolvedInput + } + + if (resolvedInput.includes('\n') || resolvedInput.length > 1024) { + return resolvedInput + } + + input = resolvedInput + + if (isBuiltin(input, { builtins: options?.builtins ?? builtins })) { + return getBuiltin(input, { builtins: options?.builtins ?? builtins }) + } + + const resolved = resolve(input, { + type: 'commonjs', + ...options + }) + + if (cache[resolved]) { + if (cache[resolved].error) { + throw cache[resolved].error + } + + return getDefaultExports(cache[resolved].exports) + } + + let child = null + + if (URL.canParse(input)) { + const url = new URL(input) + const origin = url.origin + child = module.createModule(resolved, { + loader: { headers, origin }, + ...options, + package: Package.load(resolved, { + loader: { headers, origin }, + prefix, + ...options + }) + }) + } else if (input.startsWith('.') || input.startsWith('/')) { + child = module.createModule(resolved, { + ...options, + loader: { headers, origin: module.package.loader.origin }, + package: module.package + }) + } else { + const origin = new URL('..', module.package.loader.origin) + child = module.createModule(resolved, { + loader: { headers, origin }, + ...options, + package: Package.load(input, { + loader: { headers, origin }, + prefix, + ...options + }) + }) + } + + if (options?.cache === false) { + delete cache[resolved] + } else { + cache[resolved] = child + } + + if (child.load(options)) { + return getDefaultExports(child.exports) + } + + throw new ModuleNotFoundError( + `Cannnot find module '${input}'`, + module.children.map((mod) => mod.id) + ) + } + + /** + * Resolve a module `input` to an absolute URL. + * @param {string|URL} pathname + * @param {ResolveOptions=} [options] + * @throws ModuleNotFoundError + * @return {string} + */ + function resolve (input, options = null) { + if (input instanceof URL) { + input = input.href + } + + const resolvedInput = applyResolvers(input, options) + + if (resolvedInput && typeof resolvedInput !== 'string') { + return input + } + + if (resolvedInput.includes('\n') || resolvedInput.length > 256) { + return input + } + + input = resolvedInput + + if (isBuiltin(input, { builtins: options?.builtins ?? builtins })) { + return input + } + + // A URL was given, try to resolve it as a package + if (URL.canParse(input)) { + return module.package.resolve(input, { + type: 'commonjs', + ...options + }) + } + + const origins = new Set([] + .concat(options?.origins) + .concat(resolve.paths(input)) + .filter(Boolean) + ) + + for (const origin of origins) { + // relative require + if (input.startsWith('.') || input.startsWith('/')) { + return module.resolve(input) + } else { // named module + const moduleName = Package.Name.from(input) + const pathname = moduleName.pathname.replace(moduleName.name, '.') + const pkg = new Package(moduleName.name, { + loader: { headers, origin } + }) + + try { + return pkg.resolve(pathname, { + type: 'commonjs', + ...options + }) + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err + } + } + } + } + + throw new ModuleNotFoundError( + `Cannnot find module '${input}'`, + module.children.map((mod) => mod.id) + ) + } + + /** + * Computes possible `require()` origin paths for an input module URL + * @param {string|URL} pathname + * @return {string[]?} + */ + function paths (input) { + if (isBuiltin(input, builtins)) { + return null + } + + if (URL.canParse(input)) { + return [new URL(input).origin] + } + + if (input.startsWith('.') || input.startsWith('/')) { + return [module.origin] + } + + const origins = new Set(globalPaths.map((path) => new URL(path, location.origin).href)) + let origin = module.origin + + while (true) { + const url = new URL(origin) + origins.add(origin) + + if (url.pathname === '/') { + break + } + + origin = new URL('..', origin).href + } + + const results = Array + .from(origins) + .map((origin) => origin.endsWith(prefix) + ? new URL(origin) + : new URL(prefix, origin) + ) + .map((url) => url.href) + + return Array.from(new Set(results)) + } +} + +export default createRequire + +defineBuiltin('commonjs/require', { + Meta, + globalPaths, + createRequire +}) diff --git a/api/console.js b/api/console.js index c29a0a5ceb..2a19e372fc 100644 --- a/api/console.js +++ b/api/console.js @@ -1,8 +1,9 @@ import { format, isObject } from './util.js' import { postMessage } from './ipc.js' +import os from './os.js' function isPatched (console) { - return console?.[Symbol.for('socket.console.patched')] === true + return console?.[Symbol.for('socket.runtime.console.patched')] === true } function table (data, columns, formatValues = true) { @@ -132,13 +133,73 @@ export class Console { configurable: false } }) + + this.write = this.write.bind(this) + this.assert = this.assert.bind(this) + this.clear = this.clear.bind(this) + this.count = this.count.bind(this) + this.countReset = this.countReset.bind(this) + this.debug = this.debug.bind(this) + this.dir = this.dir.bind(this) + this.dirxml = this.dirxml.bind(this) + this.error = this.error.bind(this) + this.info = this.info.bind(this) + this.log = this.log.bind(this) + this.table = this.table.bind(this) + this.time = this.time.bind(this) + this.timeEnd = this.timeEnd.bind(this) + this.timeLog = this.timeLog.bind(this) + this.trace = this.trace.bind(this) + this.warn = this.warn.bind(this) } - async write (destination, ...args) { - const value = encodeURIComponent(format(...args)) - const uri = `ipc://${destination}?value=${value}` + write (destination, ...args) { + let extra = '' + let value = '' + + value = format(...args) + if (destination === 'debug') { + destination = 'stderr' + extra = 'debug=true' + if (globalThis.location && !globalThis.window) { + value = `[${globalThis.name || globalThis.location.pathname}]: ${value}` + } else { + value = `[${globalThis.location.pathname}]: ${value}` + } + } + + if (/ios|darwin/i.test(os.platform())) { + const parts = value.split('\n') + const pending = [] + for (const part of parts) { + if (part.length > 256) { + for (let i = 0; i < part.length; i += 256) { + pending.push(part.slice(i, i + 256)) + } + } else { + pending.push(part) + } + + while (pending.length) { + const output = pending.shift() + try { + const value = encodeURIComponent(output) + const uri = `ipc://${destination}?value=${value}&${extra ? extra + '&' : ''}resolve=false` + this.postMessage?.(uri) + } catch (err) { + this.console?.warn?.(`Failed to write to ${destination}: ${err.message}`) + return + } + } + } + return + } + + value = encodeURIComponent(value) + const uri = `ipc://${destination}?value=${value}&${extra}&resolve=false` + try { - return await this.postMessage?.(uri) + return this.postMessage?.(uri) } catch (err) { this.console?.warn?.(`Failed to write to ${destination}: ${err.message}`) } @@ -175,7 +236,7 @@ export class Console { debug (...args) { this.console?.debug?.(...args) if (!isPatched(this.console)) { - this.write('stderr', ...args) + this.write('debug', ...args) } } @@ -351,7 +412,7 @@ export function patchGlobalConsole (globalConsole, options = {}) { ...options }) - globalConsole[Symbol.for('socket.console.patched')] = true + globalConsole[Symbol.for('socket.runtime.console.patched')] = true for (const key in globalConsole) { if (typeof Console.prototype[key] === 'function') { @@ -369,7 +430,10 @@ export function patchGlobalConsole (globalConsole, options = {}) { return globalConsole } -export default new Console({ +export default Object.assign(new Console({ postMessage, console: patchGlobalConsole(globalConsole) +}), { + Console, + globalConsole }) diff --git a/api/constants.js b/api/constants.js new file mode 100644 index 0000000000..73580977e9 --- /dev/null +++ b/api/constants.js @@ -0,0 +1,124 @@ +import fs from './fs/constants.js' +import window from './window/constants.js' +import os from './os/constants.js' +export * from './fs/constants.js' +export * from './window/constants.js' + +export const E2BIG = os.errno.E2BIG +export const EACCES = os.errno.EACCES +export const EADDRINUSE = os.errno.EADDRINUSE +export const EADDRNOTAVAIL = os.errno.EADDRNOTAVAIL +export const EAFNOSUPPORT = os.errno.EAFNOSUPPORT +export const EAGAIN = os.errno.EAGAIN +export const EALREADY = os.errno.EALREADY +export const EBADF = os.errno.EBADF +export const EBADMSG = os.errno.EBADMSG +export const EBUSY = os.errno.EBUSY +export const ECANCELED = os.errno.ECANCELED +export const ECHILD = os.errno.ECHILD +export const ECONNABORTED = os.errno.ECONNABORTED +export const ECONNREFUSED = os.errno.ECONNREFUSED +export const ECONNRESET = os.errno.ECONNRESET +export const EDEADLK = os.errno.EDEADLK +export const EDESTADDRREQ = os.errno.EDESTADDRREQ +export const EDOM = os.errno.EDOM +export const EDQUOT = os.errno.EDQUOT +export const EEXIST = os.errno.EEXIST +export const EFAULT = os.errno.EFAULT +export const EFBIG = os.errno.EFBIG +export const EHOSTUNREACH = os.errno.EHOSTUNREACH +export const EIDRM = os.errno.EIDRM +export const EILSEQ = os.errno.EILSEQ +export const EINPROGRESS = os.errno.EINPROGRESS +export const EINTR = os.errno.EINTR +export const EINVAL = os.errno.EINVAL +export const EIO = os.errno.EIO +export const EISCONN = os.errno.EISCONN +export const EISDIR = os.errno.EISDIR +export const ELOOP = os.errno.ELOOP +export const EMFILE = os.errno.EMFILE +export const EMLINK = os.errno.EMLINK +export const EMSGSIZE = os.errno.EMSGSIZE +export const EMULTIHOP = os.errno.EMULTIHOP +export const ENAMETOOLONG = os.errno.ENAMETOOLONG +export const ENETDOWN = os.errno.ENETDOWN +export const ENETRESET = os.errno.ENETRESET +export const ENETUNREACH = os.errno.ENETUNREACH +export const ENFILE = os.errno.ENFILE +export const ENOBUFS = os.errno.ENOBUFS +export const ENODATA = os.errno.ENODATA +export const ENODEV = os.errno.ENODEV +export const ENOENT = os.errno.ENOENT +export const ENOEXEC = os.errno.ENOEXEC +export const ENOLCK = os.errno.ENOLCK +export const ENOLINK = os.errno.ENOLINK +export const ENOMEM = os.errno.ENOMEM +export const ENOMSG = os.errno.ENOMSG +export const ENOPROTOOPT = os.errno.ENOPROTOOPT +export const ENOSPC = os.errno.ENOSPC +export const ENOSR = os.errno.ENOSR +export const ENOSTR = os.errno.ENOSTR +export const ENOSYS = os.errno.ENOSYS +export const ENOTCONN = os.errno.ENOTCONN +export const ENOTDIR = os.errno.ENOTDIR +export const ENOTEMPTY = os.errno.ENOTEMPTY +export const ENOTSOCK = os.errno.ENOTSOCK +export const ENOTSUP = os.errno.ENOTSUP +export const ENOTTY = os.errno.ENOTTY +export const ENXIO = os.errno.ENXIO +export const EOPNOTSUPP = os.errno.EOPNOTSUPP +export const EOVERFLOW = os.errno.EOVERFLOW +export const EPERM = os.errno.EPERM +export const EPIPE = os.errno.EPIPE +export const EPROTO = os.errno.EPROTO +export const EPROTONOSUPPORT = os.errno.EPROTONOSUPPORT +export const EPROTOTYPE = os.errno.EPROTOTYPE +export const ERANGE = os.errno.ERANGE +export const EROFS = os.errno.EROFS +export const ESPIPE = os.errno.ESPIPE +export const ESRCH = os.errno.ESRCH +export const ESTALE = os.errno.ESTALE +export const ETIME = os.errno.ETIME +export const ETIMEDOUT = os.errno.ETIMEDOUT +export const ETXTBSY = os.errno.ETXTBSY +export const EWOULDBLOCK = os.errno.EWOULDBLOCK +export const EXDEV = os.errno.EXDEV + +export const SIGHUP = os.signal.SIGHUP +export const SIGINT = os.signal.SIGINT +export const SIGQUIT = os.signal.SIGQUIT +export const SIGILL = os.signal.SIGILL +export const SIGTRAP = os.signal.SIGTRAP +export const SIGABRT = os.signal.SIGABRT +export const SIGIOT = os.signal.SIGIOT +export const SIGBUS = os.signal.SIGBUS +export const SIGFPE = os.signal.SIGFPE +export const SIGKILL = os.signal.SIGKILL +export const SIGUSR1 = os.signal.SIGUSR1 +export const SIGSEGV = os.signal.SIGSEGV +export const SIGUSR2 = os.signal.SIGUSR2 +export const SIGPIPE = os.signal.SIGPIPE +export const SIGALRM = os.signal.SIGALRM +export const SIGTERM = os.signal.SIGTERM +export const SIGCHLD = os.signal.SIGCHLD +export const SIGCONT = os.signal.SIGCONT +export const SIGSTOP = os.signal.SIGSTOP +export const SIGTSTP = os.signal.SIGTSTP +export const SIGTTIN = os.signal.SIGTTIN +export const SIGTTOU = os.signal.SIGTTOU +export const SIGURG = os.signal.SIGURG +export const SIGXCPU = os.signal.SIGXCPU +export const SIGXFSZ = os.signal.SIGXFSZ +export const SIGVTALRM = os.signal.SIGVTALRM +export const SIGPROF = os.signal.SIGPROF +export const SIGWINCH = os.signal.SIGWINCH +export const SIGIO = os.signal.SIGIO +export const SIGINFO = os.signal.SIGINFO +export const SIGSYS = os.signal.SIGSYS + +export default { + ...fs, + ...window, + ...os.errno, + ...os.signal +} diff --git a/api/crypto.js b/api/crypto.js index 9c68889f1a..bf3c452853 100644 --- a/api/crypto.js +++ b/api/crypto.js @@ -1,7 +1,7 @@ /* global console */ /* eslint-disable no-fallthrough */ /** - * @module Crypto + * @module crypto * * Some high-level methods around the `crypto.subtle` API for getting * random bytes and hashing. diff --git a/api/dgram.js b/api/dgram.js index 8ecdb0f4fb..965fe05460 100644 --- a/api/dgram.js +++ b/api/dgram.js @@ -1,5 +1,5 @@ /** - * @module Dgram + * @module dgram * * This module provides an implementation of UDP datagram sockets. It does * not (yet) provide any of the multicast methods or properties. @@ -13,12 +13,13 @@ import { isArrayBufferView, isFunction, noop } from './util.js' import { murmur3, rand64 } from './crypto.js' import { InternalError } from './errors.js' +import { AsyncResource } from './async/resource.js' +import { Conduit } from './internal/conduit.js' import { EventEmitter } from './events.js' import diagnostics from './diagnostics.js' import { Buffer } from './buffer.js' import { isIPv4 } from './ip.js' import process from './process.js' -import console from './console.js' import ipc from './ipc.js' import dns from './dns.js' import gc from './gc.js' @@ -48,13 +49,17 @@ const dc = diagnostics.channels.group('udp', [ 'bind' ]) -function defaultCallback (socket) { +function defaultCallback (socket, resource) { return (err) => { - if (err) socket.emit('error', err) + resource.runInAsyncScope(() => { + if (err) { + socket.emit('error', err) + } + }) } } -function createDataListener (socket) { +function createDataListener (socket, resource) { // subscribe this socket to the firehose globalThis.addEventListener('data', ondata) return ondata @@ -64,19 +69,31 @@ function createDataListener (socket) { const buffer = detail.data if (err && err.id === socket.id) { - return socket.emit('error', err) + return resource.runInAsyncScope(() => { + return socket.emit('error', err) + }) } if (!data || BigInt(data.id) !== socket.id) return if (source === 'udp.readStart') { + if (buffer && buffer instanceof ArrayBuffer) { + // @ts-ignore + if (buffer.detached) { + return + } + } + const message = Buffer.from(buffer) const info = { ...data, family: getAddressFamily(data.address) } - socket.emit('message', message, info) + resource.runInAsyncScope(() => { + socket.emit('message', message, info) + }) + dc.channel('message').publish({ socket, buffer: message, info }) } @@ -145,9 +162,19 @@ async function startReading (socket, callback) { } try { - result = await ipc.send('udp.readStart', { - id: socket.id - }) + if (socket?.conduit?.isActive) { + const opts = { + route: 'udp.readStart' + } + if (!socket.conduit.send(opts, Buffer.from(''))) { + console.warn('socket:dgram: Failed to send conduit payload') + } + result = { data: true } + } else { + result = await ipc.send('udp.readStart', { + id: socket.id + }) + } callback(result.err, result.data) } catch (err) { @@ -501,13 +528,25 @@ async function send (socket, options, callback) { address: options.address }) - result = await ipc.write('udp.send', { - id: socket.id, - port: options.port, - address: options.address - }, options.buffer) + if (socket?.conduit?.isActive) { + const opts = { + route: 'udp.send', + port: options.port, + address: options.address + } + socket.conduit.send(opts, options.buffer) + result = { data: true } + } else { + result = await ipc.write('udp.send', { + id: socket.id, + port: options.port, + address: options.address + }, options.buffer) + } - callback(result.err, result.data) + if (result.data?.detached !== true) { + callback(result.err, result.data) + } } catch (err) { callback(err) return { err } @@ -627,6 +666,8 @@ export const createSocket = (options, callback) => new Socket(options, callback) * The new keyword is not to be used to create dgram.Socket instances. */ export class Socket extends EventEmitter { + #resource = null + constructor (options, callback) { super() @@ -643,6 +684,9 @@ export class Socket extends EventEmitter { throw new ERR_SOCKET_BAD_TYPE() } + this.#resource = new AsyncResource('Socket') + this.#resource.handle = this + this.type = options.type this.signal = options?.signal ?? null @@ -678,13 +722,16 @@ export class Socket extends EventEmitter { */ [gc.finalizer] (options) { return { - args: [this.id, options], - async handle (id) { + args: [this.id, this.conduit, options], + async handle (id, conduit) { if (process.env.DEBUG) { console.warn('Closing Socket on garbage collection') } await ipc.request('udp.close', { id }, options) + if (conduit) { + conduit.close() + } } } } @@ -708,7 +755,7 @@ export class Socket extends EventEmitter { ? arg2 : isFunction(arg3) ? arg3 - : defaultCallback(this) + : defaultCallback(this, this.#resource) if (typeof arg1 === 'number' || typeof arg2 === 'string') { options.port = parseInt(arg1) @@ -734,28 +781,73 @@ export class Socket extends EventEmitter { bind(this, options, (err, info) => { if (err) { - if ( - this.knownIdWasGivenInSocketConstruction && - err.code === 'ERR_SOCKET_ALREADY_BOUND' - ) { - this.dataListener = createDataListener(this) - cb(null) - this.emit('listening') + return this.#resource.runInAsyncScope(() => { + if ( + this.knownIdWasGivenInSocketConstruction && + err.code === 'ERR_SOCKET_ALREADY_BOUND' + ) { + this.dataListener = createDataListener(this, this.#resource) + cb(null) + this.emit('listening') + } else { + cb(err) + } + }) + } + + if (!this.legacy && !this.conduit) { + this.conduit = new Conduit({ id: this.id }) + + this.conduit.receive((_, decoded) => { + if (!decoded || !decoded.options) return + + const rinfo = { + port: Number(decoded.options.port), + address: decoded.options.address, + family: getAddressFamily(decoded.options.address) + } + + const message = Buffer.from(decoded.payload) + + this.#resource.runInAsyncScope(() => { + this.emit('message', message, rinfo) + }) + + dc.channel('message').publish({ socket: this, buffer: message, info }) + }) + + const onopen = () => { + startReading(this, (err) => { + this.#resource.runInAsyncScope(() => { + if (err) { + cb(err) + } else { + cb(null) + this.emit('listening') + } + }) + }) + } + + if (!this.conduit.isActive) { + this.conduit.addEventListener('open', onopen, { once: true }) } else { - cb(err) + onopen() } return } startReading(this, (err) => { - if (err) { - cb(err) - } else { - this.dataListener = createDataListener(this) - cb(null) - this.emit('listening') - } + this.#resource.runInAsyncScope(() => { + if (err) { + cb(err) + } else { + this.dataListener = createDataListener(this, this.#resource) + cb(null) + this.emit('listening') + } + }) }) }) @@ -785,7 +877,7 @@ export class Socket extends EventEmitter { ? arg2 : isFunction(arg3) ? arg3 - : defaultCallback(this) + : defaultCallback(this, this.#resource) if (!Number.isInteger(port) || port <= 0 || port > MAX_PORT) { throw new ERR_SOCKET_BAD_PORT( @@ -798,11 +890,13 @@ export class Socket extends EventEmitter { } connect(this, { address, port }, (err, info) => { - cb(err, info) + this.#resource.runInAsyncScope(() => { + cb(err, info) - if (!err && info) { - this.emit('connect', info) - } + if (!err && info) { + this.emit('connect', info) + } + }) }) } @@ -878,7 +972,7 @@ export class Socket extends EventEmitter { let length let port let address - let cb = defaultCallback(this) + let cb = defaultCallback(this, this.#resource) if (Array.isArray(buffer)) { buffer = fromBufferList(buffer) @@ -941,7 +1035,25 @@ export class Socket extends EventEmitter { buffer = buffer.slice(0, length) - return send(this, { id, port, address, buffer }, cb) + if (buffer?.buffer?.detached) { + // XXX(@jwerle,@heapwolf): this is likely during a paused application state + // how should handle this? maybe a warning + return + } + + return send(this, { id, port, address, buffer }, (...args) => { + if (buffer.buffer?.detached) { + // XXX(@jwerle,@heapwolf): see above + return + } + + if (typeof cb === 'function') { + this.#resource.runInAsyncScope(() => { + // eslint-disable-next-line + cb(...args) + }) + } + }) } /** @@ -975,7 +1087,9 @@ export class Socket extends EventEmitter { // gc might have already closed this if (!gc.finalizers.has(this)) { if (isFunction(cb)) { - cb() + this.#resource.runInAsyncScope(() => { + cb() + }) return } } @@ -986,20 +1100,28 @@ export class Socket extends EventEmitter { }) } - if (isFunction(cb)) { - cb(err) - } else { - this.emit('error', err) - } + this.#resource.runInAsyncScope(() => { + if (isFunction(cb)) { + cb(err) + } else { + this.emit('error', err) + } + }) return } - if (isFunction(cb)) { - cb(null) + if (this.conduit) { + this.conduit.close() } - this.emit('close') + this.#resource.runInAsyncScope(() => { + if (isFunction(cb)) { + cb(null) + } + + this.emit('close') + }) }) return this diff --git a/api/diagnostics/channels.js b/api/diagnostics/channels.js index cbbc2ada4a..24cfd049bc 100644 --- a/api/diagnostics/channels.js +++ b/api/diagnostics/channels.js @@ -1,5 +1,6 @@ import { toString, IllegalConstructor } from '../util.js' import process from '../process.js' +import gc from '../gc.js' /** * Used to preallocate a minimum sized array of subscribers for @@ -45,6 +46,7 @@ export class Channel { constructor (name) { this.name = name this.group = null + gc.ref(this) } /** @@ -163,11 +165,11 @@ export class Channel { /** * A no-op for `Channel` instances. This function always returns `false`. - * @param {string} name - * @param {object} message + * @param {string|object} name + * @param {object=} [message] * @return Promise */ - async publish (name, message) { + async publish (name, message = undefined) { return false } @@ -433,11 +435,11 @@ export class ChannelGroup extends Channel { /** * Publish a message to named subscribers in this group where `targets` is an * object mapping channel names to messages. - * @param {string} name - * @param {object} message + * @param {string|object} name + * @param {object=} [message] * @return Promise */ - async publish (name, message) { + async publish (name, message = undefined) { const pending = [] const targets = name && message ? { [name]: message } : name const entries = Object.entries(targets).map((e) => normalizeEntry(this, e)) diff --git a/api/diagnostics/index.js b/api/diagnostics/index.js index a4c3f16af0..599919167c 100644 --- a/api/diagnostics/index.js +++ b/api/diagnostics/index.js @@ -1,10 +1,11 @@ import channels from './channels.js' import window from './window.js' +import runtime from './runtime.js' import * as exports from './index.js' export default exports -export { channels, window } +export { channels, window, runtime } /** * @param {string} name diff --git a/api/diagnostics/runtime.js b/api/diagnostics/runtime.js new file mode 100644 index 0000000000..282afe2bfa --- /dev/null +++ b/api/diagnostics/runtime.js @@ -0,0 +1,244 @@ +import ipc from '../ipc.js' + +/** + * A base container class for diagnostic information. + */ +export class Diagnostic { + /** + * A container for handles related to the diagnostics + */ + static Handles = class Handles { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count = 0 + + /** + * A set of known handle IDs + * @type {string[]} + */ + ids = [] + + /** + * `Diagnostic.Handles` class constructor. + * @private + */ + constructor () { + Object.seal(this) + } + } + + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles = new Diagnostic.Handles() +} + +/** + * A container for libuv diagnostics + */ +export class UVDiagnostic extends Diagnostic { + /** + * A container for libuv metrics. + */ + static Metrics = class Metrics { + /** + * The number of event loop iterations. + * @type {number} + */ + loopCount = 0 + + /** + * Number of events that have been processed by the event handler. + * @type {number} + */ + events = 0 + + /** + * Number of events that were waiting to be processed when the + * event provider was called. + * @type {number} + */ + eventsWaiting = 0 + } + + /** + * Known libuv metrics for this diagnostic. + * @type {UVDiagnostic.Metrics} + */ + metrics = new UVDiagnostic.Metrics() + + /** + * The current idle time of the libuv loop + * @type {number} + */ + idleTime = 0 + + /** + * The number of active requests in the libuv loop + * @type {number} + */ + activeRequests = 0 +} + +/** + * A container for Core Post diagnostics. + */ +export class PostsDiagnostic extends Diagnostic {} + +/** + * A container for child process diagnostics. + */ +export class ChildProcessDiagnostic extends Diagnostic {} + +/** + * A container for AI diagnostics. + */ +export class AIDiagnostic extends Diagnostic { + /** + * A container for AI LLM diagnostics. + */ + static LLMDiagnostic = class LLMDiagnostic extends Diagnostic {} + + /** + * Known AI LLM diagnostics. + * @type {AIDiagnostic.LLMDiagnostic} + */ + llm = new AIDiagnostic.LLMDiagnostic() +} + +/** + * A container for various filesystem diagnostics. + */ +export class FSDiagnostic extends Diagnostic { + /** + * A container for filesystem watcher diagnostics. + */ + static WatchersDiagnostic = class WatchersDiagnostic extends Diagnostic {} + + /** + * A container for filesystem descriptors diagnostics. + */ + static DescriptorsDiagnostic = class DescriptorsDiagnostic extends Diagnostic {} + + /** + * Known FS watcher diagnostics. + * @type {FSDiagnostic.WatchersDiagnostic} + */ + watchers = new FSDiagnostic.WatchersDiagnostic() + + /** + * @type {FSDiagnostic.DescriptorsDiagnostic} + */ + descriptors = new FSDiagnostic.DescriptorsDiagnostic() +} + +/** + * A container for various timers diagnostics. + */ +export class TimersDiagnostic extends Diagnostic { + /** + * A container for core timeout timer diagnostics. + */ + static TimeoutDiagnostic = class TimeoutDiagnostic extends Diagnostic {} + + /** + * A container for core interval timer diagnostics. + */ + static IntervalDiagnostic = class IntervalDiagnostic extends Diagnostic {} + + /** + * A container for core immediate timer diagnostics. + */ + static ImmediateDiagnostic = class ImmediateDiagnostic extends Diagnostic {} + + /** + * @type {TimersDiagnostic.TimeoutDiagnostic} + */ + timeout = new TimersDiagnostic.TimeoutDiagnostic() + + /** + * @type {TimersDiagnostic.IntervalDiagnostic} + */ + interval = new TimersDiagnostic.IntervalDiagnostic() + + /** + * @type {TimersDiagnostic.ImmediateDiagnostic} + */ + immediate = new TimersDiagnostic.ImmediateDiagnostic() +} + +/** + * A container for UDP diagnostics. + */ +export class UDPDiagnostic extends Diagnostic {} + +/** + * A container for various queried runtime diagnostics. + */ +export class QueryDiagnostic { + posts = new PostsDiagnostic() + childProcess = new ChildProcessDiagnostic() + ai = new AIDiagnostic() + fs = new FSDiagnostic() + timers = new TimersDiagnostic() + udp = new UDPDiagnostic() + uv = new UVDiagnostic() +} + +/** + * Queries runtime diagnostics. + * @return {Promise} + */ +export async function query (type) { + const result = await ipc.request('diagnostics.query') + + if (result.err) { + throw result.err + } + + const query = Object.assign(new QueryDiagnostic(), result.data) + + if (typeof globalThis.__global_ipc_extension_handler === 'function') { + const result = await ipc.request('diagnostics.query', {}, { + useExtensionIPCIfAvailable: true + }) + + if (result.data) { + extend(query, Object.assign(new QueryDiagnostic(), result.data)) + } + + function extend (left, right) { + for (const key in right) { + if (Array.isArray(left[key]) && Array.isArray(right[key])) { + left[key].push(...right[key]) + } else if (left[key] && typeof left[key] === 'object') { + if (right[key] && typeof right[key] === 'object') { + extend(left[key], right[key]) + } + } else if (typeof left[key] === 'number' && typeof right[key] === 'number') { + left[key] += right[key] + } else { + left[key] = right[key] + } + } + } + } + + if (typeof type === 'string') { + return type + .trim() + .split(/\.|\[|\]/g) + .map((key) => key.trim()) + .filter((key) => key.length > 0) + .reduce((q, k) => q ? q[k] : null, query) + } + + return query +} + +export default { + query +} diff --git a/api/diagnostics/window.js b/api/diagnostics/window.js index 803b2e75f5..556fd25ea2 100644 --- a/api/diagnostics/window.js +++ b/api/diagnostics/window.js @@ -182,7 +182,8 @@ export class XMLHttpRequestMetric extends Metric { export class WorkerMetric extends Metric { constructor (options) { super() - this.GlobalWorker = globalThis.Worker + // TODO(@heapwolf): this fix for node causes the ts-defs to lose a lot of type info + this.GlobalWorker = this.isSocketRuntime ? globalThis.Worker : class {} this.channel = dc.channel('Worker') this.Worker = class Worker extends this.GlobalWorker { constructor (url, options, ...args) { diff --git a/api/dns/index.js b/api/dns/index.js index 927d334909..03a307a47b 100644 --- a/api/dns/index.js +++ b/api/dns/index.js @@ -1,5 +1,5 @@ /** - * @module DNS + * @module dns * * This module enables name resolution. For example, use it to look up IP * addresses of host names. Although named for the Domain Name System (DNS), @@ -111,5 +111,6 @@ for (const key in exports) { const value = exports[key] if (key in promises && isFunction(value) && isFunction(promises[key])) { value[Symbol.for('nodejs.util.promisify.custom')] = promises[key] + value[Symbol.for('socket.runtime.util.promisify.custom')] = promises[key] } } diff --git a/api/dns/promises.js b/api/dns/promises.js index 35db86bfbe..770c599049 100644 --- a/api/dns/promises.js +++ b/api/dns/promises.js @@ -1,5 +1,5 @@ /** - * @module DNS.promises + * @module dns.promises * * This module enables name resolution. For example, use it to look up IP * addresses of host names. Although named for the Domain Name System (DNS), diff --git a/api/enumeration.js b/api/enumeration.js index f236e94133..a98bf9d439 100644 --- a/api/enumeration.js +++ b/api/enumeration.js @@ -117,7 +117,7 @@ export class Enumeration extends Set { /** * @ignore */ - [Symbol.for('socket.util.inspect.custom')] () { + [Symbol.for('socket.runtime.util.inspect.custom')] () { return this.inspect() } diff --git a/api/errno.js b/api/errno.js new file mode 100644 index 0000000000..27cae69588 --- /dev/null +++ b/api/errno.js @@ -0,0 +1,237 @@ +import { errno as constants } from './os/constants.js' + +/** + * @typedef {import('./os/constants.js').errno} errno + */ + +export const E2BIG = constants.E2BIG +export const EACCES = constants.EACCES +export const EADDRINUSE = constants.EADDRINUSE +export const EADDRNOTAVAIL = constants.EADDRNOTAVAIL +export const EAFNOSUPPORT = constants.EAFNOSUPPORT +export const EAGAIN = constants.EAGAIN +export const EALREADY = constants.EALREADY +export const EBADF = constants.EBADF +export const EBADMSG = constants.EBADMSG +export const EBUSY = constants.EBUSY +export const ECANCELED = constants.ECANCELED +export const ECHILD = constants.ECHILD +export const ECONNABORTED = constants.ECONNABORTED +export const ECONNREFUSED = constants.ECONNREFUSED +export const ECONNRESET = constants.ECONNRESET +export const EDEADLK = constants.EDEADLK +export const EDESTADDRREQ = constants.EDESTADDRREQ +export const EDOM = constants.EDOM +export const EDQUOT = constants.EDQUOT +export const EEXIST = constants.EEXIST +export const EFAULT = constants.EFAULT +export const EFBIG = constants.EFBIG +export const EHOSTUNREACH = constants.EHOSTUNREACH +export const EIDRM = constants.EIDRM +export const EILSEQ = constants.EILSEQ +export const EINPROGRESS = constants.EINPROGRESS +export const EINTR = constants.EINTR +export const EINVAL = constants.EINVAL +export const EIO = constants.EIO +export const EISCONN = constants.EISCONN +export const EISDIR = constants.EISDIR +export const ELOOP = constants.ELOOP +export const EMFILE = constants.EMFILE +export const EMLINK = constants.EMLINK +export const EMSGSIZE = constants.EMSGSIZE +export const EMULTIHOP = constants.EMULTIHOP +export const ENAMETOOLONG = constants.ENAMETOOLONG +export const ENETDOWN = constants.ENETDOWN +export const ENETRESET = constants.ENETRESET +export const ENETUNREACH = constants.ENETUNREACH +export const ENFILE = constants.ENFILE +export const ENOBUFS = constants.ENOBUFS +export const ENODATA = constants.ENODATA +export const ENODEV = constants.ENODEV +export const ENOENT = constants.ENOENT +export const ENOEXEC = constants.ENOEXEC +export const ENOLCK = constants.ENOLCK +export const ENOLINK = constants.ENOLINK +export const ENOMEM = constants.ENOMEM +export const ENOMSG = constants.ENOMSG +export const ENOPROTOOPT = constants.ENOPROTOOPT +export const ENOSPC = constants.ENOSPC +export const ENOSR = constants.ENOSR +export const ENOSTR = constants.ENOSTR +export const ENOSYS = constants.ENOSYS +export const ENOTCONN = constants.ENOTCONN +export const ENOTDIR = constants.ENOTDIR +export const ENOTEMPTY = constants.ENOTEMPTY +export const ENOTSOCK = constants.ENOTSOCK +export const ENOTSUP = constants.ENOTSUP +export const ENOTTY = constants.ENOTTY +export const ENXIO = constants.ENXIO +export const EOPNOTSUPP = constants.EOPNOTSUPP +export const EOVERFLOW = constants.EOVERFLOW +export const EPERM = constants.EPERM +export const EPIPE = constants.EPIPE +export const EPROTO = constants.EPROTO +export const EPROTONOSUPPORT = constants.EPROTONOSUPPORT +export const EPROTOTYPE = constants.EPROTOTYPE +export const ERANGE = constants.ERANGE +export const EROFS = constants.EROFS +export const ESPIPE = constants.ESPIPE +export const ESRCH = constants.ESRCH +export const ESTALE = constants.ESTALE +export const ETIME = constants.ETIME +export const ETIMEDOUT = constants.ETIMEDOUT +export const ETXTBSY = constants.ETXTBSY +export const EWOULDBLOCK = constants.EWOULDBLOCK +export const EXDEV = constants.EXDEV + +export const strings = Object.assign(Object.create(null), { + [E2BIG]: 'Arg list too long', + [EACCES]: 'Permission denied', + [EADDRINUSE]: 'Address already in use', + [EADDRNOTAVAIL]: 'Cannot assign requested address', + [EAFNOSUPPORT]: 'Address family not supported by protocol family', + [EAGAIN]: 'Resource temporarily unavailabl', + [EALREADY]: 'Operation already in progress', + [EBADF]: 'Bad file descriptor', + [EBADMSG]: 'Bad message', + [EBUSY]: 'Resource busy', + [ECANCELED]: 'Operation canceled', + [ECHILD]: 'No child processes', + [ECONNABORTED]: 'Software caused connection abort', + [ECONNREFUSED]: 'Connection refused', + [ECONNRESET]: 'Connection reset by peer', + [EDEADLK]: 'Resource deadlock avoided', + [EDESTADDRREQ]: 'Destination address required', + [EDOM]: 'Numerical argument out of domain', + [EDQUOT]: 'Disc quota exceeded', + [EEXIST]: 'File exists', + [EFAULT]: 'Bad address', + [EFBIG]: 'File too large', + [EHOSTUNREACH]: 'No route to host', + [EIDRM]: 'Identifier removed', + [EILSEQ]: 'Illegal byte sequence', + [EINPROGRESS]: 'Operation now in progress', + [EINTR]: 'Interrupted function call', + [EINVAL]: 'Invalid argument', + [EIO]: 'Input/output error', + [EISCONN]: 'Socket is already connected', + [EISDIR]: 'Is a directory', + [ELOOP]: 'Too many levels of symbolic links', + [EMFILE]: 'Too many open files', + [EMLINK]: 'Too many links', + [EMSGSIZE]: 'Message too long', + [EMULTIHOP]: '', + [ENAMETOOLONG]: 'File name too long', + [ENETDOWN]: 'Network is down', + [ENETRESET]: 'Network dropped connection on reset', + [ENETUNREACH]: 'Network is unreachable', + [ENFILE]: 'Too many open files in system', + [ENOBUFS]: 'No buffer space available', + [ENODATA]: 'No message available', + [ENODEV]: 'Operation not supported by device', + [ENOENT]: 'No such file or directory', + [ENOEXEC]: 'Exec format error', + [ENOLCK]: 'No locks available', + [ENOLINK]: '', + [ENOMEM]: 'Cannot allocate memory', + [ENOMSG]: 'No message of desired type', + [ENOPROTOOPT]: 'Protocol not available', + [ENOSPC]: 'Device out of space', + [ENOSR]: 'No STREAM resources', + [ENOSTR]: 'Not a STREAM', + [ENOSYS]: 'Function not implemented', + [ENOTCONN]: 'Socket is not connected', + [ENOTDIR]: 'Not a directory', + [ENOTEMPTY]: 'Directory not empty', + [ENOTSOCK]: 'Socket operation on non-socket', + [ENOTSUP]: 'Not supported', + [ENOTTY]: 'Inappropriate ioctl for device', + [ENXIO]: 'No such device or address', + [EOPNOTSUPP]: 'Operation not supported on socket', + [EOVERFLOW]: 'Value too large to be stored in data type', + [EPERM]: 'Operation not permitted', + [EPIPE]: 'Broken pipe', + [EPROTO]: 'Protocol error', + [EPROTONOSUPPORT]: 'Protocol not supported', + [EPROTOTYPE]: 'Protocol wrong type for socket', + [ERANGE]: 'Numerical result out of range', + [EROFS]: 'Read-only file system', + [ESPIPE]: 'Illegal seek', + [ESRCH]: 'No such process', + [ESTALE]: 'Stale NFS file handle', + [ETIME]: 'STREAM ioctl() timeout', + [ETIMEDOUT]: 'Operation timed out', + [ETXTBSY]: 'Text file busy', + [EWOULDBLOCK]: 'Operation would block', + [EXDEV]: 'Improper link' +}) + +/** + * Converts an `errno` code to its corresponding string message. + * @param {import('./os/constants.js').errno} {code} + * @return {string} + */ +export function toString (code) { + code = Math.abs(code) + return strings[code] ?? '' +} + +/** + * Gets the code for a given 'errno' name. + * @param {string|number} name + * @return {errno} + */ +export function getCode (name) { + if (typeof name !== 'string') { + name = name.toString() + } + + name = name.toUpperCase() + for (const key in constants) { + if (name === key) { + return constants[key] + } + } + + return 0 +} + +/** + * Gets the name for a given 'errno' code + * @return {string} + * @param {string|number} code + */ +export function getName (code) { + code = getCode(code) + for (const key in constants) { + const value = constants[key] + if (value === code) { + return key + } + } + + return '' +} + +/** + * Gets the message for a 'errno' code. + * @param {number|string} code + * @return {string} + */ +export function getMessage (code) { + if (typeof code === 'string') { + code = getCode(code) + } + + code = Math.abs(code) + return toString(code) +} + +export { constants } +export default { + constants, + strings, + toString, + getCode, + getMessage +} diff --git a/api/errors.js b/api/errors.js index c5fad948e9..9836b4bfe5 100644 --- a/api/errors.js +++ b/api/errors.js @@ -120,6 +120,41 @@ export class EncodingError extends Error { } } +/** + * An error type derived from an `errno` code. + */ +export class ErrnoError extends Error { + static get code () { return '' } + // lazily set during init phase + static errno = null + + #name = '' + #code = 0 + + /** + * `ErrnoError` class constructor. + * @param {import('./errno').errno|string} code + */ + constructor (code, message = null, ...args) { + super(message || ErrnoError.errno.getMessage(code) || '', ...args) + + this.#code = ErrnoError.errno.getCode(code) + this.#name = ErrnoError.errno.getName(code) || 'SystemError' + + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, EncodingError) + } + } + + get name () { + return this.#name + } + + get code () { + return this.#code + } +} + /** * An `FinalizationRegistryCallbackError` is an error type thrown when an internal exception * has occurred, such as in the native IPC layer. @@ -439,6 +474,7 @@ export class ModuleNotFoundError extends NotFoundError { /** * `ModuleNotFoundError` class constructor. * @param {string} message + * @param {string[]=} [requireStack] */ constructor (message, requireStack) { super(message) diff --git a/api/events.js b/api/events.js index 3eb10f609e..b107cfa077 100644 --- a/api/events.js +++ b/api/events.js @@ -1,3 +1,5 @@ +import { AsyncContext } from './async/context.js' + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -19,9 +21,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -import console from './console.js' -import * as exports from './events.js' - const R = typeof Reflect === 'object' ? Reflect : null const ReflectApply = R && typeof R.apply === 'function' ? R.apply @@ -57,6 +56,7 @@ function EventEmitter () { EventEmitter.EventEmitter = EventEmitter EventEmitter.prototype._events = undefined +EventEmitter.prototype._contexts = undefined EventEmitter.prototype._eventsCount = 0 EventEmitter.prototype._maxListeners = undefined @@ -90,6 +90,13 @@ EventEmitter.init = function () { this._eventsCount = 0 } + if ( + this._contexts === undefined || + this._contexts === Object.getPrototypeOf(this)._contexts + ) { + this._contexts = new Map() + } + this._maxListeners = this._maxListeners || undefined } @@ -140,11 +147,27 @@ EventEmitter.prototype.emit = function emit (type) { if (handler === undefined) { return false } if (typeof handler === 'function') { - ReflectApply(handler, this, args) + const context = this._contexts.get(handler) + if (context) { + context.run(() => { + ReflectApply(handler, this, args) + }) + } else { + ReflectApply(handler, this, args) + } } else { const len = handler.length const listeners = arrayClone(handler, len) - for (let i = 0; i < len; ++i) { ReflectApply(listeners[i], this, args) } + for (let i = 0; i < len; ++i) { + const context = this._contexts.get(listeners[i]) + if (context) { + context.run(() => { + ReflectApply(listeners[i], this, args) + }) + } else { + ReflectApply(listeners[i], this, args) + } + } } return true @@ -209,6 +232,8 @@ function _addListener (target, type, listener, prepend) { } } + target._contexts.set(listener, new AsyncContext.Snapshot()) + return target } @@ -301,6 +326,8 @@ EventEmitter.prototype.removeListener = if (events.removeListener !== undefined) { this.emit('removeListener', type, originalListener || listener) } } + this._contexts.delete(listener) + return this } @@ -337,6 +364,7 @@ EventEmitter.prototype.removeAllListeners = } this.removeAllListeners('removeListener') this._events = Object.create(null) + this._contexts.clear() this._eventsCount = 0 return this } @@ -552,5 +580,4 @@ export { EventEmitter, once } - -export default exports +export default EventEmitter diff --git a/api/extension.js b/api/extension.js index 893b7ce837..eab6ef87c5 100644 --- a/api/extension.js +++ b/api/extension.js @@ -18,7 +18,7 @@ import ipc from './ipc.js' import fs from './fs/promises.js' /** - * @typedef {number} {Pointer} + * @typedef {number} Pointer */ const $loaded = Symbol('loaded') diff --git a/api/fetch/fetch.js b/api/fetch/fetch.js index 37353080ca..f1832e69d1 100644 --- a/api/fetch/fetch.js +++ b/api/fetch/fetch.js @@ -1,527 +1,641 @@ // node_modules/whatwg-fetch/fetch.js -var g = typeof globalThis !== "undefined" && globalThis || typeof self !== "undefined" && self || // eslint-disable-next-line no-undef -typeof global !== "undefined" && global || {}; +/* eslint-disable no-prototype-builtins */ +var g = + (typeof globalThis !== 'undefined' && globalThis) || + (typeof self !== 'undefined' && self) || + // eslint-disable-next-line no-undef + (typeof global !== 'undefined' && global) || + {} + var support = { - searchParams: "URLSearchParams" in g, - iterable: "Symbol" in g && "iterator" in Symbol, - blob: "FileReader" in g && "Blob" in g && function() { - try { - new Blob(); - return true; - } catch (e) { - return false; - } - }(), - formData: "FormData" in g, - arrayBuffer: "ArrayBuffer" in g -}; + searchParams: 'URLSearchParams' in g, + iterable: 'Symbol' in g && 'iterator' in Symbol, + blob: + 'FileReader' in g && + 'Blob' in g && + (function() { + try { + new Blob() + return true + } catch (e) { + return false + } + })(), + formData: 'FormData' in g, + arrayBuffer: 'ArrayBuffer' in g +} + function isDataView(obj) { - return obj && DataView.prototype.isPrototypeOf(obj); + return obj && DataView.prototype.isPrototypeOf(obj) } + if (support.arrayBuffer) { - viewClasses = [ - "[object Int8Array]", - "[object Uint8Array]", - "[object Uint8ClampedArray]", - "[object Int16Array]", - "[object Uint16Array]", - "[object Int32Array]", - "[object Uint32Array]", - "[object Float32Array]", - "[object Float64Array]" - ]; - isArrayBufferView = ArrayBuffer.isView || function(obj) { - return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1; - }; -} -var viewClasses; -var isArrayBufferView; + var viewClasses = [ + '[object Int8Array]', + '[object Uint8Array]', + '[object Uint8ClampedArray]', + '[object Int16Array]', + '[object Uint16Array]', + '[object Int32Array]', + '[object Uint32Array]', + '[object Float32Array]', + '[object Float64Array]' + ] + + var isArrayBufferView = + ArrayBuffer.isView || + function(obj) { + return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 + } +} + function normalizeName(name) { - if (typeof name !== "string") { - name = String(name); + if (typeof name !== 'string') { + name = String(name) } - if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === "") { - throw new TypeError('Invalid character in header field name: "' + name + '"'); + if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { + throw new TypeError('Invalid character in header field name: "' + name + '"') } - return name.toLowerCase(); + return name.toLowerCase() } + function normalizeValue(value) { - if (typeof value !== "string") { - value = String(value); + if (typeof value !== 'string') { + value = String(value) } - return value; + return value } + +// Build a destructive iterator for the value list function iteratorFor(items) { var iterator = { next: function() { - var value = items.shift(); - return { done: value === void 0, value }; + var value = items.shift() + return {done: value === undefined, value: value} } - }; + } + if (support.iterable) { iterator[Symbol.iterator] = function() { - return iterator; - }; + return iterator + } } - return iterator; + + return iterator } -function Headers(headers) { - this.map = {}; + +export function Headers(headers) { + this.map = {} + if (headers instanceof Headers) { headers.forEach(function(value, name) { - this.append(name, value); - }, this); + this.append(name, value) + }, this) } else if (Array.isArray(headers)) { headers.forEach(function(header) { if (header.length != 2) { - throw new TypeError("Headers constructor: expected name/value pair to be length 2, found" + header.length); + throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length) } - this.append(header[0], header[1]); - }, this); + this.append(header[0], header[1]) + }, this) } else if (headers) { Object.getOwnPropertyNames(headers).forEach(function(name) { - this.append(name, headers[name]); - }, this); + this.append(name, headers[name]) + }, this) } } + Headers.prototype.append = function(name, value) { - name = normalizeName(name); - value = normalizeValue(value); - var oldValue = this.map[name]; - this.map[name] = oldValue ? oldValue + ", " + value : value; -}; -Headers.prototype["delete"] = function(name) { - delete this.map[normalizeName(name)]; -}; + name = normalizeName(name) + value = normalizeValue(value) + var oldValue = this.map[name] + this.map[name] = oldValue ? oldValue + ', ' + value : value +} + +Headers.prototype['delete'] = function(name) { + delete this.map[normalizeName(name)] +} + Headers.prototype.get = function(name) { - name = normalizeName(name); - return this.has(name) ? this.map[name] : null; -}; + name = normalizeName(name) + return this.has(name) ? this.map[name] : null +} + Headers.prototype.has = function(name) { - return this.map.hasOwnProperty(normalizeName(name)); -}; + return this.map.hasOwnProperty(normalizeName(name)) +} + Headers.prototype.set = function(name, value) { - this.map[normalizeName(name)] = normalizeValue(value); -}; + this.map[normalizeName(name)] = normalizeValue(value) +} + Headers.prototype.forEach = function(callback, thisArg) { for (var name in this.map) { if (this.map.hasOwnProperty(name)) { - callback.call(thisArg, this.map[name], name, this); + callback.call(thisArg, this.map[name], name, this) } } -}; +} + Headers.prototype.keys = function() { - var items = []; + var items = [] this.forEach(function(value, name) { - items.push(name); - }); - return iteratorFor(items); -}; + items.push(name) + }) + return iteratorFor(items) +} + Headers.prototype.values = function() { - var items = []; + var items = [] this.forEach(function(value) { - items.push(value); - }); - return iteratorFor(items); -}; + items.push(value) + }) + return iteratorFor(items) +} + Headers.prototype.entries = function() { - var items = []; + var items = [] this.forEach(function(value, name) { - items.push([name, value]); - }); - return iteratorFor(items); -}; + items.push([name, value]) + }) + return iteratorFor(items) +} + if (support.iterable) { - Headers.prototype[Symbol.iterator] = Headers.prototype.entries; + Headers.prototype[Symbol.iterator] = Headers.prototype.entries } + function consumed(body) { - if (body._noBody) - return; + if (body._noBody) return if (body.bodyUsed) { - return Promise.reject(new TypeError("Already read")); + return Promise.reject(new TypeError('Already read')) } - body.bodyUsed = true; + body.bodyUsed = true } + function fileReaderReady(reader) { return new Promise(function(resolve, reject) { reader.onload = function() { - resolve(reader.result); - }; + resolve(reader.result) + } reader.onerror = function() { - reject(reader.error); - }; - }); + reject(reader.error) + } + }) } + function readBlobAsArrayBuffer(blob) { - var reader = new FileReader(); - var promise = fileReaderReady(reader); - reader.readAsArrayBuffer(blob); - return promise; + var reader = new FileReader() + var promise = fileReaderReady(reader) + reader.readAsArrayBuffer(blob) + return promise } + function readBlobAsText(blob) { - var reader = new FileReader(); - var promise = fileReaderReady(reader); - var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type); - var encoding = match ? match[1] : "utf-8"; - reader.readAsText(blob, encoding); - return promise; + var reader = new FileReader() + var promise = fileReaderReady(reader) + var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type) + var encoding = match ? match[1] : 'utf-8' + reader.readAsText(blob, encoding) + return promise } + function readArrayBufferAsText(buf) { - var view = new Uint8Array(buf); - var chars = new Array(view.length); + var view = new Uint8Array(buf) + var chars = new Array(view.length) + for (var i = 0; i < view.length; i++) { - chars[i] = String.fromCharCode(view[i]); + chars[i] = String.fromCharCode(view[i]) } - return chars.join(""); + return chars.join('') } + function bufferClone(buf) { if (buf.slice) { - return buf.slice(0); + return buf.slice(0) } else { - var view = new Uint8Array(buf.byteLength); - view.set(new Uint8Array(buf)); - return view.buffer; + var view = new Uint8Array(buf.byteLength) + view.set(new Uint8Array(buf)) + return view.buffer } } + function Body() { - this.bodyUsed = false; + this.bodyUsed = false + this._initBody = function(body) { - this.bodyUsed = this.bodyUsed; - this._bodyInit = body; + /* + fetch-mock wraps the Response object in an ES6 Proxy to + provide useful test harness features such as flush. However, on + ES5 browsers without fetch or Proxy support pollyfills must be used; + the proxy-pollyfill is unable to proxy an attribute unless it exists + on the object before the Proxy is created. This change ensures + Response.bodyUsed exists on the instance, while maintaining the + semantic of setting Request.bodyUsed in the constructor before + _initBody is called. + */ + // eslint-disable-next-line no-self-assign + this.bodyUsed = this.bodyUsed + this._bodyInit = body if (!body) { this._noBody = true; - this._bodyText = ""; - } else if (typeof body === "string") { - this._bodyText = body; + this._bodyText = '' + } else if (typeof body === 'string') { + this._bodyText = body } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { - this._bodyBlob = body; + this._bodyBlob = body } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { - this._bodyFormData = body; + this._bodyFormData = body } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { - this._bodyText = body.toString(); + this._bodyText = body.toString() } else if (support.arrayBuffer && support.blob && isDataView(body)) { - this._bodyArrayBuffer = bufferClone(body.buffer); - this._bodyInit = new Blob([this._bodyArrayBuffer]); + this._bodyArrayBuffer = bufferClone(body.buffer) + // IE 10-11 can't handle a DataView body. + this._bodyInit = new Blob([this._bodyArrayBuffer]) } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { - this._bodyArrayBuffer = bufferClone(body); + this._bodyArrayBuffer = bufferClone(body) } else { - this._bodyText = body = Object.prototype.toString.call(body); + this._bodyText = body = Object.prototype.toString.call(body) } - if (!this.headers.get("content-type")) { - if (typeof body === "string") { - this.headers.set("content-type", "text/plain;charset=UTF-8"); + + if (!this.headers.get('content-type')) { + if (typeof body === 'string') { + this.headers.set('content-type', 'text/plain;charset=UTF-8') } else if (this._bodyBlob && this._bodyBlob.type) { - this.headers.set("content-type", this._bodyBlob.type); + this.headers.set('content-type', this._bodyBlob.type) } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { - this.headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8"); + this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') } } - }; + } + if (support.blob) { this.blob = function() { - var rejected = consumed(this); + var rejected = consumed(this) if (rejected) { - return rejected; + return rejected } + if (this._bodyBlob) { - return Promise.resolve(this._bodyBlob); + return Promise.resolve(this._bodyBlob) } else if (this._bodyArrayBuffer) { - return Promise.resolve(new Blob([this._bodyArrayBuffer])); + return Promise.resolve(new Blob([this._bodyArrayBuffer])) } else if (this._bodyFormData) { - throw new Error("could not read FormData body as blob"); + throw new Error('could not read FormData body as blob') } else { - return Promise.resolve(new Blob([this._bodyText])); + return Promise.resolve(new Blob([this._bodyText])) } - }; + } } + this.arrayBuffer = function() { if (this._bodyArrayBuffer) { - var isConsumed = consumed(this); + var isConsumed = consumed(this) if (isConsumed) { - return isConsumed; + return isConsumed } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) { return Promise.resolve( this._bodyArrayBuffer.buffer.slice( this._bodyArrayBuffer.byteOffset, this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength ) - ); + ) } else { - return Promise.resolve(this._bodyArrayBuffer); + return Promise.resolve(this._bodyArrayBuffer) } } else if (support.blob) { - return this.blob().then(readBlobAsArrayBuffer); + return this.blob().then(readBlobAsArrayBuffer) } else { - throw new Error("could not read as ArrayBuffer"); + throw new Error('could not read as ArrayBuffer') } - }; + } + this.text = function() { - var rejected = consumed(this); + var rejected = consumed(this) if (rejected) { - return rejected; + return rejected } + if (this._bodyBlob) { - return readBlobAsText(this._bodyBlob); + return readBlobAsText(this._bodyBlob) } else if (this._bodyArrayBuffer) { - return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)); + return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) } else if (this._bodyFormData) { - throw new Error("could not read FormData body as text"); + throw new Error('could not read FormData body as text') } else { - return Promise.resolve(this._bodyText); + return Promise.resolve(this._bodyText) } - }; + } + if (support.formData) { this.formData = function() { - return this.text().then(decode); - }; + return this.text().then(decode) + } } + this.json = function() { - return this.text().then(JSON.parse); - }; - return this; + return this.text().then(JSON.parse) + } + + return this } -var methods = ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"]; + +// HTTP methods whose capitalization should be normalized +var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'] + function normalizeMethod(method) { - var upcased = method.toUpperCase(); - return methods.indexOf(upcased) > -1 ? upcased : method; + var upcased = method.toUpperCase() + return methods.indexOf(upcased) > -1 ? upcased : method } -function Request(input, options) { + +export function Request(input, options) { if (!(this instanceof Request)) { - throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.'); + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') } - options = options || {}; - var body = options.body; + + options = options || {} + var body = options.body + if (input instanceof Request) { if (input.bodyUsed) { - throw new TypeError("Already read"); + throw new TypeError('Already read') } - this.url = input.url; - this.credentials = input.credentials; + this.url = input.url + this.credentials = input.credentials if (!options.headers) { - this.headers = new Headers(input.headers); + this.headers = new Headers(input.headers) } - this.method = input.method; - this.mode = input.mode; - this.signal = input.signal; + this.method = input.method + this.mode = input.mode + this.signal = input.signal if (!body && input._bodyInit != null) { - body = input._bodyInit; - input.bodyUsed = true; + body = input._bodyInit + input.bodyUsed = true } } else { - this.url = String(input); + this.url = String(input) } - this.credentials = options.credentials || this.credentials || "same-origin"; + + this.credentials = options.credentials || this.credentials || 'same-origin' if (options.headers || !this.headers) { - this.headers = new Headers(options.headers); + this.headers = new Headers(options.headers) } - this.method = normalizeMethod(options.method || this.method || "GET"); - this.mode = options.mode || this.mode || null; - this.signal = options.signal || this.signal || function() { - if ("AbortController" in g) { + this.method = normalizeMethod(options.method || this.method || 'GET') + this.mode = options.mode || this.mode || null + this.signal = options.signal || this.signal || (function () { + if ('AbortController' in g) { var ctrl = new AbortController(); return ctrl.signal; } - }(); - this.referrer = null; - if ((this.method === "GET" || this.method === "HEAD") && body) { - throw new TypeError("Body not allowed for GET or HEAD requests"); + }()); + this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && body) { + throw new TypeError('Body not allowed for GET or HEAD requests') } - this._initBody(body); - if (this.method === "GET" || this.method === "HEAD") { - if (options.cache === "no-store" || options.cache === "no-cache") { - var reParamSearch = /([?&])_=[^&]*/; + this._initBody(body) + + if (this.method === 'GET' || this.method === 'HEAD') { + if (options.cache === 'no-store' || options.cache === 'no-cache') { + // Search for a '_' parameter in the query string + var reParamSearch = /([?&])_=[^&]*/ if (reParamSearch.test(this.url)) { - this.url = this.url.replace(reParamSearch, "$1_=" + (/* @__PURE__ */ new Date()).getTime()); + // If it already exists then set the value with the current time + this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()) } else { - var reQueryString = /\?/; - this.url += (reQueryString.test(this.url) ? "&" : "?") + "_=" + (/* @__PURE__ */ new Date()).getTime(); + // Otherwise add a new '_' parameter to the end with the current time + var reQueryString = /\?/ + this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime() } } } } + Request.prototype.clone = function() { - return new Request(this, { body: this._bodyInit }); -}; + return new Request(this, {body: this._bodyInit}) +} + function decode(body) { - var form = new FormData(); - body.trim().split("&").forEach(function(bytes) { - if (bytes) { - var split = bytes.split("="); - var name = split.shift().replace(/\+/g, " "); - var value = split.join("=").replace(/\+/g, " "); - form.append(decodeURIComponent(name), decodeURIComponent(value)); - } - }); - return form; + var form = new FormData() + body + .trim() + .split('&') + .forEach(function(bytes) { + if (bytes) { + var split = bytes.split('=') + var name = split.shift().replace(/\+/g, ' ') + var value = split.join('=').replace(/\+/g, ' ') + form.append(decodeURIComponent(name), decodeURIComponent(value)) + } + }) + return form } + function parseHeaders(rawHeaders) { - var headers = new Headers(); - var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, " "); - preProcessedHeaders.split("\r").map(function(header) { - return header.indexOf("\n") === 0 ? header.substr(1, header.length) : header; - }).forEach(function(line) { - var parts = line.split(":"); - var key = parts.shift().trim(); - if (key) { - var value = parts.join(":").trim(); - try { - headers.append(key, value); - } catch (error) { - console.warn("Response " + error.message); + var headers = new Headers() + // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space + // https://tools.ietf.org/html/rfc7230#section-3.2 + var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ') + // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill + // https://github.com/github/fetch/issues/748 + // https://github.com/zloirock/core-js/issues/751 + preProcessedHeaders + .split('\r') + .map(function(header) { + return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header + }) + .forEach(function(line) { + var parts = line.split(':') + var key = parts.shift().trim() + if (key) { + var value = parts.join(':').trim() + try { + headers.append(key, value) + } catch (error) { + console.warn('Response ' + error.message) + } } - } - }); - return headers; + }) + return headers } -Body.call(Request.prototype); -function Response(bodyInit, options) { + +Body.call(Request.prototype) + +export function Response(bodyInit, options) { if (!(this instanceof Response)) { - throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.'); + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') } if (!options) { - options = {}; + options = {} } - this.type = "default"; - this.status = options.status === void 0 ? 200 : options.status; + + this.type = 'default' + this.status = options.status === undefined ? 200 : options.status if (this.status < 200 || this.status > 599) { - throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599]."); + throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].") } - this.ok = this.status >= 200 && this.status < 300; - this.statusText = options.statusText === void 0 ? "" : "" + options.statusText; - this.headers = new Headers(options.headers); - this.url = options.url || ""; - this._initBody(bodyInit); + this.ok = this.status >= 200 && this.status < 300 + this.statusText = options.statusText === undefined ? '' : '' + options.statusText + this.headers = new Headers(options.headers) + this.url = options.url || '' + this._initBody(bodyInit) } -Body.call(Response.prototype); + +Body.call(Response.prototype) + Response.prototype.clone = function() { return new Response(this._bodyInit, { status: this.status, statusText: this.statusText, headers: new Headers(this.headers), url: this.url - }); -}; + }) +} + Response.error = function() { - var response = new Response(null, { status: 200, statusText: "" }); - response.status = 0; - response.type = "error"; - return response; -}; -var redirectStatuses = [301, 302, 303, 307, 308]; + var response = new Response(null, {status: 200, statusText: ''}) + response.ok = false + response.status = 0 + response.type = 'error' + return response +} + +var redirectStatuses = [301, 302, 303, 307, 308] + Response.redirect = function(url, status) { if (redirectStatuses.indexOf(status) === -1) { - throw new RangeError("Invalid status code"); + throw new RangeError('Invalid status code') } - return new Response(null, { status, headers: { location: url } }); -}; -var DOMException = g.DOMException; + + return new Response(null, {status: status, headers: {location: url}}) +} + +export var DOMException = g.DOMException try { - new DOMException(); + new DOMException() } catch (err) { DOMException = function(message, name) { - this.message = message; - this.name = name; - var error = Error(message); - this.stack = error.stack; - }; - DOMException.prototype = Object.create(Error.prototype); - DOMException.prototype.constructor = DOMException; -} -function fetch(input, init) { + this.message = message + this.name = name + var error = Error(message) + this.stack = error.stack + } + DOMException.prototype = Object.create(Error.prototype) + DOMException.prototype.constructor = DOMException +} + +export function fetch(input, init) { return new Promise(function(resolve, reject) { - var request = new Request(input, init); + var request = new Request(input, init) + if (request.signal && request.signal.aborted) { - return reject(new DOMException("Aborted", "AbortError")); + return reject(new DOMException('Aborted', 'AbortError')) } - var xhr = new XMLHttpRequest(); + + var xhr = new XMLHttpRequest() + function abortXhr() { - xhr.abort(); + xhr.abort() } + xhr.onload = function() { var options = { - status: xhr.status, statusText: xhr.statusText, - headers: parseHeaders(xhr.getAllResponseHeaders() || "") - }; - options.url = "responseURL" in xhr ? xhr.responseURL : options.headers.get("X-Request-URL"); - var body = "response" in xhr ? xhr.response : xhr.responseText; + headers: parseHeaders(xhr.getAllResponseHeaders() || '') + } + // This check if specifically for when a user fetches a file locally from the file system + // Only if the status is out of a normal range + if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) { + options.status = 200; + } else { + options.status = xhr.status; + } + options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') + var body = 'response' in xhr ? xhr.response : xhr.responseText setTimeout(function() { - resolve(new Response(body, options)); - }, 0); - }; + resolve(new Response(body, options)) + }, 0) + } + xhr.onerror = function() { setTimeout(function() { - reject(new TypeError("Network request failed")); - }, 0); - }; + reject(new TypeError('Network request failed')) + }, 0) + } + xhr.ontimeout = function() { setTimeout(function() { - reject(new TypeError("Network request failed")); - }, 0); - }; + reject(new TypeError('Network request timed out')) + }, 0) + } + xhr.onabort = function() { setTimeout(function() { - reject(new DOMException("Aborted", "AbortError")); - }, 0); - }; + reject(new DOMException('Aborted', 'AbortError')) + }, 0) + } + function fixUrl(url) { try { - return url === "" && g.location.href ? g.location.href : url; + return url === '' && g.location.href ? g.location.href : url } catch (e) { - return url; + return url } } - xhr.open(request.method, fixUrl(request.url), true); - if (request.credentials === "include") { - xhr.withCredentials = true; - } else if (request.credentials === "omit") { - xhr.withCredentials = false; + + xhr.open(request.method, fixUrl(request.url), true) + + if (request.credentials === 'include') { + xhr.withCredentials = true + } else if (request.credentials === 'omit') { + xhr.withCredentials = false } - if ("responseType" in xhr) { + + if ('responseType' in xhr) { if (support.blob) { - xhr.responseType = "blob"; - } else if (support.arrayBuffer) { - xhr.responseType = "arraybuffer"; + xhr.responseType = 'blob' + } else if ( + support.arrayBuffer + ) { + xhr.responseType = 'arraybuffer' } } - if (init && typeof init.headers === "object" && !(init.headers instanceof Headers || g.Headers && init.headers instanceof g.Headers)) { + + if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) { var names = []; Object.getOwnPropertyNames(init.headers).forEach(function(name) { - names.push(normalizeName(name)); - xhr.setRequestHeader(name, normalizeValue(init.headers[name])); - }); + names.push(normalizeName(name)) + xhr.setRequestHeader(name, normalizeValue(init.headers[name])) + }) request.headers.forEach(function(value, name) { if (names.indexOf(name) === -1) { - xhr.setRequestHeader(name, value); + xhr.setRequestHeader(name, value) } - }); + }) } else { request.headers.forEach(function(value, name) { - xhr.setRequestHeader(name, value); - }); + xhr.setRequestHeader(name, value) + }) } + if (request.signal) { - request.signal.addEventListener("abort", abortXhr); + request.signal.addEventListener('abort', abortXhr) + xhr.onreadystatechange = function() { + // DONE (success or failure) if (xhr.readyState === 4) { - request.signal.removeEventListener("abort", abortXhr); + request.signal.removeEventListener('abort', abortXhr) } - }; + } } - xhr.send(typeof request._bodyInit === "undefined" ? null : request._bodyInit); - }); -} -fetch.polyfill = true; -if (!g.fetch) { - g.fetch = fetch; - g.Headers = Headers; - g.Request = Request; - g.Response = Response; -} -export { - DOMException, + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) + }) +} + +export default { + fetch, Headers, Request, - Response, - fetch -}; + Response +} diff --git a/api/fetch/index.js b/api/fetch/index.js index d9cad62a6e..1ddb3282a4 100644 --- a/api/fetch/index.js +++ b/api/fetch/index.js @@ -1,6 +1,311 @@ /** * @ignore */ -import { fetch } from './fetch.js' -export * from './fetch.js' +import { fetch, Headers, Request, Response } from './fetch.js' +import { Deferred } from '../async/deferred.js' +import { Buffer } from '../buffer.js' +import http from '../http.js' + +export { + fetch, + Headers, + Request, + Response +} + +Response.json = function (json, options) { + return new Response(JSON.stringify(json), { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }) +} + +const initResponseBody = Response.prototype._initBody +const initRequestBody = Request.prototype._initBody +const textEncoder = new TextEncoder() + +Response.prototype._initBody = initBody +Request.prototype._initBody = initBody + +async function initBody (body) { + this.body = null + + if ( + typeof this.statusText !== 'string' && + Number.isFinite(this.status) && + this.status in http.STATUS_CODES + ) { + this.statusText = http.STATUS_CODES[this.status] + } + + if (typeof globalThis.ReadableStream === 'function') { + if (body && body instanceof globalThis.ReadableStream) { + let controller = null + this.body = new ReadableStream({ + start (c) { + controller = c + } + }) + + const chunks = [] + const deferred = new Deferred() + + Object.assign(this, { + async text () { + await deferred + return Response.prototype.text.call(this) + }, + + async blob () { + await deferred + return Response.prototype.blob.call(this) + }, + + async arrayBuffer () { + await deferred + return Response.prototype.arrayBuffer.call(this) + } + }) + + for await (const chunk of body) { + controller.enqueue(chunk) + chunks.push(new Uint8Array(chunk)) + } + + const buffer = Buffer.concat(chunks) + + this._bodyArrayBuffer = buffer.buffer + this._bodyInit = new Blob([this._bodyArrayBuffer]) + + controller.close() + deferred.resolve() + + return + } + } + + if (this instanceof Request) { + initRequestBody.call(this, body) + } else if (this instanceof Response) { + initResponseBody.call(this, body) + } + + if (!this.body && !this._noBody) { + if (this._bodyArrayBuffer) { + const arrayBuffer = this._bodyArrayBuffer + this.body = new ReadableStream({ + start (controller) { + controller.enqueue(arrayBuffer) + controller.close() + } + }) + } else if (this._bodyBlob) { + const blob = this._bodyBlob + this.body = new ReadableStream({ + async start (controller) { + controller.enqueue(await blob.arrayBuffer()) + controller.close() + } + }) + } else if (this._bodyText) { + const text = this._bodyText + this.body = new ReadableStream({ + start (controller) { + controller.enqueue(textEncoder.encode(text)) + controller.close() + } + }) + } else { + this.body = null + } + } +} + +Object.defineProperties(Request.prototype, { + _bodyArrayBuffer: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyFormData: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyText: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyInit: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyBlob: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _noBody: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + url: { + configurable: true, + writable: true, + value: undefined + }, + + body: { + configurable: true, + writable: true, + value: undefined + }, + + credentials: { + configurable: true, + writable: true, + value: undefined + }, + + method: { + configurable: true, + writable: true, + value: undefined + }, + + mode: { + configurable: true, + writable: true, + value: undefined + }, + + signal: { + configurable: true, + writable: true, + value: undefined + }, + + headers: { + configurable: true, + writable: true, + value: undefined + }, + + referrer: { + configurable: true, + writable: true, + value: undefined + } +}) + +Object.defineProperties(Response.prototype, { + _bodyArrayBuffer: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyFormData: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyText: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyInit: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _bodyBlob: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + _noBody: { + configurable: true, + enumerable: false, + writable: true, + value: undefined + }, + + body: { + configurable: true, + writable: true, + value: undefined + }, + + type: { + configurable: true, + writable: true, + value: undefined + }, + + status: { + configurable: true, + writable: true, + value: undefined + }, + + statusText: { + configurable: true, + writable: true, + value: undefined + }, + + ok: { + configurable: true, + writable: true, + value: undefined + }, + + url: { + configurable: true, + writable: true, + value: undefined + }, + + headers: { + configurable: true, + writable: true, + value: undefined + }, + + redirected: { + configurable: true, + writable: true, + value: undefined + } +}) + export default fetch diff --git a/api/fs/bookmarks.js b/api/fs/bookmarks.js new file mode 100644 index 0000000000..46af490ccb --- /dev/null +++ b/api/fs/bookmarks.js @@ -0,0 +1,12 @@ +/** + * A map of known absolute file paths to file IDs that + * have been granted access outside of the sandbox. + * XXX(@jwerle): this is currently only used on linux, but valaues may + * be added for all platforms, likely from a file system picker dialog. + * @type {Map} + */ +export const temporary = new Map() + +export default { + temporary +} diff --git a/api/fs/constants.js b/api/fs/constants.js index e6460c2bcb..f4625f2764 100644 --- a/api/fs/constants.js +++ b/api/fs/constants.js @@ -31,6 +31,7 @@ export const UV_DIRENT_CHAR = constants.UV_DIRENT_CHAR || 6 export const UV_DIRENT_BLOCK = constants.UV_DIRENT_BLOCK || 7 export const UV_FS_SYMLINK_DIR = constants.UV_FS_SYMLINK_DIR || 1 export const UV_FS_SYMLINK_JUNCTION = constants.UV_FS_SYMLINK_JUNCTION || 2 +export const UV_FS_O_FILEMAP = constants.UV_FS_O_FILEMAP || 0 export const O_RDONLY = constants.O_RDONLY || 0 export const O_WRONLY = constants.O_WRONLY || 1 diff --git a/api/fs/dir.js b/api/fs/dir.js index 198ef8c37e..83cc73c84c 100644 --- a/api/fs/dir.js +++ b/api/fs/dir.js @@ -1,5 +1,6 @@ import { DirectoryHandle } from './handle.js' import { Buffer } from '../buffer.js' +import { clamp } from '../util.js' import { UV_DIRENT_UNKNOWN, UV_DIRENT_FILE, @@ -10,6 +11,9 @@ import { UV_DIRENT_CHAR, UV_DIRENT_BLOCK } from './constants.js' +import fds from './fds.js' + +import ipc from '../ipc.js' import * as exports from './dir.js' @@ -83,7 +87,7 @@ export class Dir { * @param {object|function} options * @param {function=} callback */ - async close (options = null, callback) { + async close (options = null, callback = null) { if (typeof options === 'function') { callback = options options = {} @@ -102,11 +106,26 @@ export class Dir { return await this.handle?.close(options) } + /** + * Closes container and underlying handle + * synchronously. + * @param {object=} [options] + */ + closeSync (options = null) { + const { id } = this.handle + const result = ipc.sendSync('fs.closedir', { id }, options) + if (result.err) { + throw result.err + } + + fds.release(id, false) + } + /** * Reads and returns directory entry. * @param {object|function} options * @param {function=} callback - * @return {Dirent|string} + * @return {Promise} */ async read (options, callback) { if (typeof options === 'function') { @@ -161,6 +180,61 @@ export class Dir { return results } + /** + * Reads and returns directory entry synchronously. + * @param {object|function} options + * @return {Dirent[]|string[]} + */ + readSync (options = null) { + const { encoding } = this + const { id } = this.handle + const entries = clamp( + options?.entries || DirectoryHandle.MAX_ENTRIES, + 1, // MIN_ENTRIES + DirectoryHandle.MAX_ENTRIES + ) + + const result = ipc.sendSync('fs.readdir', { id, entries }, options) + + if (result.err) { + throw result.err + } + + const results = result.data + .map((entry) => ({ + type: entry.type, + name: decodeURIComponent(entry.name) + })) + .map((result) => { + const { name } = result + + if (this.withFileTypes) { + result = Dirent.from(result) + + if (encoding === 'buffer') { + result.name = Buffer.from(name) + } else { + result.name = Buffer.from(name).toString(encoding) + } + return result + } + + if (encoding === 'buffer') { + return Buffer.from(name) + } else { + return Buffer.from(name).toString(encoding) + } + }) + + if (results.length === 1) { + return results[0] + } else if (results.length === 0) { + return null + } + + return results + } + /** * AsyncGenerator which yields directory entries. * @param {object=} options diff --git a/api/fs/fds.js b/api/fs/fds.js index 2b6914c737..02191121d8 100644 --- a/api/fs/fds.js +++ b/api/fs/fds.js @@ -1,5 +1,4 @@ import diagnostics from '../diagnostics.js' -import console from '../console.js' import ipc from '../ipc.js' const dc = diagnostics.channels.group('fs', [ diff --git a/api/fs/handle.js b/api/fs/handle.js index ac86dae1ae..d2a039de68 100644 --- a/api/fs/handle.js +++ b/api/fs/handle.js @@ -1,5 +1,4 @@ import { - InvertedPromise, isBufferLike, isTypedArray, isEmptyObject, @@ -7,23 +6,25 @@ import { clamp } from '../util.js' -import { rand64 } from '../crypto.js' - +import { F_OK, O_APPEND, S_IFREG } from './constants.js' import { ReadStream, WriteStream } from './stream.js' import { normalizeFlags } from './flags.js' +import { AsyncResource } from '../async/resource.js' import { EventEmitter } from '../events.js' import { AbortError } from '../errors.js' +import { Deferred } from '../async.js' import diagnostics from '../diagnostics.js' import { Buffer } from '../buffer.js' +import { rand64 } from '../crypto.js' import { Stats } from './stats.js' -import { F_OK } from './constants.js' -import console from '../console.js' import fds from './fds.js' import ipc from '../ipc.js' import gc from '../gc.js' import * as exports from './handle.js' +const kFileDescriptor = Symbol.for('socket.runtune.fs.web.FileDescriptor') + /** * @typedef {Uint8Array|Int8Array} TypedArray */ @@ -36,6 +37,25 @@ const dc = diagnostics.channels.group('fs', [ 'handle.close' ]) +function normalizePath (path) { + if (path instanceof URL) { + if (path.origin === globalThis.location.origin) { + return normalizePath(path.href) + } + + return null + } + + if (URL.canParse(path)) { + const url = new URL(path) + if (url.origin === globalThis.location.origin) { + path = `./${url.pathname.slice(1)}` + } + } + + return path +} + export const kOpening = Symbol.for('fs.FileHandle.opening') export const kClosing = Symbol.for('fs.FileHandle.closing') export const kClosed = Symbol.for('fs.FileHandle.closed') @@ -52,10 +72,21 @@ export class FileHandle extends EventEmitter { /** * Creates a `FileHandle` from a given `id` or `fd` - * @param {string|number|FileHandle|object} id + * @param {string|number|FileHandle|object|FileSystemFileHandle} id * @return {FileHandle} */ static from (id) { + if ( + globalThis.FileSystemFileHandle && + id instanceof globalThis.FileSystemFileHandle + ) { + if (id[kFileDescriptor]) { + return id[kFileDescriptor] + } + + return new this({ handle: id }) + } + if (id?.id) { return this.from(id.id) } else if (id?.fd) { @@ -97,6 +128,7 @@ export class FileHandle extends EventEmitter { mode = FileHandle.DEFAULT_ACCESS_MODE } + path = normalizePath(path) const result = await ipc.request('fs.access', { mode, path }, options) if (result.err) { @@ -125,6 +157,13 @@ export class FileHandle extends EventEmitter { mode = FileHandle.DEFAULT_OPEN_MODE } + if (path instanceof globalThis.FileSystemFileHandle) { + const handle = this.from(path, options || mode || flags) + await handle.open(options) + return handle + } + + path = normalizePath(path) const handle = new this({ path, flags, mode }) if (typeof handle.path !== 'string') { @@ -136,6 +175,9 @@ export class FileHandle extends EventEmitter { return handle } + #resource = null + #fileSystemHandle = null + /** * `FileHandle` class constructor * @ignore @@ -153,10 +195,21 @@ export class FileHandle extends EventEmitter { this[kClosing] = null this[kClosed] = false + this.#resource = new AsyncResource('FileHandle') + this.#resource.handle = this + + if (options?.handle) { + this.#fileSystemHandle = options.handle + } + this.flags = normalizeFlags(options?.flags) this.path = options?.path || null this.mode = options?.mode || FileHandle.DEFAULT_OPEN_MODE + if (this.path) { + this.path = normalizePath(this.path) + } + // this id will be used to identify the file handle that is a // reference stored in the native side this.id = String(options?.id || rand64()) @@ -171,7 +224,9 @@ export class FileHandle extends EventEmitter { * @type {boolean} */ get opened () { - return this.fd !== null && this.fd === fds.get(this.id) + return this.#fileSystemHandle || ( + this.fd !== null && this.fd === fds.get(this.id) + ) } /** @@ -209,7 +264,10 @@ export class FileHandle extends EventEmitter { async handle (id) { if (fds.has(id)) { console.warn('Closing fs.FileHandle on garbage collection') - await ipc.request('fs.close', { id }, options) + await ipc.request('fs.close', { id }, { + ...options + }) + fds.release(id, false) } } @@ -225,11 +283,21 @@ export class FileHandle extends EventEmitter { * @param {object=} [options.signal] */ async appendFile (data, options) { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } - return await this.writeFile(data, options) + if ((this.flags & O_APPEND) !== O_APPEND) { + return await this.writeFile(data, options) + } + + return await this.write(data, 0, data.length, -1, options) } /** @@ -238,9 +306,17 @@ export class FileHandle extends EventEmitter { * @param {object=} [options] */ async chmod (mode, options) { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } + + // TODO(@jwerle) } /** @@ -250,9 +326,17 @@ export class FileHandle extends EventEmitter { * @param {object=} [options] */ async chown (uid, gid, options) { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } + + // TODO(@jwerle) } /** @@ -273,12 +357,16 @@ export class FileHandle extends EventEmitter { throw new Error('FileHandle is not opened') } - this[kClosing] = new InvertedPromise() + this[kClosing] = new Deferred() - const result = await ipc.request('fs.close', { id: this.id }, options) + if (!this.#fileSystemHandle) { + const result = await ipc.request('fs.close', { id: this.id }, { + ...options + }) - if (result.err) { - return this[kClosing].reject(result.err) + if (result.err) { + return this[kClosing].reject(result.err) + } } fds.release(this.id, false) @@ -292,7 +380,9 @@ export class FileHandle extends EventEmitter { this[kClosing] = null this[kClosed] = true - this.emit('close') + this.#resource.runInAsyncScope(() => { + this.emit('close') + }) dc.channel('handle.close').publish({ handle: this }) @@ -319,7 +409,9 @@ export class FileHandle extends EventEmitter { try { await this.close() } catch (err) { - stream.emit('error', err) + this.#resource.runInAsyncScope(() => { + stream.emit('error', err) + }) } } }) @@ -347,7 +439,9 @@ export class FileHandle extends EventEmitter { try { await this.close() } catch (err) { - stream.emit('error', err) + this.#resource.runInAsyncScope(() => { + stream.emit('error', err) + }) } } }) @@ -359,6 +453,12 @@ export class FileHandle extends EventEmitter { * @param {object=} [options] */ async datasync () { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } @@ -391,26 +491,38 @@ export class FileHandle extends EventEmitter { throw new AbortError(options.signal) } - this[kOpening] = new InvertedPromise() + this[kOpening] = new Deferred() - const result = await ipc.request('fs.open', { - id, - mode, - path, - flags - }, options) - - if (result.err) { - return this[kOpening].reject(result.err) - } + if (this.#fileSystemHandle) { + fds.set(this.id, this.id, 'file') + } else { + const result = await ipc.request('fs.open', { + id, + mode, + path, + flags + }, { + ...options + }) - this.fd = result.data.fd + if (result.err) { + return this[kOpening].reject(result.err) + } - fds.set(this.id, this.fd, 'file') + if (result.data?.fd) { + this.fd = result.data.fd + fds.set(this.id, this.fd, 'file') + } else { + this.fd = id + fds.set(this.id, this.id, 'file') + } + } this[kOpening].resolve(true) - this.emit('open', this.fd) + this.#resource.runInAsyncScope(() => { + this.emit('open', this.fd) + }) dc.channel('handle.open').publish({ handle: this, mode, path, flags }) @@ -505,35 +617,47 @@ export class FileHandle extends EventEmitter { ) } - const result = await ipc.request('fs.read', { - id, - size: length, - offset: position - }, { signal, timeout, responseType: 'arraybuffer' }) + if (this.#fileSystemHandle) { + const file = this.#fileSystemHandle.getFile() + const blob = file.slice(position, position + length) + const arrayBuffer = await blob.arrayBuffer() + bytesRead = arrayBuffer.byteLength + Buffer.from(arrayBuffer).copy(Buffer.from(buffer), 0, offset) + } else { + const result = await ipc.request('fs.read', { + id, + size: length, + offset: position + }, { + responseType: 'arraybuffer', + timeout, + signal + }) - if (result.err) { - throw result.err - } + if (result.err) { + throw result.err + } - const contentType = result.headers?.get('content-type') + const contentType = result.headers?.get('content-type') - if (contentType && contentType !== 'application/octet-stream') { - throw new TypeError( - `Invalid response content type from 'fs.read'. Received: ${contentType}` - ) - } + if (contentType && contentType !== 'application/octet-stream') { + throw new TypeError( + `Invalid response content type from 'fs.read'. Received: ${contentType}` + ) + } - if (isTypedArray(result.data) || result.data instanceof ArrayBuffer) { - bytesRead = result.data.byteLength - Buffer.from(result.data).copy(Buffer.from(buffer), 0, offset) - dc.channel('handle.read').publish({ handle: this, bytesRead }) - } else if (isEmptyObject(result.data)) { - // an empty response from mac returns an empty object sometimes - bytesRead = 0 - } else { - throw new TypeError( - `Invalid response buffer from 'fs.read' Received: ${typeof result.data}` - ) + if (isTypedArray(result.data) || result.data instanceof ArrayBuffer) { + bytesRead = result.data.byteLength + Buffer.from(result.data).copy(Buffer.from(buffer), 0, offset) + dc.channel('handle.read').publish({ handle: this, bytesRead }) + } else if (isEmptyObject(result.data)) { + // an empty response from mac returns an empty object sometimes + bytesRead = 0 + } else { + throw new TypeError( + `Invalid response buffer from 'fs.read' Received: ${typeof result.data}` + ) + } } return { bytesRead, buffer } @@ -593,7 +717,47 @@ export class FileHandle extends EventEmitter { throw new Error('FileHandle is not opened') } - const result = await ipc.request('fs.fstat', { ...options, id: this.id }) + if (this.#fileSystemHandle) { + const info = { + st_mode: S_IFREG, + st_size: this.#fileSystemHandle.size + } + + const stats = Stats.from(info, Boolean(options?.bigint)) + stats.handle = this + return stats + } + + const result = await ipc.request('fs.fstat', { id: this.id }, { + ...options + }) + + if (result.err) { + throw result.err + } + + const stats = Stats.from(result.data, Boolean(options?.bigint)) + stats.handle = this + return stats + } + + /** + * Returns the stats of the underlying symbolic link. + * @param {object=} [options] + * @return {Promise} + */ + async lstat (options) { + if (this.#fileSystemHandle) { + return this.stat(options) + } + + if (this.closing || this.closed) { + throw new Error('FileHandle is not opened') + } + + const result = await ipc.request('fs.lstat', { path: this.path }, { + ...options + }) if (result.err) { throw result.err @@ -609,6 +773,12 @@ export class FileHandle extends EventEmitter { * @return {Promise} */ async sync () { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } @@ -625,6 +795,12 @@ export class FileHandle extends EventEmitter { * @return {Promise} */ async truncate (offset = 0) { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } @@ -646,6 +822,12 @@ export class FileHandle extends EventEmitter { * @param {object=} [options] */ async write (buffer, offset, length, position, options) { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } @@ -728,6 +910,12 @@ export class FileHandle extends EventEmitter { * @param {object=} [options.signal] */ async writeFile (data, options) { + if (this.#fileSystemHandle) { + return new TypeError( + 'FileHandle underlying FileSystemFileHandle is not writable' + ) + } + if (this.closing || this.closed) { throw new Error('FileHandle is not opened') } @@ -788,11 +976,24 @@ export class DirectoryHandle extends EventEmitter { static get DEFAULT_BUFFER_SIZE () { return 32 } /** - * Creates a `FileHandle` from a given `id` or `fd` - * @param {string|number|DirectoryHandle|object} id + * Creates a `DirectoryHandle` from a given `id` or `fd` + * @param {string|number|DirectoryHandle|object|FileSystemDirectoryHandle} id + * @param {object} options * @return {DirectoryHandle} */ - static from (id) { + static from (id, options) { + if ( + globalThis.FileSystemDirectoryHandle && + id instanceof globalThis.FileSystemDirectoryHandle + ) { + if (id[kFileDescriptor]) { + const dir = id[kFileDescriptor] + return dir.handle ?? dir + } + + return new this({ handle: id }) + } + if (id?.id) { return this.from(id.id) } else if (id?.fd) { @@ -803,7 +1004,7 @@ export class DirectoryHandle extends EventEmitter { throw new Error('Invalid file descriptor for directory handle.') } - return new this({ id }) + return new this({ id, ...options }) } /** @@ -813,6 +1014,18 @@ export class DirectoryHandle extends EventEmitter { * @return {Promise} */ static async open (path, options) { + if (path instanceof globalThis.FileSystemDirectoryHandle) { + if (path[kFileDescriptor]) { + const dir = path[kFileDescriptor] + return dir.handle ?? dir + } + + const handle = this.from(path, options) + await handle.open(options) + return handle + } + + path = normalizePath(path) const handle = new this({ path }) if (typeof handle.path !== 'string') { @@ -824,6 +1037,10 @@ export class DirectoryHandle extends EventEmitter { return handle } + #resource = null + #fileSystemHandle = null + #fileSystemHandleIterator = null + /** * `DirectoryHandle` class constructor * @private @@ -841,11 +1058,22 @@ export class DirectoryHandle extends EventEmitter { this[kClosing] = null this[kClosed] = false + this.#resource = new AsyncResource('DirectoryHandle') + this.#resource.handle = this + // this id will be used to identify the file handle that is a // reference stored in the native side this.id = String(options?.id || rand64()) this.path = options?.path || null + if (this.path) { + this.path = normalizePath(this.path) + } + + if (options?.handle) { + this.#fileSystemHandle = options.handle + } + // @TODO(jwerle): implement usage of this internally this.bufferSize = Math.min( DirectoryHandle.MAX_BUFFER_SIZE, @@ -935,12 +1163,14 @@ export class DirectoryHandle extends EventEmitter { throw new AbortError(options.signal) } - this[kOpening] = new InvertedPromise() + this[kOpening] = new Deferred() - const result = await ipc.request('fs.opendir', { id, path }, options) + if (!this.#fileSystemHandle) { + const result = await ipc.request('fs.opendir', { id, path }, options) - if (result.err) { - return this[kOpening].reject(result.err) + if (result.err) { + return this[kOpening].reject(result.err) + } } // directory file descriptors are not accessible because @@ -949,7 +1179,9 @@ export class DirectoryHandle extends EventEmitter { this[kOpening].resolve(true) - this.emit('open', this.fd) + this.#resource.runInAsyncScope(() => { + this.emit('open', this.fd) + }) dc.channel('handle.open').publish({ handle: this, path }) @@ -980,12 +1212,14 @@ export class DirectoryHandle extends EventEmitter { throw new AbortError(options.signal) } - this[kClosing] = new InvertedPromise() + this[kClosing] = new Deferred() - const result = await ipc.request('fs.closedir', { id }, options) + if (!this.#fileSystemHandle) { + const result = await ipc.request('fs.closedir', { id }, options) - if (result.err) { - return this[kClosing].reject(result.err) + if (result.err) { + return this[kClosing].reject(result.err) + } } fds.release(this.id, false) @@ -997,7 +1231,10 @@ export class DirectoryHandle extends EventEmitter { this[kClosing] = null this[kClosed] = true - this.emit('close') + this.#resource.runInAsyncScope(() => { + this.emit('close') + }) + dc.channel('handle.close').publish({ handle: this }) return true @@ -1027,6 +1264,26 @@ export class DirectoryHandle extends EventEmitter { DirectoryHandle.MAX_ENTRIES ) + if (this.#fileSystemHandle) { + const results = [] + + if (!this.#fileSystemHandleIterator) { + this.#fileSystemHandleIterator = this.#fileSystemHandle.entries() + } + + for (let i = 0; i < entries; ++i) { + const [name, handle] = await this.#fileSystemHandleIterator.next() + results.push({ + name, + type: handle instanceof globalThis.FileSystemDirectoryHandle + ? 'directory' + : 'file' + }) + } + + return results + } + const { id } = this const result = await ipc.request('fs.readdir', { @@ -1038,7 +1295,10 @@ export class DirectoryHandle extends EventEmitter { throw result.err } - return result.data + return result.data.map((entry) => ({ + type: entry.type, + name: decodeURIComponent(entry.name) + })) } } diff --git a/api/fs/index.js b/api/fs/index.js index 667a7474b0..f5045fab9e 100644 --- a/api/fs/index.js +++ b/api/fs/index.js @@ -1,5 +1,5 @@ /** - * @module FS + * @module fs * * This module enables interacting with the file system in a way modeled on * standard POSIX functions. @@ -24,21 +24,26 @@ */ import { isBufferLike, isFunction, noop } from '../util.js' -import console from '../console.js' +import { rand64 } from '../crypto.js' +import { Buffer } from '../buffer.js' import ipc from '../ipc.js' import gc from '../gc.js' import { Dir, Dirent, sortDirectoryEntries } from './dir.js' import { DirectoryHandle, FileHandle } from './handle.js' import { ReadStream, WriteStream } from './stream.js' +import { normalizeFlags } from './flags.js' import * as constants from './constants.js' import * as promises from './promises.js' import { Watcher } from './watcher.js' import { Stats } from './stats.js' +import bookmarks from './bookmarks.js' import fds from './fds.js' import * as exports from './index.js' +const kFileDescriptor = Symbol.for('socket.runtune.fs.web.FileDescriptor') + /** * @typedef {import('../buffer.js').Buffer} Buffer * @typedef {Uint8Array|Int8Array} TypedArray @@ -48,12 +53,33 @@ function defaultCallback (err) { if (err) throw err } +function normalizePath (path) { + if (path instanceof URL) { + if (path.origin === globalThis.location.origin) { + return normalizePath(path.href) + } + + return null + } + + if (URL.canParse(path)) { + const url = new URL(path) + if (url.origin === globalThis.location.origin) { + path = `./${url.pathname.slice(1)}` + } + } + + return path +} + async function visit (path, options = null, callback) { if (typeof options === 'function') { callback = options options = {} } + path = normalizePath(path) + const { flags, flag, mode } = options || {} let handle = null @@ -77,7 +103,7 @@ async function visit (path, options = null, callback) { /** * Asynchronously check access a file for a given mode calling `callback` * upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} + * @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback} * @param {string | Buffer | URL} path * @param {string?|function(Error?)?} [mode = F_OK(0)] * @param {function(Error?)?} [callback] @@ -92,6 +118,15 @@ export function access (path, mode, callback) { throw new TypeError('callback must be a function.') } + if ( + path instanceof globalThis.FileSystemFileHandle || + path instanceof globalThis.FileSystemDirectoryHandle + ) { + return queueMicrotask(() => callback(null, mode)) + } + + path = normalizePath(path) + FileHandle .access(path, mode) .then((mode) => callback(null, mode)) @@ -99,13 +134,57 @@ export function access (path, mode, callback) { } /** - * @ignore + * Synchronously check access a file for a given mode calling `callback` + * upon success or error. + * @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback} + * @param {string | Buffer | URL} path + * @param {string?} [mode = F_OK(0)] */ -export function appendFile (path, data, options, callback) { +export function accessSync (path, mode = constants.F_OK) { + path = normalizePath(path) + const result = ipc.sendSync('fs.access', { path, mode }) + + if (result.err) { + throw result.err + } + + // F_OK means access in any way + return mode === constants.F_OK ? true : (result.data?.mode && mode) > 0 +} + +/** + * Checks if a path exists + * @param {string | Buffer | URL} path + * @param {function(Boolean)?} [callback] + */ +export function exists (path, callback) { + if (typeof callback !== 'function') { + throw new TypeError('callback must be a function.') + } + + path = normalizePath(path) + access(path, (err) => { + // eslint-disable-next-line + callback(err !== null) + }) +} + +/** + * Checks if a path exists + * @param {string | Buffer | URL} path + * @param {function(Boolean)?} [callback] + */ +export function existsSync (path) { + path = normalizePath(path) + try { + accessSync(path) + return true + } catch { + return false + } } /** - * * Asynchronously changes the permissions of a file. * No arguments other than a possible exception are given to the completion callback * @@ -128,11 +207,45 @@ export function chmod (path, mode, callback) { throw new TypeError('callback must be a function.') } + if ( + path instanceof globalThis.FileSystemFileHandle || + path instanceof globalThis.FileSystemDirectoryHandle + ) { + return new TypeError( + 'FileSystemHandle is not writable' + ) + } + + path = normalizePath(path) ipc.request('fs.chmod', { mode, path }).then((result) => { result?.err ? callback(result.err) : callback(null) }) } +/** + * Synchronously changes the permissions of a file. + * + * @see {@link https://nodejs.org/api/fs.html#fschmodpath-mode-callback} + * @param {string | Buffer | URL} path + * @param {number} mode + */ +export function chmodSync (path, mode) { + if (typeof mode !== 'number') { + throw new TypeError(`The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received ${mode}`) + } + + if (mode < 0 || !Number.isInteger(mode)) { + throw new RangeError(`The value of "mode" is out of range. It must be an integer. Received ${mode}`) + } + + path = normalizePath(path) + const result = ipc.sendSync('fs.chmod', { mode, path }) + + if (result.err) { + throw result.err + } +} + /** * Changes ownership of file or directory at `path` with `uid` and `gid`. * @param {string} path @@ -141,6 +254,7 @@ export function chmod (path, mode, callback) { * @param {function} callback */ export function chown (path, uid, gid, callback) { + path = normalizePath(path) if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -157,14 +271,50 @@ export function chown (path, uid, gid, callback) { throw new TypeError('callback must be a function.') } + if ( + path instanceof globalThis.FileSystemFileHandle || + path instanceof globalThis.FileSystemDirectoryHandle + ) { + return new TypeError( + 'FileSystemHandle is not writable' + ) + } + ipc.request('fs.chown', { path, uid, gid }).then((result) => { result?.err ? callback(result.err) : callback(null) }).catch(callback) } +/** + * Changes ownership of file or directory at `path` with `uid` and `gid`. + * @param {string} path + * @param {number} uid + * @param {number} gid + */ +export function chownSync (path, uid, gid) { + path = normalizePath(path) + if (typeof path !== 'string') { + throw new TypeError('The argument \'path\' must be a string') + } + + if (!Number.isInteger(uid)) { + throw new TypeError('The argument \'uid\' must be an integer') + } + + if (!Number.isInteger(gid)) { + throw new TypeError('The argument \'gid\' must be an integer') + } + + const result = ipc.sendSync('fs.chown', { path, uid, gid }) + + if (result.err) { + throw result.err + } +} + /** * Asynchronously close a file descriptor calling `callback` upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsclosefd-callback} + * @see {@link https://nodejs.org/api/fs.html#fsclosefd-callback} * @param {number} fd * @param {function(Error?)?} [callback] */ @@ -184,15 +334,31 @@ export function close (fd, callback) { } } +/** + * Synchronously close a file descriptor. + * @param {number} fd - fd + */ +export function closeSync (fd) { + const id = fds.get(fd) || fd + const result = ipc.sendSync('fs.close', { id }) + if (result.err) { + throw result.err + } + fds.release(id) +} + /** * Asynchronously copies `src` to `dest` calling `callback` upon success or error. * @param {string} src - The source file path. * @param {string} dest - The destination file path. * @param {number} flags - Modifiers for copy operation. * @param {function(Error=)=} [callback] - The function to call after completion. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscopyfilesrc-dest-mode-callback} + * @see {@link https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback} */ -export function copyFile (src, dest, flags, callback) { +export function copyFile (src, dest, flags = 0, callback) { + src = normalizePath(src) + dest = normalizePath(dest) + if (typeof src !== 'string') { throw new TypeError('The argument \'src\' must be a string') } @@ -201,7 +367,7 @@ export function copyFile (src, dest, flags, callback) { throw new TypeError('The argument \'dest\' must be a string') } - if (!Number.isInteger(flags)) { + if (flags && !Number.isInteger(flags)) { throw new TypeError('The argument \'flags\' must be an integer') } @@ -209,13 +375,54 @@ export function copyFile (src, dest, flags, callback) { throw new TypeError('callback must be a function.') } + if (src instanceof globalThis.FileSystemFileHandle) { + if (src.getFile()[kFileDescriptor]) { + src = src.getFile()[kFileDescriptor].path + } else { + src.getFile().arrayBuffer.then((arrayBuffer) => { + writeFile(dest, arrayBuffer, { flags }, callback) + }) + return + } + } + ipc.request('fs.copyFile', { src, dest, flags }).then((result) => { result?.err ? callback(result.err) : callback(null) }).catch(callback) } /** - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} + * Synchronously copies `src` to `dest` calling `callback` upon success or error. + * @param {string} src - The source file path. + * @param {string} dest - The destination file path. + * @param {number} flags - Modifiers for copy operation. + * @see {@link https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback} + */ +export function copyFileSync (src, dest, flags = 0) { + src = normalizePath(src) + dest = normalizePath(dest) + + if (typeof src !== 'string') { + throw new TypeError('The argument \'src\' must be a string') + } + + if (typeof dest !== 'string') { + throw new TypeError('The argument \'dest\' must be a string') + } + + if (!Number.isInteger(flags)) { + throw new TypeError('The argument \'flags\' must be an integer') + } + + const result = ipc.sendSync('fs.copyFile', { src, dest, flags }) + + if (result.err) { + throw result.err + } +} + +/** + * @see {@link https://nodejs.org/api/fs.html#fscreatewritestreampath-options} * @param {string | Buffer | URL} path * @param {object?} [options] * @returns {ReadStream} @@ -226,6 +433,8 @@ export function createReadStream (path, options) { path = options?.path || null } + path = normalizePath(path) + let handle = null const stream = new ReadStream({ autoClose: typeof options?.fd !== 'number', @@ -258,7 +467,7 @@ export function createReadStream (path, options) { } /** - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} + * @see {@link https://nodejs.org/api/fs.html#fscreatewritestreampath-options} * @param {string | Buffer | URL} path * @param {object?} [options] * @returns {WriteStream} @@ -269,6 +478,14 @@ export function createWriteStream (path, options) { path = options?.path || null } + if (path instanceof globalThis.FileSystemHandle) { + return new TypeError( + 'FileSystemHandle is not writable' + ) + } + + path = normalizePath(path) + let handle = null const stream = new WriteStream({ autoClose: typeof options?.fd !== 'number', @@ -303,7 +520,7 @@ export function createWriteStream (path, options) { * Invokes the callback with the for the file descriptor. See * the POSIX fstat(2) documentation for more detail. * - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsfstatfd-options-callback} + * @see {@link https://nodejs.org/api/fs.html#fsfstatfd-options-callback} * * @param {number} fd - A file descriptor. * @param {object?|function?} [options] - An options object. @@ -387,6 +604,17 @@ export function ftruncate (fd, offset, callback) { * @param {function} callback */ export function lchown (path, uid, gid, callback) { + if ( + path instanceof globalThis.FileSystemFileHandle || + path instanceof globalThis.FileSystemDirectoryHandle + ) { + return new TypeError( + 'FileSystemHandle is not writable' + ) + } + + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -415,6 +643,9 @@ export function lchown (path, uid, gid, callback) { * @param {function} */ export function link (src, dest, callback) { + src = normalizePath(src) + dest = normalizePath(dest) + if (typeof src !== 'string') { throw new TypeError('The argument \'src\' must be a string') } @@ -436,8 +667,11 @@ export function link (src, dest, callback) { * @ignore */ export function mkdir (path, options, callback) { - if ((typeof options === 'undefined') || (typeof options === 'function')) { - throw new TypeError('options must be an object.') + path = normalizePath(path) + + if (typeof options === 'function') { + callback = options + options = null } if (typeof callback !== 'function') { @@ -461,9 +695,35 @@ export function mkdir (path, options, callback) { .catch(err => callback(err)) } +/** + * @ignore + * @param {string|URL} path + * @param {object=} [options] + */ +export function mkdirSync (path, options = null) { + path = normalizePath(path) + + const mode = options?.mode || 0o777 + const recursive = Boolean(options?.recursive) // default to false + + if (typeof mode !== 'number') { + throw new TypeError('mode must be a number.') + } + + if (mode < 0 || !Number.isInteger(mode)) { + throw new RangeError('mode must be a positive finite number.') + } + + const result = ipc.sendSync('fs.mkdir', { mode, path, recursive }) + + if (result.err) { + throw result.err + } +} + /** * Asynchronously open a file calling `callback` upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} + * @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback} * @param {string | Buffer | URL} path * @param {string?} [flags = 'r'] * @param {string?} [mode = 0o666] @@ -505,6 +765,8 @@ export function open (path, flags = 'r', mode = 0o666, options = null, callback) throw new TypeError('callback must be a function.') } + path = normalizePath(path) + FileHandle .open(path, flags, mode, options) .then((handle) => { @@ -514,9 +776,54 @@ export function open (path, flags = 'r', mode = 0o666, options = null, callback) .catch((err) => callback(err)) } +/** + * Synchronously open a file. + * @param {string | Buffer | URL} path + * @param {string?} [flags = 'r'] + * @param {string?} [mode = 0o666] + * @param {object?|function?} [options] + */ +export function openSync (path, flags = 'r', mode = 0o666, options = null) { + if (typeof flags === 'object') { + options = flags + flags = FileHandle.DEFAULT_OPEN_FLAGS + mode = FileHandle.DEFAULT_OPEN_MODE + } + + if (typeof mode === 'object') { + options = mode + flags = FileHandle.DEFAULT_OPEN_FLAGS + mode = FileHandle.DEFAULT_OPEN_MODE + } + + path = normalizePath(path) + + const id = String(options?.id || rand64()) + const result = ipc.sendSync('fs.open', { + id, + mode, + path, + flags + }, { + ...options + }) + + if (result.err) { + throw result.err + } + + if (result.data?.fd) { + fds.set(id, result.data.fd, 'file') + } else { + fds.set(id, id, 'file') + } + + return result.data?.fd || id +} + /** * Asynchronously open a directory calling `callback` upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} * @param {string | Buffer | URL} path * @param {object?|function(Error?, Dir?)} [options] * @param {string?} [options.encoding = 'utf8'] @@ -533,15 +840,43 @@ export function opendir (path, options = {}, callback) { throw new TypeError('callback must be a function.') } + path = normalizePath(path) + DirectoryHandle .open(path, options) .then((handle) => callback(null, new Dir(handle, options))) .catch((err) => callback(err)) } +/** + * Synchronously open a directory. + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} + * @param {string | Buffer | URL} path + * @param {object?|function(Error?, Dir?)} [options] + * @param {string?} [options.encoding = 'utf8'] + * @param {boolean?} [options.withFileTypes = false] + * @return {Dir} + */ +export function opendirSync (path, options = {}) { + path = normalizePath(path) + // @ts-ignore + const id = String(options?.id || rand64()) + const result = ipc.sendSync('fs.opendir', { id, path }, options) + + if (result.err) { + throw result.err + } + + fds.set(id, id, 'directory') + + // @ts-ignore + const handle = new DirectoryHandle({ id, path }) + return new Dir(handle, options) +} + /** * Asynchronously read from an open file descriptor. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback} + * @see {@link https://nodejs.org/api/fs.html#fsreadfd-buffer-offset-length-position-callback} * @param {number} fd * @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to. * @param {number} offset - The position in buffer to write the data to. @@ -574,9 +909,44 @@ export function read (fd, buffer, offset, length, position, options, callback) { } } +/** + * Asynchronously write to an open file descriptor. + * @see {@link https://nodejs.org/api/fs.html#fswritefd-buffer-offset-length-position-callback} + * @param {number} fd + * @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to. + * @param {number} offset - The position in buffer to write the data to. + * @param {number} length - The number of bytes to read. + * @param {number | BigInt | null} position - Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged. + * @param {function(Error?, number?, Buffer?)} callback + */ +export function write (fd, buffer, offset, length, position, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + + if (typeof buffer === 'object' && !isBufferLike(buffer)) { + options = buffer + } + + if (typeof callback !== 'function') { + throw new TypeError('callback must be a function.') + } + + try { + FileHandle + .from(fd) + .write({ ...options, buffer, offset, length, position }) + .then(({ bytesWritten, buffer }) => callback(null, bytesWritten, buffer)) + .catch((err) => callback(err)) + } catch (err) { + callback(err) + } +} + /** * Asynchronously read all entries in a directory. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} * @param {string | Buffer | URL } path * @param {object?|function(Error?, object[])} [options] * @param {string?} [options.encoding ? 'utf8'] @@ -597,6 +967,7 @@ export function readdir (path, options = {}, callback) { throw new TypeError('callback must be a function.') } + path = normalizePath(path) options = { entries: DirectoryHandle.MAX_ENTRIES, withFileTypes: false, @@ -626,6 +997,36 @@ export function readdir (path, options = {}, callback) { .catch((err) => callback(err)) } +/** + * Synchronously read all entries in a directory. + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} + * @param {string | Buffer | URL } path + * @param {object?|function(Error?, object[])} [options] + * @param {string?} [options.encoding ? 'utf8'] + * @param {boolean?} [options.withFileTypes ? false] + */ +export function readdirSync (path, options = {}) { + options = { + entries: DirectoryHandle.MAX_ENTRIES, + withFileTypes: false, + ...options + } + const dir = opendirSync(path, options) + const entries = [] + + do { + const entry = dir.readSync(options) + if (!entry || entry.length === 0) { + break + } + + entries.push(...[].concat(entry)) + } while (true) + + dir.closeSync() + return entries +} + /** * @param {string | Buffer | URL | number } path * @param {object?|function(Error?, Buffer?)} [options] @@ -644,10 +1045,8 @@ export function readFile (path, options = {}, callback) { options = { encoding: options } } - options = { - flags: 'r', - ...options - } + path = normalizePath(path) + options = { flags: 'r', ...options } if (typeof callback !== 'function') { throw new TypeError('callback must be a function.') @@ -672,6 +1071,70 @@ export function readFile (path, options = {}, callback) { }) } +/** + * @param {string|Buffer|URL|number} path + * @param {{ encoding?: string = 'utf8', flags?: string = 'r'}} [options] + * @param {object?|function(Error?, Buffer?)} [options] + * @param {AbortSignal?} [options.signal] + */ +export function readFileSync (path, options = null) { + if (typeof options === 'string') { + options = { encoding: options } + } + + path = normalizePath(path) + options = { + flags: 'r', + encoding: options?.encoding, + ...options + } + + let result = null + + const stats = statSync(path) + const flags = normalizeFlags(options.flags) + const mode = FileHandle.DEFAULT_OPEN_MODE + // @ts-ignore + const id = String(options?.id || rand64()) + + result = ipc.sendSync('fs.open', { + mode, + flags, + id, + path + }, options) + + if (result.err) { + throw result.err + } + + result = ipc.sendSync('fs.read', { + id, + size: stats.size, + offset: 0 + }, { responseType: 'arraybuffer' }) + + if (result.err) { + throw result.err + } + + const data = result.data + + result = ipc.sendSync('fs.close', { id }, options) + + if (result.err) { + throw result.err + } + + const buffer = data ? Buffer.from(data) : Buffer.alloc(0) + + if (typeof options?.encoding === 'string') { + return buffer.toString(options.encoding) + } + + return buffer +} + /** * Reads link at `path` * @param {string} path @@ -686,8 +1149,9 @@ export function readlink (path, callback) { throw new TypeError('callback must be a function.') } + path = normalizePath(path) ipc.request('fs.readlink', { path }).then((result) => { - result?.err ? callback(result.err) : callback(result.data.path) + result?.err ? callback(result.err) : callback(null, result.data.path) }).catch(callback) } @@ -710,6 +1174,22 @@ export function realpath (path, callback) { }).catch(callback) } +/** + * Computes real path for `path` + * @param {string} path + */ +export function realpathSync (path) { + if (typeof path !== 'string') { + throw new TypeError('The argument \'path\' must be a string') + } + + const result = ipc.sendSync('fs.realpath', { path }) + + if (result.err) { + throw result.err + } +} + /** * Renames file or directory at `src` to `dest`. * @param {string} src @@ -717,6 +1197,9 @@ export function realpath (path, callback) { * @param {function} callback */ export function rename (src, dest, callback) { + src = normalizePath(src) + dest = normalizePath(dest) + if (typeof src !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -734,12 +1217,38 @@ export function rename (src, dest, callback) { }) } +/** + * Renames file or directory at `src` to `dest`, synchronously. + * @param {string} src + * @param {string} dest + */ +export function renameSync (src, dest) { + src = normalizePath(src) + dest = normalizePath(dest) + + if (typeof src !== 'string') { + throw new TypeError('The argument \'path\' must be a string') + } + + if (typeof dest !== 'string') { + throw new TypeError('The argument \'dest\' must be a string') + } + + const result = ipc.sendSync('fs.rename', { src, dest }) + + if (result.err) { + throw result.err + } +} + /** * Removes directory at `path`. * @param {string} path * @param {function} callback */ export function rmdir (path, callback) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -754,7 +1263,43 @@ export function rmdir (path, callback) { } /** - * + * Removes directory at `path`, synchronously. + * @param {string} path + */ +export function rmdirSync (path) { + path = normalizePath(path) + + if (typeof path !== 'string') { + throw new TypeError('The argument \'path\' must be a string') + } + + const result = ipc.sendSync('fs.rmdir', { path }) + + if (result.err) { + throw result.err + } +} + +/** + * Synchronously get the stats of a file + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.flag ? 'r'] + */ +export function statSync (path, options = null) { + path = normalizePath(path) + const result = ipc.sendSync('fs.stat', { path }) + + if (result.err) { + throw result.err + } + + return Stats.from(result.data, Boolean(options?.bigint)) +} + +/** + * Get the stats of a file * @param {string | Buffer | URL | number } path - filename or file descriptor * @param {object?} options * @param {string?} [options.encoding ? 'utf8'] @@ -772,6 +1317,40 @@ export function stat (path, options, callback) { throw new TypeError('callback must be a function.') } + if ( + path instanceof globalThis.FileSystemFileHandle || + path instanceof globalThis.FileSystemDirectoryHandle + ) { + if (path.getFile()[kFileDescriptor]) { + try { + path.getFile()[kFileDescriptor].stat(options).then( + (stats) => callback(null, stats), + (err) => callback(err) + ) + } catch (err) { + callback(err) + } + + return + } else { + queueMicrotask(() => { + const info = { + st_mode: path instanceof globalThis.FileSystemDirectoryHandle + ? constants.S_IFDIR + : constants.S_IFREG, + st_size: path instanceof globalThis.FileSystemFileHandle + ? path.size + : 0 + } + + const stats = Stats.from(info, Boolean(options?.bigint)) + callback(null, stats) + }) + + return + } + } + visit(path, {}, async (err, handle) => { let stats = null @@ -791,6 +1370,51 @@ export function stat (path, options, callback) { }) } +/** + * Get the stats of a symbolic link + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.flag ? 'r'] + * @param {AbortSignal?} [options.signal] + * @param {function(Error?, Stats?)} callback + */ +export function lstat (path, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + + if (typeof callback !== 'function') { + throw new TypeError('callback must be a function.') + } + + if ( + path instanceof globalThis.FileSystemFileHandle || + path instanceof globalThis.FileSystemDirectoryHandle + ) { + return stat(path, options, callback) + } + + visit(path, {}, async (err, handle) => { + let stats = null + + if (err) { + callback(err) + return + } + + try { + stats = await handle.lstat(options) + } catch (err) { + callback(err) + return + } + + callback(null, stats) + }) +} + /** * Creates a symlink of `src` at `dest`. * @param {string} src @@ -798,6 +1422,8 @@ export function stat (path, options, callback) { */ export function symlink (src, dest, type = null, callback) { let flags = 0 + src = normalizePath(src) + dest = normalizePath(dest) if (typeof src !== 'string') { throw new TypeError('The argument \'src\' must be a string') @@ -838,6 +1464,8 @@ export function symlink (src, dest, type = null, callback) { * @param {function} callback */ export function unlink (path, callback) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -852,7 +1480,25 @@ export function unlink (path, callback) { } /** - * @see {@url https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fswritefilefile-data-options-callback} + * Unlinks (removes) file at `path`, synchronously. + * @param {string} path + */ +export function unlinkSync (path) { + path = normalizePath(path) + + if (typeof path !== 'string') { + throw new TypeError('The argument \'path\' must be a string') + } + + const result = ipc.sendSync('fs.unlink', { path }) + + if (result.err) { + throw result.err + } +} + +/** + * @see {@url https://nodejs.org/api/fs.html#fswritefilefile-data-options-callback} * @param {string | Buffer | URL | number } path - filename or file descriptor * @param {string | Buffer | TypedArray | DataView | object } data * @param {object?} options @@ -872,11 +1518,8 @@ export function writeFile (path, data, options, callback) { options = { encoding: options } } - options = { - mode: 0o666, - flag: 'w', - ...options - } + path = normalizePath(path) + options = { mode: 0o666, flag: 'w', ...options } if (typeof callback !== 'function') { throw new TypeError('callback must be a function.') @@ -899,6 +1542,44 @@ export function writeFile (path, data, options, callback) { }) } +/** + * Writes data to a file synchronously. + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {string | Buffer | TypedArray | DataView | object } data + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.mode ? 0o666] + * @param {string?} [options.flag ? 'w'] + * @param {AbortSignal?} [options.signal] + * @see {@link https://nodejs.org/api/fs.html#fswritefilesyncfile-data-options} + */ +export function writeFileSync (path, data, options) { + const id = String(options?.id || rand64()) + + let result = ipc.sendSync('fs.open', { + id, + mode: options?.mode || 0o666, + path, + flags: options?.flags ? normalizeFlags(options.flags) : 'w' + }, options) + + if (result.err) { + throw result.err + } + + result = ipc.sendSync('fs.write', { id, offset: 0 }, null, data) + + if (result.err) { + throw result.err + } + + result = ipc.sendSync('fs.close', { id }, options) + + if (result.err) { + throw result.err + } +} + /** * Watch for changes at `path` calling `callback` * @param {string} @@ -912,6 +1593,7 @@ export function watch (path, options, callback = null) { callback = options } + path = normalizePath(path) const watcher = new Watcher(path, options) watcher.on('change', callback) return watcher @@ -919,6 +1601,7 @@ export function watch (path, options, callback = null) { // re-exports export { + bookmarks, constants, Dir, DirectoryHandle, @@ -938,5 +1621,6 @@ for (const key in exports) { const value = exports[key] if (key in promises && isFunction(value) && isFunction(promises[key])) { value[Symbol.for('nodejs.util.promisify.custom')] = promises[key] + value[Symbol.for('socket.runtime.util.promisify.custom')] = promises[key] } } diff --git a/api/fs/promises.js b/api/fs/promises.js index 3d40221406..3de41eaec3 100644 --- a/api/fs/promises.js +++ b/api/fs/promises.js @@ -1,5 +1,5 @@ /** - * @module FS.promises + * @module fs.promises * * * This module enables interacting with the file system in a way modeled on * standard POSIX functions. @@ -22,7 +22,6 @@ * import fs from 'socket:fs/promises' * ``` */ -import console from '../console.js' import ipc from '../ipc.js' import { Dir, Dirent, sortDirectoryEntries } from './dir.js' @@ -31,12 +30,14 @@ import { ReadStream, WriteStream } from './stream.js' import * as constants from './constants.js' import { Watcher } from './watcher.js' import { Stats } from './stats.js' +import bookmarks from './bookmarks.js' import fds from './fds.js' import * as exports from './promises.js' // re-exports export { + bookmarks, constants, Dir, DirectoryHandle, @@ -49,6 +50,27 @@ export { WriteStream } +const kFileDescriptor = Symbol.for('socket.runtune.fs.web.FileDescriptor') + +function normalizePath (path) { + if (path instanceof URL) { + if (path.origin === globalThis.location.origin) { + return normalizePath(path.href) + } + + return null + } + + if (URL.canParse(path)) { + const url = new URL(path) + if (url.origin === globalThis.location.origin) { + path = `./${url.pathname.slice(1)}` + } + } + + return path +} + /** * @typedef {import('../buffer.js').Buffer} Buffer * @typedef {import('.stats.js').Stats} Stats @@ -62,12 +84,15 @@ async function visit (path, options, callback) { } const { flags, flag, mode } = options || {} + path = normalizePath(path) // just visit `FileHandle`, without closing if given if (path instanceof FileHandle) { return await callback(path) } else if (path?.fd) { return await callback(FileHandle.from(path.fd)) + } else if (path && typeof path === 'object' && kFileDescriptor in path) { + return await callback(FileHandle.from(path)) } const handle = await FileHandle.open(path, flags || flag, mode, options) @@ -84,6 +109,7 @@ async function visit (path, options, callback) { * @param {object?} [options] */ export async function access (path, mode, options) { + path = normalizePath(path) return await FileHandle.access(path, mode, options) } @@ -94,6 +120,8 @@ export async function access (path, mode, options) { * @returns {Promise} */ export async function chmod (path, mode) { + path = normalizePath(path) + if (typeof mode !== 'number') { throw new TypeError(`The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received ${mode}`) } @@ -117,6 +145,8 @@ export async function chmod (path, mode) { * @return {Promise} */ export async function chown (path, uid, gid) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -143,7 +173,10 @@ export async function chown (path, uid, gid) { * @param {number} flags - Modifiers for copy operation. * @return {Promise} */ -export async function copyFile (src, dest, flags) { +export async function copyFile (src, dest, flags = 0) { + src = normalizePath(src) + dest = normalizePath(dest) + if (typeof src !== 'string') { throw new TypeError('The argument \'src\' must be a string') } @@ -171,6 +204,8 @@ export async function copyFile (src, dest, flags) { * @return {Promise} */ export async function lchown (path, uid, gid) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'src\' must be a string') } @@ -197,6 +232,9 @@ export async function lchown (path, uid, gid) { * @return {Promise} */ export async function link (src, dest) { + src = normalizePath(src) + dest = normalizePath(dest) + if (typeof src !== 'string') { throw new TypeError('The argument \'src\' must be a string') } @@ -225,6 +263,8 @@ export async function mkdir (path, options = {}) { const mode = options.mode ?? 0o777 const recursive = Boolean(options.recursive) + path = normalizePath(path) + if (typeof mode !== 'number') { throw new TypeError('mode must be a number.') } @@ -250,6 +290,7 @@ export async function mkdir (path, options = {}) { * @return {Promise} */ export async function open (path, flags = 'r', mode = 0o666) { + path = normalizePath(path) return await FileHandle.open(path, flags, mode) } @@ -262,6 +303,7 @@ export async function open (path, flags = 'r', mode = 0o666) { * @return {Promise} */ export async function opendir (path, options) { + path = normalizePath(path) const handle = await DirectoryHandle.open(path, options) return new Dir(handle, options) } @@ -274,7 +316,12 @@ export async function opendir (path, options) { * @param {boolean?} [options.withFileTypes = false] */ export async function readdir (path, options) { - options = { entries: DirectoryHandle.MAX_ENTRIES, ...options } + path = normalizePath(path) + options = { + entries: DirectoryHandle.MAX_ENTRIES, + withFileTypes: false, + ...options + } const entries = [] const handle = await DirectoryHandle.open(path, options) @@ -311,6 +358,7 @@ export async function readFile (path, options) { options = { encoding: options } } + path = normalizePath(path) options = { flags: 'r', ...options } return await visit(path, options, async (handle) => { @@ -324,6 +372,8 @@ export async function readFile (path, options) { * @return {Promise} */ export async function readlink (path) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -343,6 +393,8 @@ export async function readlink (path) { * @return {Promise} */ export async function realpath (path) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -363,6 +415,9 @@ export async function realpath (path) { * @return {Promise} */ export async function rename (src, dest) { + src = normalizePath(src) + dest = normalizePath(dest) + if (typeof src !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -384,6 +439,8 @@ export async function rename (src, dest) { * @return {Promise} */ export async function rmdir (path) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -396,6 +453,7 @@ export async function rmdir (path) { } /** + * Get the stats of a file * @see {@link https://nodejs.org/api/fs.html#fspromisesstatpath-options} * @param {string | Buffer | URL} path * @param {object?} [options] @@ -403,11 +461,27 @@ export async function rmdir (path) { * @return {Promise} */ export async function stat (path, options) { + path = normalizePath(path) return await visit(path, {}, async (handle) => { return await handle.stat(options) }) } +/** + * Get the stats of a symbolic link. + * @see {@link https://nodejs.org/api/fs.html#fspromiseslstatpath-options} + * @param {string | Buffer | URL} path + * @param {object?} [options] + * @param {boolean?} [options.bigint = false] + * @return {Promise} + */ +export async function lstat (path, options) { + path = normalizePath(path) + return await visit(path, {}, async (handle) => { + return await handle.lstat(options) + }) +} + /** * Creates a symlink of `src` at `dest`. * @param {string} src @@ -416,6 +490,7 @@ export async function stat (path, options) { */ export async function symlink (src, dest, type = null) { let flags = 0 + src = normalizePath(dest) if (typeof src !== 'string') { throw new TypeError('The argument \'src\' must be a string') @@ -450,6 +525,8 @@ export async function symlink (src, dest, type = null) { * @return {Promise} */ export async function unlink (path) { + path = normalizePath(path) + if (typeof path !== 'string') { throw new TypeError('The argument \'path\' must be a string') } @@ -477,6 +554,7 @@ export async function writeFile (path, data, options) { options = { encoding: options } } + path = normalizePath(path) options = { flag: 'w', mode: 0o666, ...options } return await visit(path, options, async (handle) => { @@ -493,6 +571,7 @@ export async function writeFile (path, data, options) { * @return {Watcher} */ export function watch (path, options) { + path = normalizePath(path) return new Watcher(path, options) } diff --git a/api/fs/stream.js b/api/fs/stream.js index ff2f3da43c..d3f5516a12 100644 --- a/api/fs/stream.js +++ b/api/fs/stream.js @@ -1,12 +1,12 @@ /** - * @module FS.Stream + * @module fs.stream */ import { Readable, Writable } from '../stream.js' import { AbortError } from '../errors.js' import * as exports from './stream.js' -export const DEFAULT_STREAM_HIGH_WATER_MARK = 64 * 1024 +export const DEFAULT_STREAM_HIGH_WATER_MARK = 2 * 1024 * 1024 /** * @typedef {import('./handle.js').FileHandle} FileHandle @@ -27,7 +27,9 @@ export class ReadStream extends Readable { throw new AbortError(options.signal) } - if (typeof options?.highWaterMark !== 'number') { + if (typeof options?.highWaterMark === 'number') { + this._readableState.highWaterMark = options.highWaterMark + } else { this._readableState.highWaterMark = this.constructor.highWaterMark } @@ -149,9 +151,8 @@ export class ReadStream extends Readable { } if (typeof result.bytesRead === 'number' && result.bytesRead > 0) { - const slice = new Uint8Array(buffer.slice(0, result.bytesRead)) this.bytesRead += result.bytesRead - this.push(slice) + this.push(Buffer.from(buffer.slice(0, result.bytesRead))) if (this.bytesRead >= this.end) { this.push(null) @@ -175,7 +176,9 @@ export class WriteStream extends Writable { constructor (options) { super(options) - if (typeof options?.highWaterMark !== 'number') { + if (typeof options?.highWaterMark === 'number') { + this._writableState.highWaterMark = options.highWaterMark + } else { this._writableState.highWaterMark = this.constructor.highWaterMark } diff --git a/api/fs/watcher.js b/api/fs/watcher.js index fd4fed363c..102b62dd60 100644 --- a/api/fs/watcher.js +++ b/api/fs/watcher.js @@ -1,3 +1,4 @@ +import { AsyncResource } from '../async/resource.js' import { EventEmitter } from '../events.js' import { FileHandle } from './handle.js' import { AbortError } from '../errors.js' @@ -56,7 +57,7 @@ async function start (watcher) { * @param {Watcher} watcher * @return {function} */ -function listen (watcher) { +function listen (watcher, resource) { return hooks.onData((event) => { const { data, source } = event.detail.params if (source !== 'fs.watch') { @@ -69,7 +70,9 @@ function listen (watcher) { const { path, events } = data - watcher.emit('change', events[0], encodeFilename(watcher, path)) + resource.runInAsyncScope(() => { + watcher.emit('change', events[0], encodeFilename(watcher, path)) + }) }) } @@ -121,6 +124,8 @@ export class Watcher extends EventEmitter { */ stopListening = null + #resource = null + /** * `Watcher` class constructor. * @ignore @@ -139,6 +144,9 @@ export class Watcher extends EventEmitter { this.aborted = this.signal?.aborted === true this.encoding = options?.encoding || this.encoding + this.#resource = new AsyncResource('FileSystemWatcher') + this.#resource.handle = this + gc.ref(this) if (this.signal?.aborted) { @@ -174,9 +182,11 @@ export class Watcher extends EventEmitter { this.stopListening() } - this.stopListening = listen(this) + this.stopListening = listen(this, this.#resource) } catch (err) { - this.emit('err', err) + this.#resource.runInAsyncScope(() => { + this.emit('error', err) + }) } } diff --git a/api/fs/web.js b/api/fs/web.js index c63ec8fb42..02cd142b92 100644 --- a/api/fs/web.js +++ b/api/fs/web.js @@ -2,12 +2,14 @@ import { DEFAULT_STREAM_HIGH_WATER_MARK } from './stream.js' import { isBufferLike, toBuffer } from '../util.js' import { NotAllowedError } from '../errors.js' +import { readFileSync } from './index.js' import mime from '../mime.js' import path from '../path.js' import fs from './promises.js' -const kFileSystemHandleFullName = Symbol('kFileSystemHandleFullName') -const kFileFullName = Symbol('kFileFullName') +export const kFileSystemHandleFullName = Symbol.for('socket.runtune.fs.web.FileSystemHandleFullName') +export const kFileDescriptor = Symbol.for('socket.runtune.fs.web.FileDescriptor') +export const kFileFullName = Symbol.for('socket.runtune.fs.web.FileFullName') // @ts-ignore export const File = globalThis.File ?? @@ -21,6 +23,7 @@ export const File = globalThis.File ?? slice () {} async arrayBuffer () {} + async bytes () {} async text () {} stream () {} } @@ -35,7 +38,7 @@ export const FileSystemHandle = globalThis.FileSystemHandle ?? // @ts-ignore export const FileSystemFileHandle = globalThis.FileSystemFileHandle ?? class FileSystemFileHandle extends FileSystemHandle { - getFile () {} + async getFile () {} async createWritable (options = null) {} async createSyncAccessHandle () {} } @@ -102,27 +105,66 @@ export async function createFile (filename, options = null) { const decoder = new TextDecoder() const stats = options?.fd ? await options.fd.stat() : await fs.stat(filename) - const types = await mime.lookup(path.extname(filename).slice(1)) + const types = await mime.lookup( + URL.canParse(filename) + ? filename + : path.extname(filename).slice(1) + ) const type = types[0]?.mime ?? '' const highWaterMark = Number.isFinite(options?.highWaterMark) ? options.highWaterMark : Math.min(stats.size, DEFAULT_STREAM_HIGH_WATER_MARK) + let fd = options?.fd ?? null + let blobBuffer = null + let bytesBuffer = null + + const name = URL.canParse(filename) + ? filename + : path.basename(filename) + + if (!fd) { + fd = await fs.open(filename) + } + return create(File, class File { get [kFileFullName] () { return filename } + get [kFileDescriptor] () { return fd } get lastModifiedDate () { return new Date(stats.mtimeMs) } get lastModified () { return stats.mtimeMs } - get name () { return path.basename(filename) } + get name () { return name } get size () { return stats.size } get type () { return type } - slice () { - console.warn('socket:fs/web: File.slice() is not supported in implementation') - // This synchronous interface is not supported - // An empty and ephemeral `Blob` is returned instead - return new Blob([], { type }) + slice (start = 0, end = stats.size, contentType = type) { + if (!blobBuffer) { + blobBuffer = readFileSync(filename) + } + + const blob = new Blob([blobBuffer.buffer], { + type: contentType + }) + + if (start < 0) { + start = stats.size - start + } + + if (end < 0) { + end = stats.size - end + } + + return blob.slice(start, end) + } + + async bytes () { + if (!bytesBuffer) { + bytesBuffer = await this.arrayBuffer() + blobBuffer = bytesBuffer + } + + return new Uint8Array(bytesBuffer) } async arrayBuffer () { @@ -151,16 +193,14 @@ export async function createFile (filename, options = null) { stream () { let buffer = null let offset = 0 - let fd = null const stream = new ReadableStream({ async start (controller) { - fd = options?.fd || await fs.open(filename) + const fd = await fs.open(filename) fd.once('close', () => controller.close()) if (highWaterMark === 0) { await fd.close() await controller.close() - fd = null return } @@ -240,7 +280,7 @@ export async function createFileSystemWritableFileStream (handle, options) { console.warn('socket:fs/web: Missing platform \'FileSystemWritableFileStream\' implementation') } - const file = handle.getFile() + const file = await handle.getFile() let offset = 0 let fd = null @@ -256,6 +296,8 @@ export async function createFileSystemWritableFileStream (handle, options) { // @ts-ignore return create(FileSystemWritableFileStream, class FileSystemWritableFileStream { + get [kFileDescriptor] () { return fd } + async seek (position) { offset = position } @@ -333,14 +375,9 @@ export async function createFileSystemFileHandle (file, options = null) { console.warn('socket:fs/web: Missing platform \'FileSystemFileHandle\' implementation') } - if (typeof file === 'string') { - file = await createFile(file) - } - return create(FileSystemFileHandle, class FileSystemFileHandle { - get [kFileSystemHandleFullName] () { - return file[kFileFullName] - } + get [kFileSystemHandleFullName] () { return file[kFileFullName] } + get [kFileDescriptor] () { return file[kFileDescriptor] } get name () { return file.name @@ -350,7 +387,11 @@ export async function createFileSystemFileHandle (file, options = null) { return 'file' } - getFile () { + async getFile () { + if (typeof file === 'string') { + file = await createFile(file) + } + return file } @@ -376,10 +417,14 @@ export async function createFileSystemFileHandle (file, options = null) { } async move (nameOrDestinationHandle, name = null) { - if (writable === false) { + if (writable === false || URL.canParse(file?.name ?? file)) { throw new NotAllowedError('FileSystemFileHandle is in \'readonly\' mode') } + if (typeof file === 'string') { + file = await createFile(file) + } + let destination = null if (typeof nameOrDestinationHandle === 'string') { name = nameOrDestinationHandle @@ -402,10 +447,14 @@ export async function createFileSystemFileHandle (file, options = null) { } async createWritable (options = null) { - if (writable === false) { + if (writable === false || URL.canParse(file?.name ?? file)) { throw new NotAllowedError('FileSystemFileHandle is in \'readonly\' mode') } + if (typeof file === 'string') { + file = await createFile(file) + } + return await createFileSystemWritableFileStream(this, options) } }) @@ -446,10 +495,13 @@ export async function createFileSystemDirectoryHandle (dirname, options = null) // `fd` is opened with `lazyOpen` at on demand let fd = null + if (options?.open === true) { + await lazyOpen() + } + return create(FileSystemDirectoryHandle, class FileSystemDirectoryHandle { - get [kFileSystemHandleFullName] () { - return dirname - } + get [kFileSystemHandleFullName] () { return dirname } + get [kFileDescriptor] () { return fd } get name () { return path.basename(dirname) diff --git a/api/gc.js b/api/gc.js index a4cb325b36..12829821ab 100644 --- a/api/gc.js +++ b/api/gc.js @@ -1,7 +1,5 @@ import { FinalizationRegistryCallbackError } from './errors.js' -import diagnostics from './diagnostics.js' -import { noop } from './util.js' -import console from './console.js' +import symbols from './internal/symbols.js' if (typeof FinalizationRegistry === 'undefined') { console.warn( @@ -10,16 +8,12 @@ if (typeof FinalizationRegistry === 'undefined') { ) } -const dc = diagnostics.channels.group('gc', [ - 'finalizer.start', - 'finalizer.end', - 'unref', - 'ref' -]) - export const finalizers = new WeakMap() -export const kFinalizer = Symbol.for('gc.finalizer') +export const kFinalizer = Symbol.for('socket.runtime.gc.finalizer') export const finalizer = kFinalizer +/** + * @type {Set} + */ export const pool = new Set() /** @@ -58,7 +52,7 @@ export default gc * @param {Finalizer} finalizer */ async function finalizationRegistryCallback (finalizer) { - dc.channel('finalizer.start').publish({ finalizer }) + if (!finalizer) return // potentially called when finalizer is already gone if (typeof finalizer.handle === 'function') { try { @@ -68,7 +62,9 @@ async function finalizationRegistryCallback (finalizer) { cause: e }) + // @ts-ignore if (typeof Error.captureStackTrace === 'function') { + // @ts-ignore Error.captureStackTrace(err, finalizationRegistryCallback) } @@ -87,7 +83,6 @@ async function finalizationRegistryCallback (finalizer) { pool.delete(weakRef) } - dc.channel('finalizer.end').publish({ finalizer }) finalizer = undefined } @@ -97,6 +92,9 @@ async function finalizationRegistryCallback (finalizer) { * garbage collected. */ export class Finalizer { + args = [] + handle = null + /** * Creates a `Finalizer` from input. */ @@ -108,7 +106,7 @@ export class Finalizer { let { handle, args } = handler if (typeof handle !== 'function') { - handle = noop + handle = () => {} } if (!Array.isArray(args)) { @@ -131,29 +129,35 @@ export class Finalizer { } /** - * Track `object` ref to call `Symbol.for('gc.finalize')` method when + * Track `object` ref to call `Symbol.for('socket.runtime.gc.finalize')` method when * environment garbage collects object. * @param {object} object * @return {boolean} */ export async function ref (object, ...args) { - if (object && typeof object[kFinalizer] === 'function') { - const finalizer = Finalizer.from(await object[kFinalizer](...args)) + if ( + object && ( + typeof object[kFinalizer] === 'function' || + typeof object[symbols.dispose] === 'function' + ) + ) { + const finalizer = Finalizer.from( + await (object[kFinalizer] || object[symbols.dispose]).call(object, ...args) + ) + const weakRef = new WeakRef(finalizer) finalizers.set(object, weakRef) pool.add(weakRef) registry.register(object, finalizer, object) - - dc.channel('ref').publish({ object, finalizer: weakRef }) } return finalizers.has(object) } /** - * Stop tracking `object` ref to call `Symbol.for('gc.finalize')` method when + * Stop tracking `object` ref to call `Symbol.for('socket.runtime.gc.finalize')` method when * environment garbage collects object. * @param {object} object * @return {boolean} @@ -163,7 +167,12 @@ export function unref (object) { return false } - if (typeof object[kFinalizer] === 'function' && finalizers.has(object)) { + if ( + finalizers.has(object) && ( + typeof object[kFinalizer] === 'function' || + typeof object[symbols.dispose] === 'function' + ) + ) { const weakRef = finalizers.get(object) if (weakRef) { @@ -172,7 +181,6 @@ export function unref (object) { finalizers.delete(object) registry.unregister(object) - dc.channel('unref').publish({ object, finalizer: weakRef }) return true } @@ -202,7 +210,10 @@ export async function finalize (object, ...args) { if (finalizer instanceof Finalizer && await unref(object)) { await finalizationRegistryCallback(finalizer) } else { - const finalizer = Finalizer.from(await object[kFinalizer](...args)) + const finalizer = Finalizer.from( + await (object[kFinalizer] || object[symbols.dispose]).call(object, ...args) + ) + await finalizationRegistryCallback(finalizer) } return true @@ -218,6 +229,7 @@ export async function finalize (object, ...args) { */ export async function release () { for (const weakRef of pool) { + // @ts-ignore await finalizationRegistryCallback(weakRef?.deref?.()) pool.delete(weakRef) } diff --git a/api/hooks.js b/api/hooks.js index 7b99a49c50..42e504725b 100644 --- a/api/hooks.js +++ b/api/hooks.js @@ -62,6 +62,14 @@ * hooks.onApplicationURL((event) => { * // called when 'applicationurl' events are dispatched on the global object * }) + * + * hooks.onApplicationResume((event) => { + * // called when 'applicationresume' events are dispatched on the global object + * }) + * + * hooks.onApplicationPause((event) => { + * // called when 'applicationpause' events are dispatched on the global object + * }) * ``` */ import { Event, CustomEvent, ErrorEvent, MessageEvent } from './events.js' @@ -72,29 +80,25 @@ import location from './location.js' * @typedef {{ signal?: AbortSignal }} WaitOptions */ -// primordial setup -const EventTargetPrototype = { - addEventListener: Function.prototype.call.bind(EventTarget.prototype.addEventListener), - removeEventListener: Function.prototype.call.bind(EventTarget.prototype.removeEventListener), - dispatchEvent: Function.prototype.call.bind(EventTarget.prototype.dispatchEvent) -} - -function addEventListener (target, type, callback) { - EventTargetPrototype.addEventListener(target, type, callback) +function addEventListener (target, type, callback, ...args) { + target.addEventListener(type, callback, ...args) } function addEventListenerOnce (target, type, callback) { - EventTargetPrototype.addEventListener(target, type, callback, { once: true }) + target.addEventListener(type, callback, { once: true }) } -async function waitForEvent (target, type) { +async function waitForEvent (target, type, timeout = -1) { return await new Promise((resolve) => { + if (timeout > -1) { + setTimeout(resolve, timeout) + } addEventListenerOnce(target, type, resolve) }) } function dispatchEvent (target, event) { - queueMicrotask(() => EventTargetPrototype.dispatchEvent(target, event)) + queueMicrotask(() => target.dispatchEvent(event)) } function dispatchInitEvent (target) { @@ -111,23 +115,35 @@ function dispatchReadyEvent (target) { function proxyGlobalEvents (global, target) { for (const type of GLOBAL_EVENTS) { - addEventListener(global, type, (event) => { + const globalObject = GLOBAL_TOP_LEVEL_EVENTS.includes(type) + ? global.top ?? global + : global + + addEventListener(globalObject, type, (event) => { const { type, data, detail = null, error } = event const { origin } = location + if (type === 'applicationurl') { dispatchEvent(target, new ApplicationURLEvent(type, { + origin, data: event.data, url: event.url.toString() })) - } else if (error) { - const { message, filename = import.meta.url || globalThis.location.href } = error - dispatchEvent(target, new ErrorEvent(type, { message, filename, error, detail })) - } else if (type && data) { - dispatchEvent(target, new MessageEvent(type, { origin, data, detail })) + } else if (type === 'error' || error) { + const { message, filename = import.meta.url || globalThis.location.href } = error || {} + dispatchEvent(target, new ErrorEvent(type, { + message, + filename, + error, + detail, + origin + })) + } else if (data || type === 'message') { + dispatchEvent(target, new MessageEvent(type, event)) } else if (detail) { - dispatchEvent(target, new CustomEvent(type, { detail })) + dispatchEvent(target, new CustomEvent(type, event)) } else { - dispatchEvent(target, new Event(type)) + dispatchEvent(target, new Event(type, event)) } }) } @@ -135,13 +151,14 @@ function proxyGlobalEvents (global, target) { // state let isGlobalLoaded = false -let isRuntimeInitialized = false export const RUNTIME_INIT_EVENT_NAME = '__runtime_init__' export const GLOBAL_EVENTS = [ RUNTIME_INIT_EVENT_NAME, 'applicationurl', + 'applicationpause', + 'applicationresume', 'data', 'error', 'init', @@ -157,6 +174,20 @@ export const GLOBAL_EVENTS = [ 'unhandledrejection' ] +const GLOBAL_TOP_LEVEL_EVENTS = [ + 'applicationurl', + 'applicationpause', + 'applicationresume', + 'data', + 'languagechange', + 'notificationpresented', + 'notificationresponse', + 'offline', + 'online', + 'permissionchange', + 'unhandledrejection' +] + /** * An event dispatched when the runtime has been initialized. */ @@ -237,7 +268,7 @@ export class Hooks extends EventTarget { * @type {object} */ get global () { - return globalThis || new EventTarget() + return globalThis } /** @@ -277,7 +308,7 @@ export class Hooks extends EventTarget { * @type {boolean} */ get isRuntimeReady () { - return isRuntimeInitialized + return Boolean(globalThis.__RUNTIME_INIT_NOW__) } /** @@ -325,14 +356,12 @@ export class Hooks extends EventTarget { const { isWorkerContext, document, global } = this const readyState = document?.readyState - isRuntimeInitialized = Boolean(global.__RUNTIME_INIT_NOW__) - proxyGlobalEvents(global, this) // if runtime is initialized, then 'DOMContentLoaded' (document), // 'load' (window), and the 'init' (window) events have all been dispatched // prior to hook initialization - if (isRuntimeInitialized) { + if (this.isRuntimeReady) { dispatchLoadEvent(this) dispatchInitEvent(this) dispatchReadyEvent(this) @@ -340,13 +369,18 @@ export class Hooks extends EventTarget { } addEventListenerOnce(global, RUNTIME_INIT_EVENT_NAME, () => { - isRuntimeInitialized = true dispatchInitEvent(this) dispatchReadyEvent(this) }) if (!isWorkerContext && readyState !== 'complete') { - await waitForEvent(global, 'load') + const pending = [] + pending.push(waitForEvent(global, 'load', 500)) + if (document) { + pending.push(waitForEvent(document, 'DOMContentLoaded')) + } + + await Promise.race(pending) } isGlobalLoaded = true @@ -557,6 +591,26 @@ export class Hooks extends EventTarget { this.addEventListener('applicationurl', callback) return () => this.removeEventListener('applicationurl', callback) } + + /** + * Calls callback when an `ApplicationPause` is dispatched. + * @param {function} callback + * @return {function} + */ + onApplicationPause (callback) { + this.addEventListener('applicationpause', callback) + return () => this.removeEventListener('applicationpause', callback) + } + + /** + * Calls callback when an `ApplicationResume` is dispatched. + * @param {function} callback + * @return {function} + */ + onApplicationResume (callback) { + this.addEventListener('applicationresume', callback) + return () => this.removeEventListener('applicationresume', callback) + } } /** @@ -698,4 +752,22 @@ export function onApplicationURL (callback) { return hooks.onApplicationURL(callback) } +/** + * Calls callback when a `ApplicationPause` is dispatched. + * @param {function} callback + * @return {function} + */ +export function onApplicationPause (callback) { + return hooks.onApplicationPause(callback) +} + +/** + * Calls callback when a `ApplicationResume` is dispatched. + * @param {function} callback + * @return {function} + */ +export function onApplicationResume (callback) { + return hooks.onApplicationResume(callback) +} + export default hooks diff --git a/api/http.js b/api/http.js new file mode 100644 index 0000000000..5b36922956 --- /dev/null +++ b/api/http.js @@ -0,0 +1,1901 @@ +import { Writable, Readable, Duplex } from './stream.js' +import { AsyncResource } from './async/resource.js' +import { AsyncContext } from './async/context.js' +import { EventEmitter } from './events.js' +import { toProperCase } from './util.js' +import { Buffer } from './buffer.js' +import location from './location.js' +import adapters from './http/adapters.js' +import gc from './gc.js' + +// re-export +import * as exports from './http.js' + +/** + * All known possible HTTP methods. + * @type {string[]} + */ +export const METHODS = [ + 'ACL', + 'BIND', + 'CHECKOUT', + 'CONNECT', + 'COPY', + 'DELETE', + 'GET', + 'HEAD', + 'LINK', + 'LOCK', + 'M-SEARCH', + 'MERGE', + 'MKACTIVITY', + 'MKCALENDAR', + 'MKCOL', + 'MOVE', + 'NOTIFY', + 'OPTIONS', + 'PATCH', + 'POST', + 'PROPFIND', + 'PROPPATCH', + 'PURGE', + 'PUT', + 'QUERY', + 'REBIND', + 'REPORT', + 'SEARCH', + 'SOURCE', + 'SUBSCRIBE', + 'TRACE', + 'UNBIND', + 'UNLINK', + 'UNLOCK', + 'UNSUBSCRIBE' +] + +/** + * A mapping of status codes to status texts + * @type {object} + */ +export const STATUS_CODES = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 103: 'Early Hints', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 208: 'Already Reported', + 226: 'IM Used', + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Payload Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: "I'm a Teapot", + 421: 'Misdirected Request', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 509: 'Bandwidth Limit Exceeded', + 510: 'Not Extended', + 511: 'Network Authentication Required' +} + +export const CONTINUE = 100 +export const SWITCHING_PROTOCOLS = 101 +export const PROCESSING = 102 +export const EARLY_HINTS = 103 +export const OK = 200 +export const CREATED = 201 +export const ACCEPTED = 202 +export const NONAUTHORITATIVE_INFORMATION = 203 +export const NO_CONTENT = 204 +export const RESET_CONTENT = 205 +export const PARTIAL_CONTENT = 206 +export const MULTISTATUS = 207 +export const ALREADY_REPORTED = 208 +export const IM_USED = 226 +export const MULTIPLE_CHOICES = 300 +export const MOVED_PERMANENTLY = 301 +export const FOUND = 302 +export const SEE_OTHER = 303 +export const NOT_MODIFIED = 304 +export const USE_PROXY = 305 +export const TEMPORARY_REDIRECT = 307 +export const PERMANENT_REDIRECT = 308 +export const BAD_REQUEST = 400 +export const UNAUTHORIZED = 401 +export const PAYMENT_REQUIRED = 402 +export const FORBIDDEN = 403 +export const NOT_FOUND = 404 +export const METHOD_NOT_ALLOWED = 405 +export const NOT_ACCEPTABLE = 406 +export const PROXY_AUTHENTICATION_REQUIRED = 407 +export const REQUEST_TIMEOUT = 408 +export const CONFLICT = 409 +export const GONE = 410 +export const LENGTH_REQUIRED = 411 +export const PRECONDITION_FAILED = 412 +export const PAYLOAD_TOO_LARGE = 413 +export const URI_TOO_LONG = 414 +export const UNSUPPORTED_MEDIA_TYPE = 415 +export const RANGE_NOT_SATISFIABLE = 416 +export const EXPECTATION_FAILED = 417 +export const IM_A_TEAPOT = 418 +export const MISDIRECTED_REQUEST = 421 +export const UNPROCESSABLE_ENTITY = 422 +export const LOCKED = 423 +export const FAILED_DEPENDENCY = 424 +export const TOO_EARLY = 425 +export const UPGRADE_REQUIRED = 426 +export const PRECONDITION_REQUIRED = 428 +export const TOO_MANY_REQUESTS = 429 +export const REQUEST_HEADER_FIELDS_TOO_LARGE = 431 +export const UNAVAILABLE_FOR_LEGAL_REASONS = 451 +export const INTERNAL_SERVER_ERROR = 500 +export const NOT_IMPLEMENTED = 501 +export const BAD_GATEWAY = 502 +export const SERVICE_UNAVAILABLE = 503 +export const GATEWAY_TIMEOUT = 504 +export const HTTP_VERSION_NOT_SUPPORTED = 505 +export const VARIANT_ALSO_NEGOTIATES = 506 +export const INSUFFICIENT_STORAGE = 507 +export const LOOP_DETECTED = 508 +export const BANDWIDTH_LIMIT_EXCEEDED = 509 +export const NOT_EXTENDED = 510 +export const NETWORK_AUTHENTICATION_REQUIRED = 511 + +/** + * The parent class of `ClientRequest` and `ServerResponse`. + * It is an abstract outgoing message from the perspective of the + * participants of an HTTP transaction. + * @see {@link https://nodejs.org/api/http.html#class-httpoutgoingmessage} + */ +export class OutgoingMessage extends Writable { + #headers = new Headers() + #timeout = null + #finished = false + #buffers = [] + + /** + * `true` if the headers were sent + * @type {boolean} + */ + headersSent = false + + /** + * `OutgoingMessage` class constructor. + * @ignore + */ + constructor () { + super({ + write: (data, callback) => { + this.emit('writebuffer') + this.buffers.push(Buffer.from(data)) + callback(null) + } + }) + + this.once('finish', () => { + this.#finished = true + if (this.#timeout) { + clearTimeout(this.#timeout) + } + }) + } + + /** + * Internal buffers + * @ignore + * @type {Buffer[]} + */ + get buffers () { + return this.#buffers + } + + /** + * An object of the outgoing message headers. + * This is equivalent to `getHeaders()` + * @type {object} + */ + get headers () { + return Object.fromEntries(this.#headers.entries()) + } + + /** + * @ignore + */ + get socket () { + return this + } + + /** + * `true` if the write state is "ended" + * @type {boolean} + */ + get writableEnded () { + return this._writableState?.ended === true + } + + /** + * `true` if the write state is "finished" + * @type {boolean} + */ + get writableFinished () { + return this.#finished + } + + /** + * The number of buffered bytes. + * @type {number} + */ + get writableLength () { + return this._writableState.buffered + } + + /** + * @ignore + * @type {boolean} + */ + get writableObjectMode () { + return false + } + + /** + * @ignore + */ + get writableCorked () { + return 0 + } + + /** + * The `highWaterMark` of the writable stream. + * @type {number} + */ + get writableHighWaterMark () { + return this._writableState.highWaterMark + } + + /** + * @ignore + * @return {OutgoingMessage} + */ + addTrailers (headers) { + // not supported + return this + } + + /** + * @ignore + * @return {OutgoingMessage} + */ + cork () { + // not supported + return this + } + + /** + * @ignore + * @return {OutgoingMessage} + */ + uncork () { + // not supported + return this + } + + /** + * Destroys the message. + * Once a socket is associated with the message and is connected, + * that socket will be destroyed as well. + * @param {Error?} [err] + * @return {OutgoingMessage} + */ + destroy (err = null) { + super.destroy(err) + return this + } + + /** + * Finishes the outgoing message. + * @param {(Buffer|Uint8Array|string|function)=} [chunk] + * @param {(string|function)=} [encoding] + * @param {function=} [callback] + * @return {OutgoingMessage} + */ + end (chunk = null, encoding = null, callback = null) { + if (typeof chunk === 'function') { + callback = chunk + chunk = null + encoding = null + } else if (typeof encoding === 'function') { + callback = encoding + encoding = null + } + + if (typeof callback === 'function') { + this.once('finish', callback) + } + + if (chunk !== null) { + this.write(chunk) + } + + this.emit('prefinish') + + super.end(null) + return this + } + + /** + * Append a single header value for the header object. + * @param {string} name + * @param {string|string[]} value + * @return {OutgoingMessage} + */ + appendHeader (name, value) { + if (name && typeof name === 'string') { + if (Array.isArray(value)) { + for (const v of value) { + this.#headers.append(name.toLowerCase(), v) + } + } else { + this.#headers.append(name.toLowerCase(), value) + } + } + return this + } + + /** + * Append a single header value for the header object. + * @param {string} name + * @param {string} value + * @return {OutgoingMessage} + */ + setHeader (name, value) { + if (name && typeof name === 'string') { + this.#headers.set(name.toLowerCase(), value) + } + return this + } + + /** + * Flushes the message headers. + */ + flushHeaders () { + queueMicrotask(() => this.emit('flushheaders')) + } + + /** + * Gets the value of the HTTP header with the given name. + * If that header is not set, the returned value will be `undefined`. + * @param {string} + * @return {string|undefined} + */ + getHeader (name) { + if (name && typeof name === 'string') { + return this.#headers.get(name.toLowerCase()) ?? undefined + } + + return undefined + } + + /** + * Returns an array containing the unique names of the current outgoing + * headers. All names are lowercase. + * @return {string[]} + */ + getHeaderNames () { + return Array.from(this.#headers.keys()) + } + + /** + * @ignore + */ + getRawHeaderNames () { + return this.getHeaderNames() + .map((name) => name.split('-').map(toProperCase).join('-')) + } + + /** + * Returns a copy of the HTTP headers as an object. + * @return {object} + */ + getHeaders () { + return Object.fromEntries(this.#headers.entries()) + } + + /** + * Returns true if the header identified by name is currently set in the + * outgoing headers. The header name is case-insensitive. + * @param {string} name + * @return {boolean} + */ + hasHeader (name) { + if (name && typeof name === 'string') { + return this.#headers.has(name.toLowerCase()) + } + + return false + } + + /** + * Removes a header that is queued for implicit sending. + * @param {string} name + */ + removeHeader (name) { + if (name && typeof name === 'string') { + this.#headers.delete(name.toLowerCase()) + } + } + + /** + * Sets the outgoing message timeout with an optional callback. + * @param {number} timeout + * @param {function=} [callback] + * @return {OutgoingMessage} + */ + setTimeout (timeout, callback = null) { + if (!timeout || !Number.isFinite(timeout)) { + throw new TypeError('Expecting a finite integer for a timeout') + } + + if (typeof callback === 'function') { + this.once('timeout', callback) + } + + this.#timeout = setTimeout(() => { + this.emit('timeout') + }, timeout) + + return this + } + + /** + * @ignore + */ + _implicitHeader () { + throw new TypeError('_implicitHeader is not implemented') + } +} + +/** + * An `IncomingMessage` object is created by `Server` or `ClientRequest` and + * passed as the first argument to the 'request' and 'response' event + * respectively. + * It may be used to access response status, headers, and data. + * @see {@link https://nodejs.org/api/http.html#class-httpincomingmessage} + */ +export class IncomingMessage extends Readable { + #httpVersionMajor = 1 + #httpVersionMinor = 1 + #statusMessage = null + #statusCode = 0 + #complete = false + #context = new AsyncContext.Variable() + #headers = {} + #timeout = null + #method = 'GET' + #server = null + #url = null + + /** + * `IncomingMessage` class constructor. + * @ignore + * @param {object} options + */ + constructor (options) { + super() + + this.#server = options?.server ?? null + + if (options?.headers && typeof options?.headers === 'object') { + const { headers } = options + if (Array.isArray(headers)) { + for (const entry of headers) { + if (typeof entry === 'string') { + const index = entry.indexOf(':') + if (index >= 0) { + const [key, value] = [ + entry.slice(0, index + 1), + entry.slice(index + 1) + ] + + if (key && value) { + this.#headers[key.toLowerCase()] = value + } + } + } else if (Array.isArray(entry) && entry.length === 2) { + const [key, value] = entry + if ( + (key && typeof key === 'string') && + (value && typeof value === 'string') + ) { + this.#headers[key.toLowerCase()] = value + } + } + } + } else { + const entries = typeof headers.entries === 'function' + ? headers.entries() + : Object.entries(headers) + + for (const [key, value] of entries) { + if (key && value && typeof value === 'string') { + this.#headers[key.toLowerCase()] = value + } + } + } + } + + // let construction decide this + if (options?.complete === true) { + this.#complete = true + } else { + this.once('complete', () => { + this.#complete = true + clearTimeout(this.#timeout) + }) + } + + if (options?.method && METHODS.includes(options.method)) { + this.#method = options.method + } + + if ( + options?.statusCode && + Number.isFinite(options.statusCode) && + STATUS_CODES[options.statusCode] + ) { + this.#statusCode = options.statusCode + this.#statusMessage = ( + options.statusMessage ?? + STATUS_CODES[options.statusCode] + ) + } + + if (options?.url) { + this.url = options.url + } + } + + /** + * @type {Server} + */ + get server () { + return this.#server + } + + /** + * @type {AsyncContext.Variable} + */ + get context () { + return this.#context + } + + /** + * This property will be `true` if a complete HTTP message has been received + * and successfully parsed. + * @type {boolean} + */ + get complete () { + return this.#complete + } + + /** + * An object of the incoming message headers. + * @type {object} + */ + get headers () { + return this.#headers + } + + /** + * The URL for this incoming message. This value is not absolute with + * respect to the protocol and hostname. It includes the path and search + * query component parameters. + * @type {string} + */ + get url () { return this.#url } + set url (url) { + if (typeof url === 'string') { + if (URL.canParse(url)) { + url = new URL(url) + } + } + + if (url instanceof URL) { + const { hostname, pathname, search } = url + this.#url = `${pathname}${search}` + this.#headers.host = hostname + } else if (typeof url === 'string') { + if (!url.startsWith('/')) { + url = `/${url}` + } + + this.#url = url + } else { + throw new TypeError('Invalid URL given') + } + } + + /** + * Similar to `message.headers`, but there is no join logic and the values + * are always arrays of strings, even for headers received just once. + * @type {object} + */ + get headersDistinct () { + const headers = {} + for (const key in this.#headers) { + headers[key] = this.#headers[key].split(',') + } + return headers + } + + /** + * The HTTP major version of this request. + * @type {number} + */ + get httpVersionMajor () { + return this.#httpVersionMajor + } + + /** + * The HTTP minor version of this request. + * @type {number} + */ + get httpVersionMinor () { + return this.#httpVersionMinor + } + + /** + * The HTTP version string. + * A concatenation of `httpVersionMajor` and `httpVersionMinor`. + * @type {string} + */ + get httpVersion () { + return `${this.httpVersionMajor}.${this.httpVersionMinor}` + } + + /** + * The HTTP request method. + * @type {string} + */ + get method () { + return this.#method + } + + /** + * The raw request/response headers list potentially as they were received. + * @type {string[]} + */ + get rawHeaders () { + return Array.from(Object.entries(this.#headers)).reduce((h, e) => h.concat(e), []) + } + + /** + * @ignore + */ + get rawTrailers () { + // not supported + return [] + } + + /** + * @ignore + */ + get socket () { + return this + } + + /** + * The HTTP request status code. + * Only valid for response obtained from `ClientRequest`. + * @type {number} + */ + get statusCode () { + return this.#statusCode + } + + /** + * The HTTP response status message (reason phrase). + * Such as "OK" or "Internal Server Error." + * Only valid for response obtained from `ClientRequest`. + * @type {string?} + */ + get statusMessage () { + return this.#statusMessage + } + + /** + * An alias for `statusCode` + * @type {number} + */ + get status () { + return this.#statusCode + } + + /** + * An alias for `statusMessage` + * @type {string?} + */ + get statusText () { + return this.#statusMessage + } + + /** + * @ignore + */ + get trailers () { + // not supported + return {} + } + + /** + * @ignore + */ + get trailersDistinct () { + // not supported + return {} + } + + /** + * Gets the value of the HTTP header with the given name. + * If that header is not set, the returned value will be `undefined`. + * @param {string} + * @return {string|undefined} + */ + getHeader (name) { + if (name && typeof name === 'string') { + return this.#headers[name.toLowerCase()] ?? undefined + } + + return undefined + } + + /** + * Returns an array containing the unique names of the current outgoing + * headers. All names are lowercase. + * @return {string[]} + */ + getHeaderNames () { + return Array.from(Object.keys(this.#headers)) + } + + /** + * @ignore + */ + getRawHeaderNames () { + return this.getHeaderNames() + .map((name) => name.split('-').map(toProperCase).join('-')) + } + + /** + * Returns a copy of the HTTP headers as an object. + * @return {object} + */ + getHeaders () { + return Array.from(Object.entries(this.#headers)) + } + + /** + * Returns true if the header identified by name is currently set in the + * outgoing headers. The header name is case-insensitive. + * @param {string} name + * @return {boolean} + */ + hasHeader (name) { + if (name && typeof name === 'string') { + const value = this.#headers[name.toLowerCase()] + return value && typeof value === 'string' + } + + return false + } + + /** + * Sets the incoming message timeout with an optional callback. + * @param {number} timeout + * @param {function=} [callback] + * @return {IncomingMessage} + */ + setTimeout (timeout, callback = null) { + if (!timeout || !Number.isFinite(timeout)) { + throw new TypeError('Expecting a finite integer for a timeout') + } + + if (this.complete) { + return this + } + + if (typeof callback === 'function') { + this.once('timeout', callback) + } + + this.#timeout = setTimeout(() => { + this.emit('timeout') + }, timeout) + + return this + } +} + +/** + * An object that is created internally and returned from `request()`. + * @see {@link https://nodejs.org/api/http.html#class-httpclientrequest} + */ +export class ClientRequest extends OutgoingMessage { + #method = null + #agent = null + #url = null + + #maxHeadersCount = 2000 + + /** + * `ClientRequest` class constructor. + * @ignore + * @param {object} options + */ + constructor (options) { + super() + + this.#agent = options?.agent ?? null + + if (this.#agent) { + this.#agent.requests.add(this) + this.once('finish', () => { + this.#agent.requests.delete(this) + }) + } + + if (options?.method && METHODS.includes(options.method)) { + this.#method = options.method + } + + if (options?.url) { + let url = options.url + if (typeof url === 'string') { + if (URL.canParse(url)) { + url = new URL(url) + } else if (URL.canParse(url, location.origin)) { + url = new URL(url, location.origin) + } else { + url = null + } + } + + if (url instanceof URL) { + const { hostname, pathname, search } = url + this.#url = `${pathname}${search}` + this.setHeader('host', hostname) + } else { + throw new TypeError('Invalid URL given') + } + } + } + + /** + * The HTTP request method. + * @type {string} + */ + get method () { + return this.#method + } + + /** + * The request protocol + * @type {string?} + */ + get protocol () { + return this.#url?.protoocl ?? null + } + + /** + * The request path. + * @type {string} + */ + get path () { + if (!this.#url) { + return null + } + + return this.#url.pathname + this.#url.search + } + + /** + * The request host name (including port). + * @type {string?} + */ + get host () { + return this.#url?.hostname ?? null + } + + /** + * The URL for this outgoing message. This value is not absolute with + * respect to the protocol and hostname. It includes the path and search + * query component parameters. + * @type {string} + */ + get url () { + return this.#url + } + + /** + * @ignore + * @type {boolean} + */ + get finished () { + return this.writableEnded + } + + /** + * @ignore + * @type {boolean} + */ + get reusedSocket () { + return false + } + + /** + * @ignore + * @param {boolean=} [value] + * @return {ClientRequest} + */ + setNoDelay (value = false) { + // not supported + return this + } + + /** + * @ignore + * @param {boolean=} [enable] + * @param {number=} [initialDelay] + * @return {ClientRequest} + */ + setSocketKeepAlive (enable = false, initialDelay = 0) { + // not supported + return this + } +} + +/** + * An object that is created internally by a `Server` instance, not by the user. + * It is passed as the second parameter to the 'request' event. + * @see {@link https://nodejs.org/api/http.html#class-httpserverresponse} + */ +export class ServerResponse extends OutgoingMessage { + #statusMessage = STATUS_CODES[200] + #statusCode = 200 + #request = null + #sendDate = true + #server = null + + /** + * `ServerResponse` class constructor. + * @param {object} options + */ + constructor (options) { + super() + this.#request = options?.request ?? null + this.#server = options?.server ?? null + } + + /** + * @type {Server} + */ + get server () { + return this.#server + } + + /** + * A reference to the original HTTP request object. + * @type {IncomingMessage} + */ + get request () { + return this.#request + } + + /** + * A reference to the original HTTP request object. + * @type {IncomingMessage} + */ + get req () { + return this.request + } + + /** + * The HTTP request status code. + * Only valid for response obtained from `ClientRequest`. + * @type {number} + */ + get statusCode () { return this.#statusCode } + set statusCode (statusCode) { + this.#statusCode = statusCode + } + + /** + * The HTTP response status message (reason phrase). + * Such as "OK" or "Internal Server Error." + * Only valid for response obtained from `ClientRequest`. + * @type {string?} + */ + get statusMessage () { return this.#statusMessage } + set statusMessage (statusMessage) { + this.#statusMessage = statusMessage + } + + /** + * An alias for `statusCode` + * @type {number} + */ + get status () { return this.#statusCode } + set status (status) { + this.#statusCode = status + } + + /** + * An alias for `statusMessage` + * @type {string?} + */ + get statusText () { return this.#statusMessage } + set statusText (statusText) { + this.#statusMessage = statusText + } + + /** + * If `true`, the "Date" header will be automatically generated and sent in + * the response if it is not already present in the headers. + * Defaults to `true`. + * @type {boolean} + */ + get sendDate () { return this.#sendDate } + set sendDate (value) { + if (typeof value === 'boolean') { + this.#sendDate = value + } + } + + /** + * @ignore + */ + writeContinue () { + // not supported + return this + } + + /** + * @ignore + */ + writeEarlyHints () { + // not supported + return this + } + + /** + * @ignore + */ + writeProcessing () { + // not supported + return this + } + + /** + * Writes the response header to the request. + * The `statusCode` is a 3-digit HTTP status code, like 200 or 404. + * The last argument, `headers`, are the response headers. + * Optionally one can give a human-readable `statusMessage` + * as the second argument. + * @param {number|string} statusCode + * @param {string|object|string[]} [statusMessage] + * @param {object|string[]} [headers] + * @return {ClientRequest} + */ + writeHead (statusCode, statusMessage = null, headers = null) { + if ( + statusMessage && + (typeof statusMessage === 'object' || Array.isArray(statusMessage)) + ) { + headers = statusMessage + statusMessage = null + } + + this.#statusCode = parseInt(statusCode) + this.#statusMessage = statusMessage ?? STATUS_CODES[statusCode] + + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i] + const value = headers[i + 1] + this.appendHeader(key, value) + } + } else if (headers && typeof headers === 'object') { + for (const key in headers) { + const value = headers[key] + this.setHeader(key, value) + } + } + + return this + } + + /** + * Finishes the server response. + * @param {(Buffer|Uint8Array|string|function)=} [chunk] + * @param {(string|function)=} [encoding] + * @param {function=} [callback] + * @return {OutgoingMessage} + */ + end (chunk = null, encoding = null, callback = null) { + if (this.#server) { + for (const connection of this.#server.connections) { + if (connection.response === this) { + connection.close() + } + } + } + + super.end(chunk, encoding, callback) + return this + } + + /** + * @ignore + */ + _implicitHeader () { + this.writeHead(this.statusCode) + } +} + +/** + * An options object container for an `Agent` instance. + */ +export class AgentOptions { + keepAlive = false + timeout = -1 + + /** + * `AgentOptions` class constructor. + * @ignore + * @param {{ + * keepAlive?: boolean, + * timeout?: number + * }} [options] + */ + constructor (options) { + this.keepAlive = options?.keepAlive === true + this.timeout = Number.isFinite(options?.timeout) && options.timeout > 0 + ? options.timeout + : -1 + } +} + +/** + * An Agent is responsible for managing connection persistence + * and reuse for HTTP clients. + * @see {@link https://nodejs.org/api/http.html#class-httpagent} + */ +export class Agent extends EventEmitter { + defaultProtocol = 'http:' + options = null + requests = new Set() + sockets = {} + + // unused + maxFreeSockets = 256 + maxTotalSockets = Infinity + maxSockets = Infinity + + /** + * `Agent` class constructor. + * @param {AgentOptions=} [options] + */ + constructor (options = null) { + super() + this.options = new AgentOptions(options) + } + + /** + * @ignore + */ + get freeSockets () { + return {} + } + + /** + * @ignore + * @param {object} options + */ + getName (options) { + const { + host = '', + port = '', + localAddress = '', + family = '' + } = options ?? {} + + if (host && port && localAddress && family) { + return [host, port, localAddress, family].join(':') + } + + return [host, port, localAddress].join(':') + } + + /** + * Produces a socket/stream to be used for HTTP requests. + * @param {object} options + * @param {function(Duplex)=} [callback] + * @return {Duplex} + */ + createConnection (options, callback = null) { + let controller = null + let timeout = null + let url = null + + const abortController = new AbortController() + const readable = new ReadableStream({ start (c) { controller = c } }) + const pending = { callbacks: [], data: [] } + + const stream = new Duplex({ + signal: abortController.signal, + write (data, cb) { + controller.enqueue(data) + cb(null) + }, + + read (cb) { + if (pending.data.length) { + const data = pending.data.shift() + this.push(data) + cb(null) + } else { + pending.callbacks.push(cb) + } + } + }) + + stream.on('finish', () => readable.close()) + + url = `${options.protocol ?? this.defaultProtocol}//` + url += (options.host || options.hostname) + if (options.port) { + url += `:${options.port}` + } + + url += (options.path || options.pathname) + + if (options.signal) { + options.signal.addEventListener('abort', () => { + abortController.abort(options.signal.reason) + }) + } + + if (options.timeout) { + timeout = setTimeout(() => { + abortController.abort('Connection timed out') + stream.emit('timeout') + }, options.timeout) + } + + abortController.signal.addEventListener('abort', () => { + stream.emit('aborted') + stream.emit('error', Object.assign(new Error('aborted'), { code: 'ECONNRESET' })) + }) + + const deferredRequestPromise = options.makeRequest + ? options.makeRequest() + : Promise.resolve() + + deferredRequestPromise.then(makeRequest) + + function makeRequest (req) { + const request = fetch(url, { + // @ts-ignore + headers: Object.fromEntries( + Array.from(Object.entries( + options.headers?.entries?.() ?? options.headers ?? {} + )).concat(req.headers.entries()) + ), + signal: abortController.signal, + method: options.method ?? 'GET', + body: /put|post/i.test(options.method ?? '') + ? readable + : undefined + }) + + if (options.handleResponse) { + request.then(options.handleResponse) + } + + request.finally(() => clearTimeout(timeout)) + request + .then((response) => { + if (response.body) { + return response.body.getReader() + } + + return response + .blob() + .then((blob) => blob.stream().getReader()) + }) + .then((reader) => { + read() + function read () { + reader.read() + .then(({ done, value }) => { + if (pending.callbacks.length) { + const cb = pending.callbacks.shift() + stream.push(value) + cb(null) + } else { + pending.data.push(value ?? null) + } + + if (!done) { + read() + } + }) + } + }) + + if (typeof callback === 'function') { + callback(stream) + } + } + + return stream + } + + /** + * @ignore + */ + keepSocketAlive () { + // not supported + } + + /** + * @ignore + */ + reuseSocket () { + // not supported + } + + /** + * @ignore + */ + destroy () { + for (const request of this.requests) { + // @ts-ignore + if (typeof request?.destroy === 'function') { + // @ts-ignore + request.destroy() + } + } + } +} + +/** + * The global and default HTTP agent. + * @type {Agent} + */ +export const globalAgent = new Agent() + +/** + * A duplex stream between a HTTP request `IncomingMessage` and the + * response `ServerResponse` + */ +export class Connection extends Duplex { + server = null + active = false + request = null + response = null + + /** + * `Connection` class constructor. + * @ignore + * @param {Server} server + * @param {IncomingMessage} incomingMessage + * @param {ServerResponse} serverResponse + */ + constructor (server, incomingMessage, serverResponse) { + super({ + read (cb) { + try { + this.push(incomingMessage.read()) + cb(null) + } catch (err) { + cb(err) + } + }, + + write (data, cb) { + try { + serverResponse.write(data) + cb(null) + } catch (err) { + cb(err) + } + } + }) + + this.server = server + this.request = incomingMessage + this.response = serverResponse + + if (this.server.requestTimeout > 0) { + const timeout = setTimeout( + () => { + this.response.statusCode = 408 + this.response.statusMessage = STATUS_CODES[408] + this.response.buffers.splice(0, this.response.buffers.length) + this.response.end() + this.emit('timeout') + }, + this.server.requestTimeout + ) + + this.request.once('end', () => { + clearTimeout(timeout) + }) + } + + if (this.server.timeout > 0) { + const waitForNoActivity = () => { + const timeout = setTimeout( + () => { + this.response.statusCode = 408 + this.response.statusMessage = STATUS_CODES[408] + this.response.buffers.splice(0, this.response.buffers.length) + this.response.end() + this.emit('timeout') + }, + this.server.timeout + ) + + this.response.on('writebuffer', () => { + clearTimeout(timeout) + waitForNoActivity() + }) + } + + waitForNoActivity() + } + } + + /** + * Closes the connection, destroying the underlying duplex, request, and + * response streams. + * @return {Connection} + */ + close () { + this.destroy() + } +} + +/** + * A nodejs compat HTTP server typically intended for running in a "worker" + * environment. + * @see {@link https://nodejs.org/api/http.html#class-httpserver} + */ +export class Server extends EventEmitter { + #maxConnections = Infinity + #connections = [] + #listening = false + #adapter = null + #closed = false + #resource = new AsyncResource('HTTPServer') + #port = 0 + #host = null + + requestTimeout = 30000 + timeout = 0 + + // unused + maxRequestsPerSocket = 0 + keepAliveTimeout = 0 + headersTimeout = 60000 + + /** + * @ignore + * @type {AsyncResource} + */ + get resource () { + return this.#resource + } + + /** + * The adapter interface for this `Server` instance. + * @ignore + */ + get adapterInterace () { + return { + Connection, + globalAgent, + IncomingMessage, + METHODS, + ServerResponse, + STATUS_CODES + } + } + + /** + * `true` if the server is closed, otherwise `false`. + * @type {boolean} + */ + get closed () { + return this.#closed + } + + /** + * The host to listen to. This value can be `null`. + * Defaults to `location.hostname`. This value + * is used to filter requests by hostname. + * @type {string?} + */ + get host () { + return this.#host ?? null + } + + /** + * The `port` to listen on. This value can be `0`, which is the default. + * This value is used to filter requests by port, if given. A port value + * of `0` does not filter on any port. + * @type {number} + */ + get port () { + return this.#port ?? 0 + } + + /** + * A readonly array of all active or inactive (idle) connections. + * @type {Connection[]} + */ + get connections () { + return Array.from(this.#connections) + } + + /** + * `true` if the server is listening for requests. + * @type {boolean} + */ + get listening () { + return this.#listening + } + + /** + * The number of concurrent max connections this server should handle. + * Default: Infinity + * @type {number} + */ + get maxConnections () { return this.#maxConnections } + set maxConnections (value) { + if (value && typeof value === 'number' && value > 0) { + this.#maxConnections = value + } + } + + /** + * Gets the HTTP server address and port that it this server is + * listening (emulated) on in the runtime with respect to the + * adapter internal being used by the server. + * @return {{ family: string, address: string, port: number}} + */ + address () { + return { family: 'IPv4', address: this.#host, port: this.#port } + } + + /** + * Closes the server. + * @param {function=} [close] + */ + close (callback = null) { + if (typeof callback === 'function') { + this.once('close', callback) + } + + this.closeAllConnections() + this.#adapter.destroy() + this.#closed = true + queueMicrotask(() => this.emit('close')) + } + + /** + * Closes all connections. + */ + closeAllConnections () { + for (const connection of this.#connections) { + connection.close() + } + this.#connections = [] + } + + /** + * Closes all idle connections. + */ + closeIdleConnections () { + for (const connection of this.#connections) { + if (!connection.active) { + connection.close() + } + } + + this.#connections = this.#connections.filter((connection) => + connection.active === true + ) + } + + /** + * @ignore + */ + setTimeout (timeout = 0, callback = null) { + // not supported + return this + } + + /** + * @param {number|object=} [port] + * @param {string=} [host] + * @param {function|null} [unused] + * @param {function=} [callback + * @return Server + */ + listen (port = 0, host = null, unused = null, callback) { + if (typeof port === 'function') { + callback = port + port = 0 + host = null + unused = null + } + + if (typeof host === 'function') { + callback = host + host = null + unused = null + } + + if (typeof unused === 'function') { + callback = unused + } + + if (port && typeof port === 'object') { + const options = /** @type {{ hostname?: string, port?: number }} */ (port) + + if (typeof host === 'function') { + callback = host + } + + port = options?.port ?? 0 + host = options?.host ?? location?.hostname ?? null + } + + if (typeof callback === 'function') { + this.once('listening', callback) + } + + if (typeof port === 'number' && Number.isFinite(port) && port > 0) { + this.#port = port + } else { + this.#port = 0 + } + + if (host && typeof host === 'string') { + this.#host = host + } else { + this.#host = location?.hostname ?? null + } + + if (globalThis.isServiceWorkerScope === true) { + this.#adapter = new adapters.ServiceWorkerServerAdapter( + this, + this.adapterInterace + ) + + this.#adapter.addEventListener('activate', () => { + this.#listening = true + this.emit('listening') + }) + } + + this.on('connection', (connection) => { + if (this.#connections.length < this.maxConnections) { + this.#connections.push(connection) + connection.response.once('finish', () => { + const index = this.#connections.indexOf(connection) + if (index >= 0) { + this.#connections.splice(index, 1) + } + }) + } else { + connection.close() + } + }) + + gc.ref(this) + return this + } + + /** + * Implements `gc.finalizer` for gc'd resource cleanup. + * @return {gc.Finalizer} + * @ignore + */ + [gc.finalizer] () { + return { + args: [this.#adapter], + handle (adapter) { + if (adapter) { + adapter.destroy() + } + } + } + } +} + +/** + * Makes a HTTP request, optionally a `socket://` for relative paths when + * `socket:` is the origin protocol. + * @param {string|object} optionsOrURL + * @param {(object|function)=} [options] + * @param {function=} [callback] + * @return {ClientRequest} + */ +async function request (optionsOrURL, options, callback) { + if (typeof options === 'function') { + callback = options + } + + if (optionsOrURL && typeof optionsOrURL === 'object') { + options = optionsOrURL + callback = options + } else if (typeof optionsOrURL === 'string') { + const url = location.origin.startsWith('blob') + ? new URL(optionsOrURL, new URL(location.origin).pathname) + : new URL(optionsOrURL, location.origin) + + options = { + host: url.host, + port: url.port, + pathname: url.pathname, + protocol: url.protocol, + ...options + } + } + + let agent = null + + if (options.agent) { + agent = options.agent + } else if (options.agent === false) { + agent = new (options.Agent ?? Agent)() + } else { + agent = globalAgent + } + + let url = `${options.protocol ?? agent?.defaultProtocol ?? 'http:'}//${options.host || options.hostname}` + + if (options.port) { + url += `:${options.port}` + } + + url += options.pathname ?? '/' + + const request = new ClientRequest({ + method: options?.method ?? 'GET', + agent, + url + }) + + options = { + ...options, + makeRequest () { + return new Promise((resolve) => { + if (!/post|put/i.test(options.method ?? '')) { + resolve(request) + } else { + stream.on('finish', () => resolve(request)) + } + + request.headersSent = true + }) + }, + + handleResponse (response) { + const incomingMessage = new IncomingMessage({ + statusMessage: response.statusText, + statusCode: response.status, + complete: true, + headers: response.headers, + method: request.method, + url: response.url + }) + + stream.response = response + stream.pipe(incomingMessage) + request.emit('response', incomingMessage) + } + } + + const stream = agent.createConnection(options, callback) + + stream.on('finish', () => request.emit('finish')) + stream.on('timeout', () => request.emit('timeout')) + + return request +} + +/** + * Makes a HTTP or `socket:` GET request. A simplified alias to `request()`. + * @param {string|object} optionsOrURL + * @param {(object|function)=} [options] + * @param {function=} [callback] + * @return {ClientRequest} + */ +export function get (optionsOrURL, options, callback) { + return request(optionsOrURL, options, callback) +} + +/** + * Creates a HTTP server that can listen for incoming requests. + * Requests that are dispatched to this server depend on the context + * in which it is created, such as a service worker which will use a + * "fetch event" adapter. + * @param {object|function=} [options] + * @param {function=} [callback] + * @return {Server} + */ +export function createServer (options = null, callback = null) { + if (typeof options === 'function') { + callback = options + options = null + } + + const server = new Server(options) + + if (typeof callback === 'function') { + server.on('request', callback) + } + + return server +} + +export default exports diff --git a/api/http/adapters.js b/api/http/adapters.js new file mode 100644 index 0000000000..f23243c71e --- /dev/null +++ b/api/http/adapters.js @@ -0,0 +1,235 @@ +import { Deferred, AsyncContext } from '../async.js' +import { Buffer } from '../buffer.js' +import process from '../process.js' +import assert from '../assert.js' + +/** + * @typedef {{ + * Connection: typeof import('../http.js').Connection, + * globalAgent: import('../http.js').Agent, + * IncomingMessage: typeof import('../http.js').IncomingMessage, + * ServerResponse: typeof import('../http.js').ServerResponse, + * STATUS_CODES: object, + * METHODS: string[] + * }} HTTPModuleInterface + */ + +/** + * An abstract base clase for a HTTP server adapter. + */ +export class ServerAdapter extends EventTarget { + #server = null + #context = new AsyncContext.Variable() + #httpInterface = null + + /** + * `ServerAdapter` class constructor. + * @ignore + * @param {import('../http.js').Server} server + * @param {HTTPModuleInterface} httpInterface + */ + constructor (server, httpInterface) { + super() + + this.#server = server + this.#httpInterface = httpInterface + } + + /** + * A readonly reference to the underlying HTTP(S) server + * for this adapter. + * @type {import('../http.js').Server} + */ + get server () { + return this.#server + } + + /** + * A readonly reference to the underlying HTTP(S) module interface + * for creating various HTTP module class objects. + * @type {HTTPModuleInterface} + */ + get httpInterface () { + return this.#httpInterface + } + + /** + * A readonly reference to the `AsyncContext.Variable` associated with this + * `ServerAdapter` instance. + */ + get context () { + return this.#context + } + + /** + * Called when the adapter should destroy itself. + * @abstract + */ + async destroy () {} +} + +/** + * A HTTP adapter for running a HTTP server in a service worker that uses the + * "fetch" event for the request and response lifecycle. + */ +export class ServiceWorkerServerAdapter extends ServerAdapter { + /** + * `ServiceWorkerServerAdapter` class constructor. + * @ignore + * @param {import('../http.js').Server} server + * @param {HTTPModuleInterface} httpInterface + */ + constructor (server, httpInterface) { + assert( + globalThis.isServiceWorkerScope, + 'The ServiceWorkerServerAdapter can only be used in a ServiceWorker scope' + ) + + super(server, httpInterface) + + this.onInstall = this.onInstall.bind(this) + this.onActivate = this.onActivate.bind(this) + this.onFetch = this.onFetch.bind(this) + + globalThis.addEventListener('install', this.onInstall, { once: true }) + globalThis.addEventListener('activate', this.onActivate, { once: true }) + globalThis.addEventListener('fetch', this.onFetch) + } + + /** + * Called when the adapter should destroy itself. + */ + async destroy () { + globalThis.removeEventListener('install', this.onInstall, { once: true }) + globalThis.removeEventListener('activate', this.onActivate, { once: true }) + globalThis.removeEventListener('fetch', this.onFetch) + } + + /** + * Handles the 'install' service worker event. + * @ignore + * @param {import('../service-worker/events.js').ExtendableEvent} event + */ + async onInstall (event) { + // eslint-disable-next-line + void event; + globalThis.skipWaiting() + this.dispatchEvent(new Event('install')) + } + + /** + * Handles the 'activate' service worker event. + * @ignore + * @param {import('../service-worker/events.js').ExtendableEvent} event + */ + async onActivate (event) { + // eslint-disable-next-line + void event; + globalThis.clients.claim() + this.dispatchEvent(new Event('activate')) + } + + /** + * Handles the 'fetch' service worker event. + * @ignore + * @param {import('../service-worker/events.js').FetchEvent} + */ + async onFetch (event) { + if (this.server.closed) { + return + } + + const url = new URL(event.request.url) + + // allow port to be ignored in request + // this could be dangerous as it would lead to a race in request responses + // if there are multiple HTTP server instances in a single service worker + if (!process.env.SOCKET_RUNTIME_HTTP_ADAPTER_SERVICE_WORKER_IGNORE_PORT_CHECK) { + if (this.server.port !== 0 && url.port !== this.server.port) { + return + } + } + + if (this.server.host !== '0.0.0.0' && this.server.host !== '*') { + // the host MUST be checked and validated if not configured for + // ALL interfaces or uses a special wildcard token ('*') + if (this.server.host && url.hostname !== this.server.host) { + return + } + } + + if (this.server.connections.length >= this.server.maxConnections) { + event.respondWith(new Response()) + return + } + + const deferred = new Deferred() + + event.respondWith(deferred.promise) + + const incomingMessage = new this.httpInterface.IncomingMessage({ + complete: !/post|put/i.test(event.request.method), + headers: event.request.headers, + method: event.request.method, + server: this.server, + url: event.request.url + }) + + incomingMessage.event = event + + const serverResponse = new this.httpInterface.ServerResponse({ + request: incomingMessage, + server: this.server + }) + + const connection = new this.httpInterface.Connection( + this.server, + incomingMessage, + serverResponse + ) + + this.server.resource.runInAsyncScope(() => { + this.context.run({ connection, incomingMessage, serverResponse, event }, () => { + incomingMessage.context.run({ event }, () => { + this.server.emit('connection', connection) + this.server.emit('request', incomingMessage, serverResponse, event) + }) + }) + }) + + if (/post|put/i.test(event.request.method)) { + const { highWaterMark } = incomingMessage._readableState + const requestArrayBuffer = await event.request.arrayBuffer() + const buffer = Buffer.from(requestArrayBuffer) + for (let i = 0; i < buffer.byteLength; i += highWaterMark) { + incomingMessage.push(buffer.slice(i, i + highWaterMark)) + } + } + + serverResponse.on('finish', () => { + const buffer = Buffer.concat(serverResponse.buffers) + + if (!serverResponse.hasHeader('content-length')) { + serverResponse.setHeader('content-length', buffer.byteLength) + } + + // disable cache for error responses + if (serverResponse.statusCode >= 400) { + serverResponse.setHeader('cache-control', 'no-cache') + } + + const response = new Response(buffer, { + status: serverResponse.statusCode, + statusText: serverResponse.statusText, + headers: serverResponse.headers + }) + + deferred.resolve(response) + }) + } +} + +export default { + ServerAdapter, + ServiceWorkerServerAdapter +} diff --git a/api/https.js b/api/https.js new file mode 100644 index 0000000000..3fd8feca17 --- /dev/null +++ b/api/https.js @@ -0,0 +1,202 @@ +import http from './http.js' +// re-export +import * as exports from './http.js' + +export const CONTINUE = http.CONTINUE +export const SWITCHING_PROTOCOLS = http.SWITCHING_PROTOCOLS +export const PROCESSING = http.PROCESSING +export const EARLY_HINTS = http.EARLY_HINTS +export const OK = http.OK +export const CREATED = http.CREATED +export const ACCEPTED = http.ACCEPTED +export const NONAUTHORITATIVE_INFORMATION = http.NONAUTHORITATIVE_INFORMATION +export const NO_CONTENT = http.NO_CONTENT +export const RESET_CONTENT = http.RESET_CONTENT +export const PARTIAL_CONTENT = http.PARTIAL_CONTENT +export const MULTISTATUS = http.MULTISTATUS +export const ALREADY_REPORTED = http.ALREADY_REPORTED +export const IM_USED = http.IM_USED +export const MULTIPLE_CHOICES = http.MULTIPLE_CHOICES +export const MOVED_PERMANENTLY = http.MOVED_PERMANENTLY +export const FOUND = http.FOUND +export const SEE_OTHER = http.SEE_OTHER +export const NOT_MODIFIED = http.NOT_MODIFIED +export const USE_PROXY = http.USE_PROXY +export const TEMPORARY_REDIRECT = http.TEMPORARY_REDIRECT +export const PERMANENT_REDIRECT = http.PERMANENT_REDIRECT +export const BAD_REQUEST = http.BAD_REQUEST +export const UNAUTHORIZED = http.UNAUTHORIZED +export const PAYMENT_REQUIRED = http.PAYMENT_REQUIRED +export const FORBIDDEN = http.FORBIDDEN +export const NOT_FOUND = http.NOT_FOUND +export const METHOD_NOT_ALLOWED = http.METHOD_NOT_ALLOWED +export const NOT_ACCEPTABLE = http.NOT_ACCEPTABLE +export const PROXY_AUTHENTICATION_REQUIRED = http.PROXY_AUTHENTICATION_REQUIRED +export const REQUEST_TIMEOUT = http.REQUEST_TIMEOUT +export const CONFLICT = http.CONFLICT +export const GONE = http.GONE +export const LENGTH_REQUIRED = http.LENGTH_REQUIRED +export const PRECONDITION_FAILED = http.PRECONDITION_FAILED +export const PAYLOAD_TOO_LARGE = http.PAYLOAD_TOO_LARGE +export const URI_TOO_LONG = http.URI_TOO_LONG +export const UNSUPPORTED_MEDIA_TYPE = http.UNSUPPORTED_MEDIA_TYPE +export const RANGE_NOT_SATISFIABLE = http.RANGE_NOT_SATISFIABLE +export const EXPECTATION_FAILED = http.EXPECTATION_FAILED +export const IM_A_TEAPOT = http.IM_A_TEAPOT +export const MISDIRECTED_REQUEST = http.MISDIRECTED_REQUEST +export const UNPROCESSABLE_ENTITY = http.UNPROCESSABLE_ENTITY +export const LOCKED = http.LOCKED +export const FAILED_DEPENDENCY = http.FAILED_DEPENDENCY +export const TOO_EARLY = http.TOO_EARLY +export const UPGRADE_REQUIRED = http.UPGRADE_REQUIRED +export const PRECONDITION_REQUIRED = http.PRECONDITION_REQUIRED +export const TOO_MANY_REQUESTS = http.TOO_MANY_REQUESTS +export const REQUEST_HEADER_FIELDS_TOO_LARGE = http.REQUEST_HEADER_FIELDS_TOO_LARGE +export const UNAVAILABLE_FOR_LEGAL_REASONS = http.UNAVAILABLE_FOR_LEGAL_REASONS +export const INTERNAL_SERVER_ERROR = http.INTERNAL_SERVER_ERROR +export const NOT_IMPLEMENTED = http.NOT_IMPLEMENTED +export const BAD_GATEWAY = http.BAD_GATEWAY +export const SERVICE_UNAVAILABLE = http.SERVICE_UNAVAILABLE +export const GATEWAY_TIMEOUT = http.GATEWAY_TIMEOUT +export const HTTP_VERSION_NOT_SUPPORTED = http.HTTP_VERSION_NOT_SUPPORTED +export const VARIANT_ALSO_NEGOTIATES = http.VARIANT_ALSO_NEGOTIATES +export const INSUFFICIENT_STORAGE = http.INSUFFICIENT_STORAGE +export const LOOP_DETECTED = http.LOOP_DETECTED +export const BANDWIDTH_LIMIT_EXCEEDED = http.BANDWIDTH_LIMIT_EXCEEDED +export const NOT_EXTENDED = http.NOT_EXTENDED +export const NETWORK_AUTHENTICATION_REQUIRED = http.NETWORK_AUTHENTICATION_REQUIRED + +/** + * All known possible HTTP methods. + * @type {string[]} + */ +export const METHODS = http.METHODS + +/** + * A mapping of status codes to status texts + * @type {object} + */ +export const STATUS_CODES = http.STATUS_CODES + +/** + * An options object container for an `Agent` instance. + */ +export class AgentOptions extends http.AgentOptions {} + +/** + * An Agent is responsible for managing connection persistence + * and reuse for HTTPS clients. + * @see {@link https://nodejs.org/api/https.html#class-httpsagent} + */ +export class Agent extends http.Agent { + defaultProtocol = 'https:' +} + +/** + * An object that is created internally and returned from `request()`. + * @see {@link https://nodejs.org/api/http.html#class-httpclientrequest} + */ +export class ClientRequest extends http.ClientRequest {} + +/** + * The parent class of `ClientRequest` and `ServerResponse`. + * It is an abstract outgoing message from the perspective of the + * participants of an HTTP transaction. + * @see {@link https://nodejs.org/api/http.html#class-httpoutgoingmessage} + */ +export class OutgoingMessage extends http.OutgoingMessage {} + +/** + * An `IncomingMessage` object is created by `Server` or `ClientRequest` and + * passed as the first argument to the 'request' and 'response' event + * respectively. + * It may be used to access response status, headers, and data. + * @see {@link https://nodejs.org/api/http.html#class-httpincomingmessage} + */ +export class IncomingMessage extends http.IncomingMessage {} + +/** + * An object that is created internally by a `Server` instance, not by the user. + * It is passed as the second parameter to the 'request' event. + * @see {@link https://nodejs.org/api/http.html#class-httpserverresponse} + */ +export class ServerResponse extends http.ServerResponse {} + +/** + * A duplex stream between a HTTP request `IncomingMessage` and the + * response `ServerResponse` + */ +export class Connection extends http.Connection {} + +/** + * A nodejs compat HTTP server typically intended for running in a "worker" + * environment. + * @see {@link https://nodejs.org/api/http.html#class-httpserver} + */ +export class Server extends http.Server { + /** + * The adapter interface for this `Server` instance. + * @ignore + */ + get adapterInterace () { + return { + Connection, + globalAgent, + IncomingMessage, + METHODS, + ServerResponse, + STATUS_CODES + } + } +} + +/** + * The global and default HTTPS agent. + * @type {Agent} + */ +export const globalAgent = new Agent() + +/** + * Makes a HTTPS request, optionally a `socket://` for relative paths when + * `socket:` is the origin protocol. + * @param {string|object} optionsOrURL + * @param {(object|function)=} [options] + * @param {function=} [callback] + * @return {ClientRequest} + */ +export function request (optionsOrURL, options, callback) { + if (typeof optionsOrURL === 'string') { + options = { Agent, ...options } + return http.request(optionsOrURL, options, callback) + } + + options = { Agent, ...optionsOrURL } + callback = options + return http.request(optionsOrURL, options, callback) +} + +/** + * Makes a HTTPS or `socket:` GET request. A simplified alias to `request()`. + * @param {string|object} optionsOrURL + * @param {(object|function)=} [options] + * @param {function=} [callback] + * @return {ClientRequest} + */ +export function get (optionsOrURL, options, callback) { + return request(optionsOrURL, options, callback) +} + +/** + * Creates a HTTPS server that can listen for incoming requests. + * Requests that are dispatched to this server depend on the context + * in which it is created, such as a service worker which will use a + * "fetch event" adapter. + * @param {object|function=} [options] + * @param {function=} [callback] + * @return {Server} + */ +export function createServer (...args) { + return http.createServer(...args) +} + +export default exports diff --git a/api/index.d.ts b/api/index.d.ts index 1794c5dc5b..3fb9ce277f 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -1,318 +1,456 @@ -declare module "socket:errors" { - export default exports; - export const ABORT_ERR: any; - export const ENCODING_ERR: any; - export const INVALID_ACCESS_ERR: any; - export const INDEX_SIZE_ERR: any; - export const NETWORK_ERR: any; - export const NOT_ALLOWED_ERR: any; - export const NOT_FOUND_ERR: any; - export const NOT_SUPPORTED_ERR: any; - export const OPERATION_ERR: any; - export const SECURITY_ERR: any; - export const TIMEOUT_ERR: any; + +declare module "socket:async/context" { /** - * An `AbortError` is an error type thrown in an `onabort()` level 0 - * event handler on an `AbortSignal` instance. + * @module async.context + * + * Async Context for JavaScript based on the TC39 proposal. + * + * Example usage: + * ```js + * // `AsyncContext` is also globally available as `globalThis.AsyncContext` + * import AsyncContext from 'socket:async/context' + * + * const var = new AsyncContext.Variable() + * var.run('top', () => { + * console.log(var.get()) // 'top' + * queueMicrotask(() => { + * var.run('nested', () => { + * console.log(var.get()) // 'nested' + * }) + * }) + * }) + * ``` + * + * @see {@link https://tc39.es/proposal-async-context} + * @see {@link https://github.com/tc39/proposal-async-context} */ - export class AbortError extends Error { - /** - * The code given to an `ABORT_ERR` `DOMException` - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} - */ - static get code(): any; - /** - * `AbortError` class constructor. - * @param {AbortSignal|string} reasonOrSignal - * @param {AbortSignal=} [signal] - */ - constructor(reason: any, signal?: AbortSignal | undefined, ...args: any[]); - signal: AbortSignal; - get name(): string; - get code(): string; - } /** - * An `BadRequestError` is an error type thrown in an `onabort()` level 0 - * event handler on an `BadRequestSignal` instance. + * @template T + * @typedef {{ + * name?: string, + * defaultValue?: T + * }} VariableOptions */ - export class BadRequestError extends Error { - /** - * The default code given to a `BadRequestError` - */ - static get code(): number; - /** - * `BadRequestError` class constructor. - * @param {string} message - * @param {number} [code] - */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; - } /** - * An `EncodingError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. + * @callback AnyFunc + * @template T + * @this T + * @param {...any} args + * @returns {any} */ - export class EncodingError extends Error { + /** + * `FrozenRevert` holds a frozen Mapping that will be simply restored + * when the revert is run. + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/fork.ts} + */ + export class FrozenRevert { /** - * The code given to an `ENCODING_ERR` `DOMException`. + * `FrozenRevert` class constructor. + * @param {Mapping} mapping */ - static get code(): any; + constructor(mapping: Mapping); /** - * `EncodingError` class constructor. - * @param {string} message - * @param {number} [code] + * Restores (unchaged) mapping from this `FrozenRevert`. This function is + * called by `AsyncContext.Storage` when it reverts a current mapping to the + * previous state before a "fork". + * @param {Mapping=} [unused] + * @return {Mapping} */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; + restore(unused?: Mapping | undefined): Mapping; + #private; } /** - * An `FinalizationRegistryCallbackError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. + * Revert holds the state on how to revert a change to the + * `AsyncContext.Storage` current `Mapping` + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/fork.ts} + * @template T */ - export class FinalizationRegistryCallbackError extends Error { + export class Revert { /** - * The default code given to an `FinalizationRegistryCallbackError` + * `Revert` class constructor. + * @param {Mapping} mapping + * @param {Variable} key */ - static get code(): number; + constructor(mapping: Mapping, key: Variable); /** - * `FinalizationRegistryCallbackError` class constructor. - * @param {string} message - * @param {number} [code] + * @type {T|undefined} */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; + get previousVariable(): T; + /** + * Restores a mapping from this `Revert`. This function is called by + * `AsyncContext.Storage` when it reverts a current mapping to the + * previous state before a "fork". + * @param {Mapping} current + * @return {Mapping} + */ + restore(current: Mapping): Mapping; + #private; } /** - * An `IllegalConstructorError` is an error type thrown when a constructor is - * called for a class constructor when it shouldn't be. + * A container for all `AsyncContext.Variable` instances and snapshot state. + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/mapping.ts} */ - export class IllegalConstructorError extends TypeError { + export class Mapping { /** - * The default code given to an `IllegalConstructorError` + * `Mapping` class constructor. + * @param {Map, any>} data */ - static get code(): number; + constructor(data: Map, any>); /** - * `IllegalConstructorError` class constructor. - * @param {string} message - * @param {number} [code] + * Freezes the `Mapping` preventing `AsyncContext.Variable` modifications with + * `set()` and `delete()`. */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; - } - /** - * An `IndexSizeError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. - */ - export class IndexSizeError extends Error { + freeze(): void; /** - * The code given to an `INDEX_SIZE_ERR` `DOMException` + * Returns `true` if the `Mapping` is frozen, otherwise `false`. + * @return {boolean} */ - static get code(): any; + isFrozen(): boolean; /** - * `IndexSizeError` class constructor. - * @param {string} message - * @param {number} [code] + * Optionally returns a new `Mapping` if the current one is "frozen", + * otherwise it just returns the current instance. + * @return {Mapping} */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; - } - export const kInternalErrorCode: unique symbol; - /** - * An `InternalError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. - */ - export class InternalError extends Error { + fork(): Mapping; /** - * The default code given to an `InternalError` + * Returns `true` if the `Mapping` has a `AsyncContext.Variable` at `key`, + * otherwise `false. + * @template T + * @param {Variable} key + * @return {boolean} */ - static get code(): number; + has(key: Variable): boolean; /** - * `InternalError` class constructor. - * @param {string} message - * @param {number} [code] + * Gets an `AsyncContext.Variable` value at `key`. If not set, this function + * returns `undefined`. + * @template T + * @param {Variable} key + * @return {boolean} */ - constructor(message: string, code?: number, ...args: any[]); - get name(): string; + get(key: Variable): boolean; /** - * @param {number|string} + * Sets an `AsyncContext.Variable` value at `key`. If the `Mapping` is frozen, + * then a "forked" (new) instance with the value set on it is returned, + * otherwise the current instance. + * @template T + * @param {Variable} key + * @param {T} value + * @return {Mapping} */ - set code(code: string | number); + set(key: Variable, value: T): Mapping; /** - * @type {number|string} + * Delete an `AsyncContext.Variable` value at `key`. + * If the `Mapping` is frozen, then a "forked" (new) instance is returned, + * otherwise the current instance. + * @template T + * @param {Variable} key + * @param {T} value + * @return {Mapping} */ - get code(): string | number; - [exports.kInternalErrorCode]: number; + delete(key: Variable): Mapping; + #private; } /** - * An `InvalidAccessError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. + * A container of all `AsyncContext.Variable` data. + * @ignore + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/src/storage.ts} */ - export class InvalidAccessError extends Error { + export class Storage { /** - * The code given to an `INVALID_ACCESS_ERR` `DOMException` - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * The current `Mapping` for this `AsyncContext`. + * @type {Mapping} */ - static get code(): any; + static "__#4@#current": Mapping; /** - * `InvalidAccessError` class constructor. - * @param {string} message - * @param {number} [code] + * Returns `true` if the current `Mapping` has a + * `AsyncContext.Variable` at `key`, + * otherwise `false. + * @template T + * @param {Variable} key + * @return {boolean} */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; - } - /** - * An `NetworkError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. - */ - export class NetworkError extends Error { + static has(key: Variable): boolean; /** - * The code given to an `NETWORK_ERR` `DOMException` - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * Gets an `AsyncContext.Variable` value at `key` for the current `Mapping`. + * If not set, this function returns `undefined`. + * @template T + * @param {Variable} key + * @return {T|undefined} */ - static get code(): any; + static get(key: Variable): T | undefined; /** - * `NetworkError` class constructor. - * @param {string} message - * @param {number} [code] + * Set updates the `AsyncContext.Variable` with a new value and returns a + * revert action that allows the modification to be reversed in the future. + * @template T + * @param {Variable} key + * @param {T} value + * @return {Revert|FrozenRevert} */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; - } - /** - * An `NotAllowedError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. - */ - export class NotAllowedError extends Error { + static set(key: Variable, value: T): Revert | FrozenRevert; /** - * The code given to an `NOT_ALLOWED_ERR` `DOMException` - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * "Freezes" the current storage `Mapping`, and returns a new `FrozenRevert` + * or `Revert` which can restore the storage state to the state at + * the time of the snapshot. + * @return {FrozenRevert} */ - static get code(): any; + static snapshot(): FrozenRevert; /** - * `NotAllowedError` class constructor. - * @param {string} message - * @param {number} [code] + * Restores the storage `Mapping` state to state at the time the + * "revert" (`FrozenRevert` or `Revert`) was created. + * @template T + * @param {Revert|FrozenRevert} revert */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; + static restore(revert: Revert | FrozenRevert): void; + /** + * Switches storage `Mapping` state to the state at the time of a + * "snapshot". + * @param {FrozenRevert} snapshot + * @return {FrozenRevert} + */ + static switch(snapshot: FrozenRevert): FrozenRevert; } /** - * An `NotFoundError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. + * `AsyncContext.Variable` is a container for a value that is associated with + * the current execution flow. The value is propagated through async execution + * flows, and can be snapshot and restored with Snapshot. + * @template T + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextvariable} */ - export class NotFoundError extends Error { + export class Variable { /** - * The code given to an `NOT_FOUND_ERR` `DOMException` - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * `Variable` class constructor. + * @param {VariableOptions=} [options] */ - static get code(): any; + constructor(options?: VariableOptions | undefined); + set defaultValue(defaultValue: T); /** - * `NotFoundError` class constructor. - * @param {string} message - * @param {number} [code] + * @ignore */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; - } - /** - * An `NotSupportedError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. - */ - export class NotSupportedError extends Error { + get defaultValue(): T; /** - * The code given to an `NOT_SUPPORTED_ERR` `DOMException` - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @ignore */ - static get code(): any; + get revert(): FrozenRevert | Revert; /** - * `NotSupportedError` class constructor. - * @param {string} message - * @param {number} [code] + * The name of this async context variable. + * @type {string} */ - constructor(message: string, ...args: any[]); get name(): string; - get code(): string; - } - /** - * An `ModuleNotFoundError` is an error type thrown when an imported or - * required module is not found. - */ - export class ModuleNotFoundError extends exports.NotFoundError { /** - * `ModuleNotFoundError` class constructor. - * @param {string} message + * Executes a function `fn` with specified arguments, + * setting a new value to the current context before the call, + * and ensuring the environment is reverted back afterwards. + * The function allows for the modification of a specific context's + * state in a controlled manner, ensuring that any changes can be undone. + * @template T, F extends AnyFunc + * @param {T} value + * @param {F} fn + * @param {...Parameters} args + * @returns {ReturnType} + */ + run(value: T_1, fn: F, ...args: Parameters[]): ReturnType; + /** + * Get the `AsyncContext.Variable` value. + * @template T + * @return {T|undefined} */ - constructor(message: string, requireStack: any); - requireStack: any; + get(): T_1 | undefined; + #private; } /** - * An `OperationError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. + * `AsyncContext.Snapshot` allows you to opaquely capture the current values of + * all `AsyncContext.Variable` instances and execute a function at a later time + * as if those values were still the current values (a snapshot and restore). + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextsnapshot} */ - export class OperationError extends Error { + export class Snapshot { /** - * The code given to an `OPERATION_ERR` `DOMException` + * Wraps a given function `fn` with additional logic to take a snapshot of + * `Storage` before invoking `fn`. Returns a new function with the same + * signature as `fn` that when called, will invoke `fn` with the current + * `this` context and provided arguments, after restoring the `Storage` + * snapshot. + * + * `AsyncContext.Snapshot.wrap` is a helper which captures the current values + * of all Variables and returns a wrapped function. When invoked, this + * wrapped function restores the state of all Variables and executes the + * inner function. + * + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextsnapshotwrap} + * + * @template F + * @param {F} fn + * @returns {F} */ - static get code(): any; + static wrap(fn: F): F; /** - * `OperationError` class constructor. - * @param {string} message - * @param {number} [code] + * Runs the given function `fn` with arguments `args`, using a `null` + * context and the current snapshot. + * + * @template F extends AnyFunc + * @param {F} fn + * @param {...Parameters} args + * @returns {ReturnType} */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; + run(fn: F, ...args: Parameters[]): ReturnType; + #private; } /** - * An `SecurityError` is an error type thrown when an internal exception - * has occurred, such as in the native IPC layer. + * `AsyncContext` container. */ - export class SecurityError extends Error { + export class AsyncContext { /** - * The code given to an `SECURITY_ERR` `DOMException` + * `AsyncContext.Variable` is a container for a value that is associated with + * the current execution flow. The value is propagated through async execution + * flows, and can be snapshot and restored with Snapshot. + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextvariable} + * @type {typeof Variable} */ - static get code(): any; + static Variable: typeof Variable; /** - * `SecurityError` class constructor. - * @param {string} message - * @param {number} [code] + * `AsyncContext.Snapshot` allows you to opaquely capture the current values of + * all `AsyncContext.Variable` instances and execute a function at a later time + * as if those values were still the current values (a snapshot and restore). + * @see {@link https://github.com/tc39/proposal-async-context/blob/master/README.md#asynccontextsnapshot} + * @type {typeof Snapshot} */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; + static Snapshot: typeof Snapshot; } - /** - * An `TimeoutError` is an error type thrown when an operation timesout. - */ - export class TimeoutError extends Error { - /** - * The code given to an `TIMEOUT_ERR` `DOMException` - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} - */ - static get code(): any; - /** - * `TimeoutError` class constructor. - * @param {string} message - */ - constructor(message: string, ...args: any[]); - get name(): string; - get code(): string; + export default AsyncContext; + export type VariableOptions = { + name?: string; + defaultValue?: T; + }; + export type AnyFunc = () => any; +} + +declare module "socket:events" { + export const Event: { + new (type: string, eventInitDict?: EventInit): Event; + prototype: Event; + readonly NONE: 0; + readonly CAPTURING_PHASE: 1; + readonly AT_TARGET: 2; + readonly BUBBLING_PHASE: 3; + } | { + new (): {}; + }; + export const EventTarget: { + new (): {}; + }; + export const CustomEvent: { + new (type: string, eventInitDict?: CustomEventInit): CustomEvent; + prototype: CustomEvent; + } | { + new (type: any, options: any): { + "__#7@#detail": any; + readonly detail: any; + }; + }; + export const MessageEvent: { + new (type: string, eventInitDict?: MessageEventInit): MessageEvent; + prototype: MessageEvent; + } | { + new (type: any, options: any): { + "__#8@#detail": any; + "__#8@#data": any; + readonly detail: any; + readonly data: any; + }; + }; + export const ErrorEvent: { + new (type: string, eventInitDict?: ErrorEventInit): ErrorEvent; + prototype: ErrorEvent; + } | { + new (type: any, options: any): { + "__#9@#detail": any; + "__#9@#error": any; + readonly detail: any; + readonly error: any; + }; + }; + export default EventEmitter; + export function EventEmitter(): void; + export class EventEmitter { + _events: any; + _contexts: any; + _eventsCount: number; + _maxListeners: number; + setMaxListeners(n: any): this; + getMaxListeners(): any; + emit(type: any, ...args: any[]): boolean; + addListener(type: any, listener: any): any; + on(arg0: any, arg1: any): any; + prependListener(type: any, listener: any): any; + once(type: any, listener: any): this; + prependOnceListener(type: any, listener: any): this; + removeListener(type: any, listener: any): this; + off(type: any, listener: any): this; + removeAllListeners(type: any, ...args: any[]): this; + listeners(type: any): any[]; + rawListeners(type: any): any[]; + listenerCount(type: any): any; + eventNames(): (string | symbol)[]; } - import * as exports from "socket:errors"; - + export namespace EventEmitter { + export { EventEmitter }; + export let defaultMaxListeners: number; + export function init(): void; + export function listenerCount(emitter: any, type: any): any; + export { once }; + } + export function once(emitter: any, name: any): Promise; +} + +declare module "socket:url/urlpattern/urlpattern" { + export { me as URLPattern }; + var me: { + new (t: {}, r: any, n: any): { + "__#11@#i": any; + "__#11@#n": {}; + "__#11@#t": {}; + "__#11@#e": {}; + "__#11@#s": {}; + "__#11@#l": boolean; + test(t: {}, r: any): boolean; + exec(t: {}, r: any): { + inputs: any[] | {}[]; + }; + readonly protocol: any; + readonly username: any; + readonly password: any; + readonly hostname: any; + readonly port: any; + readonly pathname: any; + readonly search: any; + readonly hash: any; + readonly hasRegExpGroups: boolean; + }; + compareComponent(t: any, r: any, n: any): number; + }; +} + +declare module "socket:url/url/url" { + const _default: any; + export default _default; } + declare module "socket:buffer" { export default Buffer; + export const File: { + new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File; + prototype: File; + }; + export const Blob: { + new (blobParts?: BlobPart[], options?: BlobPropertyBag): Blob; + prototype: Blob; + }; + export namespace constants { + export { kMaxLength as MAX_LENGTH }; + export { kMaxLength as MAX_STRING_LENGTH }; + } + export const btoa: any; + export const atob: any; /** * The Buffer constructor returns instances of `Uint8Array` that have their * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of @@ -454,7978 +592,15197 @@ declare module "socket:buffer" { export function concat(list: any, length: any): Uint8Array; export { byteLength }; } + export const kMaxLength: 2147483647; export function SlowBuffer(length: any): Uint8Array; export const INSPECT_MAX_BYTES: 50; - export const kMaxLength: 2147483647; function byteLength(string: any, encoding: any, ...args: any[]): any; } -declare module "socket:url/urlpattern/urlpattern" { - export { me as URLPattern }; - var me: { - new (t: {}, r: any, n: any): { - "__#2@#i": any; - "__#2@#n": {}; - "__#2@#t": {}; - "__#2@#e": {}; - "__#2@#s": {}; - test(t: {}, r: any): boolean; - exec(t: {}, r: any): { - inputs: any[] | {}[]; - }; - readonly protocol: any; - readonly username: any; - readonly password: any; - readonly hostname: any; - readonly port: any; - readonly pathname: any; - readonly search: any; - readonly hash: any; - }; - compareComponent(t: any, r: any, n: any): number; - }; -} -declare module "socket:url/url/url" { - const _default: any; + +declare module "socket:querystring" { + export function unescapeBuffer(s: any, decodeSpaces: any): any; + export function unescape(s: any, decodeSpaces: any): any; + export function escape(str: any): any; + export function stringify(obj: any, sep: any, eq: any, options: any): string; + export function parse(qs: any, sep: any, eq: any, options: any): {}; + export function decode(qs: any, sep: any, eq: any, options: any): {}; + export function encode(obj: any, sep: any, eq: any, options: any): string; + namespace _default { + export { decode }; + export { encode }; + export { parse }; + export { stringify }; + export { escape }; + export { unescape }; + } export default _default; } + declare module "socket:url/index" { + export function parse(input: any, options?: any): { + hash: any; + host: any; + hostname: any; + origin: any; + auth: string; + password: any; + pathname: any; + path: any; + port: any; + protocol: any; + search: any; + searchParams: any; + username: any; + [Symbol.toStringTag]: string; + }; export function resolve(from: any, to: any): any; - export const parse: any; + export function format(input: any): any; + export function fileURLToPath(url: any): any; + /** + * @type {Set & { handlers: Set }} + */ + export const protocols: Set & { + handlers: Set; + }; export default URL; - export const URL: any; - import { URLPattern } from "socket:url/urlpattern/urlpattern"; + export class URL { + private constructor(); + } export const URLSearchParams: any; export const parseURL: any; + import { URLPattern } from "socket:url/urlpattern/urlpattern"; export { URLPattern }; } + declare module "socket:url" { export * from "socket:url/index"; export default URL; import URL from "socket:url/index"; } -declare module "socket:util" { - export function hasOwnProperty(object: any, property: any): any; - export function isTypedArray(object: any): boolean; - export function isArrayLike(object: any): boolean; - export function isArrayBufferView(buf: any): boolean; - export function isAsyncFunction(object: any): boolean; - export function isArgumentsObject(object: any): boolean; - export function isEmptyObject(object: any): boolean; - export function isObject(object: any): boolean; - export function isPlainObject(object: any): boolean; - export function isArrayBuffer(object: any): boolean; - export function isBufferLike(object: any): boolean; - export function isFunction(value: any): boolean; - export function isErrorLike(error: any): boolean; - export function isClass(value: any): boolean; - export function isPromiseLike(object: any): boolean; - export function toString(object: any): string; - export function toBuffer(object: any, encoding?: any): any; - export function toProperCase(string: any): any; - export function splitBuffer(buffer: any, highWaterMark: any): any[]; - export function InvertedPromise(): Promise; - export function clamp(value: any, min: any, max: any): number; - export function promisify(original: any): any; - export function inspect(value: any, options: any): any; - export namespace inspect { - let custom: symbol; - let ignore: symbol; - } - export function format(format: any, ...args: any[]): string; - export function parseJSON(string: any): any; - export function parseHeaders(headers: any): string[][]; - export function noop(): void; - export function isValidPercentageValue(input: any): boolean; - export function compareBuffers(a: any, b: any): any; - export class IllegalConstructor { + +declare module "socket:fs/bookmarks" { + /** + * A map of known absolute file paths to file IDs that + * have been granted access outside of the sandbox. + * XXX(@jwerle): this is currently only used on linux, but valaues may + * be added for all platforms, likely from a file system picker dialog. + * @type {Map} + */ + export const temporary: Map; + namespace _default { + export { temporary }; } - export default exports; - import * as exports from "socket:util"; - + export default _default; } -declare module "socket:window/constants" { - export const WINDOW_ERROR: -1; - export const WINDOW_NONE: 0; - export const WINDOW_CREATING: 10; - export const WINDOW_CREATED: 11; - export const WINDOW_HIDING: 20; - export const WINDOW_HIDDEN: 21; - export const WINDOW_SHOWING: 30; - export const WINDOW_SHOWN: 31; - export const WINDOW_CLOSING: 40; - export const WINDOW_CLOSED: 41; - export const WINDOW_EXITING: 50; - export const WINDOW_EXITED: 51; - export const WINDOW_KILLING: 60; - export const WINDOW_KILLED: 61; - export * as _default from "socket:window/constants"; - + +declare module "socket:internal/serialize" { + export default function serialize(value: any): any; } + declare module "socket:location" { - export function toString(): string; - export const globalLocation: Location | { - origin: string; - host: string; - hostname: string; - pathname: string; - href: string; - }; - export const href: string; - export const protocol: "socket:"; - export const hostname: string; - export const host: string; - export const search: string; - export const hash: string; - export const pathname: string; - export const origin: string; - namespace _default { - export { origin }; - export { href }; - export { protocol }; - export { hostname }; - export { host }; - export { search }; - export { pathname }; - export { toString }; + export class Location { + get url(): URL; + get protocol(): string; + get host(): string; + get hostname(): string; + get port(): string; + get pathname(): string; + get search(): string; + get origin(): string; + get href(): string; + get hash(): string; + toString(): string; } + const _default: Location; export default _default; } -declare module "socket:console" { - export function patchGlobalConsole(globalConsole: any, options?: {}): any; - export const globalConsole: globalThis.Console; - export class Console { - /** - * @ignore - */ - constructor(options: any); - /** - * @type {import('dom').Console} - */ - console: any; - /** - * @type {Map} - */ - timers: Map; - /** - * @type {Map} - */ - counters: Map; - /** - * @type {function?} - */ - postMessage: Function | null; - write(destination: any, ...args: any[]): Promise; - assert(assertion: any, ...args: any[]): void; - clear(): void; - count(label?: string): void; - countReset(label?: string): void; - debug(...args: any[]): void; - dir(...args: any[]): void; - dirxml(...args: any[]): void; - error(...args: any[]): void; - info(...args: any[]): void; - log(...args: any[]): void; - table(...args: any[]): any; - time(label?: string): void; - timeEnd(label?: string): void; - timeLog(label?: string): void; - trace(...objects: any[]): void; - warn(...args: any[]): void; + +declare module "socket:internal/symbols" { + export const dispose: any; + export const serialize: any; + namespace _default { + export { dispose }; + export { serialize }; } - const _default: Console; export default _default; } -declare module "socket:events" { - export const Event: { - new (type: string, eventInitDict?: EventInit): Event; - prototype: Event; - readonly NONE: 0; - readonly CAPTURING_PHASE: 1; - readonly AT_TARGET: 2; - readonly BUBBLING_PHASE: 3; - } | { - new (): {}; - }; - export const EventTarget: { - new (): {}; - }; - export const CustomEvent: { - new (type: string, eventInitDict?: CustomEventInit): CustomEvent; - prototype: CustomEvent; - } | { - new (type: any, options: any): { - "__#3@#detail": any; - readonly detail: any; - }; - }; - export const MessageEvent: { - new (type: string, eventInitDict?: MessageEventInit): MessageEvent; - prototype: MessageEvent; - } | { - new (type: any, options: any): { - "__#4@#detail": any; - "__#4@#data": any; - readonly detail: any; - readonly data: any; - }; - }; - export const ErrorEvent: { - new (type: string, eventInitDict?: ErrorEventInit): ErrorEvent; - prototype: ErrorEvent; - } | { - new (type: any, options: any): { - "__#5@#detail": any; - "__#5@#error": any; - readonly detail: any; - readonly error: any; - }; - }; - export default exports; - export function EventEmitter(): void; - export class EventEmitter { - _events: any; - _eventsCount: number; - _maxListeners: number; - setMaxListeners(n: any): this; - getMaxListeners(): any; - emit(type: any, ...args: any[]): boolean; - addListener(type: any, listener: any): any; - on(arg0: any, arg1: any): any; - prependListener(type: any, listener: any): any; - once(type: any, listener: any): this; - prependOnceListener(type: any, listener: any): this; - removeListener(type: any, listener: any): this; - off(type: any, listener: any): this; - removeAllListeners(type: any, ...args: any[]): this; - listeners(type: any): any[]; - rawListeners(type: any): any[]; - listenerCount(type: any): any; - eventNames(): any; - } - export namespace EventEmitter { - export { EventEmitter }; - export let defaultMaxListeners: number; - export function init(): void; - export function listenerCount(emitter: any, type: any): any; - export { once }; - } - export function once(emitter: any, name: any): Promise; - import * as exports from "socket:events"; - -} -declare module "socket:os" { + +declare module "socket:gc" { /** - * Returns the operating system CPU architecture for which Socket was compiled. - * @returns {string} - 'arm64', 'ia32', 'x64', or 'unknown' + * Track `object` ref to call `Symbol.for('socket.runtime.gc.finalize')` method when + * environment garbage collects object. + * @param {object} object + * @return {boolean} */ - export function arch(): string; + export function ref(object: object, ...args: any[]): boolean; /** - * Returns an array of objects containing information about each CPU/core. - * @returns {Array} cpus - An array of objects containing information about each CPU/core. - * The properties of the objects are: - * - model `` - CPU model name. - * - speed `` - CPU clock speed (in MHz). - * - times `` - An object containing the fields user, nice, sys, idle, irq representing the number of milliseconds the CPU has spent in each mode. - * - user `` - Time spent by this CPU or core in user mode. - * - nice `` - Time spent by this CPU or core in user mode with low priority (nice). - * - sys `` - Time spent by this CPU or core in system mode. - * - idle `` - Time spent by this CPU or core in idle mode. - * - irq `` - Time spent by this CPU or core in IRQ mode. - * @see {@link https://nodejs.org/api/os.html#os_os_cpus} + * Stop tracking `object` ref to call `Symbol.for('socket.runtime.gc.finalize')` method when + * environment garbage collects object. + * @param {object} object + * @return {boolean} */ - export function cpus(): Array; + export function unref(object: object): boolean; /** - * Returns an object containing network interfaces that have been assigned a network address. - * @returns {object} - An object containing network interfaces that have been assigned a network address. - * Each key on the returned object identifies a network interface. The associated value is an array of objects that each describe an assigned network address. - * The properties available on the assigned network address object include: - * - address `` - The assigned IPv4 or IPv6 address. - * - netmask `` - The IPv4 or IPv6 network mask. - * - family `` - The address family ('IPv4' or 'IPv6'). - * - mac `` - The MAC address of the network interface. - * - internal `` - Indicates whether the network interface is a loopback interface. - * - scopeid `` - The numeric scope ID (only specified when family is 'IPv6'). - * - cidr `` - The CIDR notation of the interface. - * @see {@link https://nodejs.org/api/os.html#os_os_networkinterfaces} + * An alias for `unref()` + * @param {object} object} + * @return {boolean} */ - export function networkInterfaces(): object; + export function retain(object: object): boolean; /** - * Returns the operating system platform. - * @returns {string} - 'android', 'cygwin', 'freebsd', 'linux', 'darwin', 'ios', 'openbsd', 'win32', or 'unknown' - * @see {@link https://nodejs.org/api/os.html#os_os_platform} - * The returned value is equivalent to `process.platform`. + * Call finalize on `object` for `gc.finalizer` implementation. + * @param {object} object] + * @return {Promise} */ - export function platform(): string; + export function finalize(object: object, ...args: any[]): Promise; /** - * Returns the operating system name. - * @returns {string} - 'CYGWIN_NT', 'Mac', 'Darwin', 'FreeBSD', 'Linux', 'OpenBSD', 'Windows_NT', 'Win32', or 'Unknown' - * @see {@link https://nodejs.org/api/os.html#os_os_type} + * Calls all pending finalization handlers forcefully. This function + * may have unintended consequences as objects be considered finalized + * and still strongly held (retained) somewhere. */ - export function type(): string; + export function release(): Promise; + export const finalizers: WeakMap; + export const kFinalizer: unique symbol; + export const finalizer: symbol; /** - * @returns {boolean} - `true` if the operating system is Windows. + * @type {Set} */ - export function isWindows(): boolean; + export const pool: Set>; /** - * @returns {string} - The operating system's default directory for temporary files. + * Static registry for objects to clean up underlying resources when they + * are gc'd by the environment. There is no guarantee that the `finalizer()` + * is called at any time. */ - export function tmpdir(): string; + export const registry: FinalizationRegistry; /** - * Get resource usage. + * Default exports which also acts a retained value to persist bound + * `Finalizer#handle()` functions from being gc'd before the + * `FinalizationRegistry` callback is called because `heldValue` must be + * strongly held (retained) in order for the callback to be called. */ - export function rusage(): any; + export const gc: any; + export default gc; /** - * Returns the system uptime in seconds. - * @returns {number} - The system uptime in seconds. + * A container for strongly (retain) referenced finalizer function + * with arguments weakly referenced to an object that will be + * garbage collected. */ - export function uptime(): number; + export class Finalizer { + /** + * Creates a `Finalizer` from input. + */ + static from(handler: any): Finalizer; + /** + * `Finalizer` class constructor. + * @private + * @param {array} args + * @param {function} handle + */ + private constructor(); + args: any[]; + handle: any; + } +} + +declare module "socket:ipc" { + export function maybeMakeError(error: any, caller: any): any; /** - * Returns the operating system name. - * @returns {string} - The operating system name. + * Parses `seq` as integer value + * @param {string|number} seq + * @param {object=} [options] + * @param {boolean} [options.bigint = false] + * @ignore */ - export function uname(): string; + export function parseSeq(seq: string | number, options?: object | undefined): number | bigint; /** - * It's implemented in process.hrtime.bigint() + * If `debug.enabled === true`, then debug output will be printed to console. + * @param {(boolean)} [enable] + * @return {boolean} * @ignore */ - export function hrtime(): any; + export function debug(enable?: (boolean)): boolean; + export namespace debug { + let enabled: boolean; + function log(...args: any[]): any; + } /** - * Node.js doesn't have this method. + * Find transfers for an in worker global `postMessage` + * that is proxied to the main thread. * @ignore */ - export function availableMemory(): any; + export function findMessageTransfers(transfers: any, object: any): any; /** - * The host operating system. This value can be one of: - * - android - * - android-emulator - * - iphoneos - * - iphone-simulator - * - linux - * - macosx - * - unix - * - unknown - * - win32 * @ignore - * @return {'android'|'android-emulator'|'iphoneos'|iphone-simulator'|'linux'|'macosx'|unix'|unknown'|win32'} */ - export function host(): 'android' | 'android-emulator' | 'iphoneos' | iphone; + export function postMessage(message: any, ...args: any[]): any; /** - * @type {string} - * The operating system's end-of-line marker. `'\r\n'` on Windows and `'\n'` on POSIX. + * Waits for the native IPC layer to be ready and exposed on the + * global window object. + * @ignore */ - export const EOL: string; - export default exports; - import * as exports from "socket:os"; - -} -declare module "socket:process" { + export function ready(): Promise; /** - * Adds callback to the 'nextTick' queue. - * @param {Function} callback + * Sends a synchronous IPC command over XHR returning a `Result` + * upon success or error. + * @param {string} command + * @param {any?} [value] + * @param {object?} [options] + * @return {Result} + * @ignore */ - export function nextTick(callback: Function): void; + export function sendSync(command: string, value?: any | null, options?: object | null, buffer?: any): Result; /** - * @returns {string} The home directory of the current user. + * Emit event to be dispatched on `window` object. + * @param {string} name + * @param {any} value + * @param {EventTarget=} [target = window] + * @param {Object=} options */ - export function homedir(): string; + export function emit(name: string, value: any, target?: EventTarget | undefined, options?: any | undefined): Promise; /** - * Computed high resolution time as a `BigInt`. - * @param {Array?} [time] - * @return {bigint} + * Resolves a request by `seq` with possible value. + * @param {string} seq + * @param {any} value + * @ignore */ - export function hrtime(time?: Array | null): bigint; - export namespace hrtime { - function bigint(): any; - } + export function resolve(seq: string, value: any): Promise; /** - * @param {number=} [code=0] - The exit code. Default: 0. + * Sends an async IPC command request with parameters. + * @param {string} command + * @param {any=} value + * @param {object=} [options] + * @param {boolean=} [options.cache=false] + * @param {boolean=} [options.bytes=false] + * @return {Promise} */ - export function exit(code?: number | undefined): Promise; + export function send(command: string, value?: any | undefined, options?: object | undefined): Promise; /** - * Returns an object describing the memory usage of the Node.js process measured in bytes. - * @returns {Object} - */ - export function memoryUsage(): any; - export namespace memoryUsage { - function rss(): any; - } - export default process; - const process: any; -} -declare module "socket:path/path" { - /** - * The path.resolve() method resolves a sequence of paths or path segments into an absolute path. - * @param {strig} ...paths - * @returns {string} - * @see {@link https://nodejs.org/api/path.html#path_path_resolve_paths} - */ - export function resolve(options: any, ...components: any[]): string; - /** - * Computes current working directory for a path - * @param {object=} [opts] - * @param {boolean=} [opts.posix] Set to `true` to force POSIX style path - * @return {string} - */ - export function cwd(opts?: object | undefined): string; - /** - * Computed location origin. Defaults to `socket:///` if not available. - * @return {string} - */ - export function origin(): string; - /** - * Computes the relative path from `from` to `to`. - * @param {object} options - * @param {PathComponent} from - * @param {PathComponent} to - * @return {string} - */ - export function relative(options: object, from: PathComponent, to: PathComponent): string; - /** - * Joins path components. This function may not return an absolute path. - * @param {object} options - * @param {...PathComponent} components - * @return {string} + * Sends an async IPC command request with parameters and buffered bytes. + * @param {string} command + * @param {any=} value + * @param {(Buffer|Uint8Array|ArrayBuffer|string|Array)=} buffer + * @param {object=} options + * @ignore */ - export function join(options: object, ...components: PathComponent[]): string; + export function write(command: string, value?: any | undefined, buffer?: (Buffer | Uint8Array | ArrayBuffer | string | any[]) | undefined, options?: object | undefined): Promise; /** - * Computes directory name of path. - * @param {object} options - * @param {...PathComponent} components - * @return {string} + * Sends an async IPC command request with parameters requesting a response + * with buffered bytes. + * @param {string} command + * @param {any=} value + * @param {object=} options + * @ignore */ - export function dirname(options: object, path: any): string; + export function request(command: string, value?: any | undefined, options?: object | undefined): Promise; /** - * Computes base name of path. - * @param {object} options - * @param {...PathComponent} components - * @return {string} + * Factory for creating a proxy based IPC API. + * @param {string} domain + * @param {(function|object)=} ctx + * @param {string=} [ctx.default] + * @return {Proxy} + * @ignore */ - export function basename(options: object, path: any): string; + export function createBinding(domain: string, ctx?: (Function | object) | undefined): ProxyConstructor; + export function inflateIPCMessageTransfers(object: any, types?: Map): any; + export function findIPCMessageTransfers(transfers: any, object: any): any; /** - * Computes extension name of path. - * @param {object} options - * @param {PathComponent} path - * @return {string} + * Represents an OK IPC status. + * @ignore */ - export function extname(options: object, path: PathComponent): string; + export const OK: 0; /** - * Computes normalized path - * @param {object} options - * @param {PathComponent} path - * @return {string} + * Represents an ERROR IPC status. + * @ignore */ - export function normalize(options: object, path: PathComponent): string; + export const ERROR: 1; /** - * Formats `Path` object into a string. - * @param {object} options - * @param {object|Path} path - * @return {string} + * Timeout in milliseconds for IPC requests. + * @ignore */ - export function format(options: object, path: object | Path): string; + export const TIMEOUT: number; /** - * Parses input `path` into a `Path` instance. - * @param {PathComponent} path - * @return {object} + * Symbol for the `ipc.debug.enabled` property + * @ignore */ - export function parse(path: PathComponent): object; + export const kDebugEnabled: unique symbol; /** - * @typedef {(string|Path|URL|{ pathname: string }|{ url: string)} PathComponent + * @ignore */ + export class Headers extends globalThis.Headers { + /** + * @ignore + */ + static from(input: any): Headers; + /** + * @ignore + */ + get length(): number; + /** + * @ignore + */ + toJSON(): { + [k: string]: string; + }; + } + const Message_base: any; /** - * A container for a parsed Path. + * A container for a IPC message based on a `ipc://` URI scheme. + * @ignore */ - export class Path { + export class Message extends Message_base { + [x: string]: any; /** - * Creates a `Path` instance from `input` and optional `cwd`. - * @param {PathComponent} input - * @param {string} [cwd] + * The expected protocol for an IPC message. + * @ignore */ - static from(input: PathComponent, cwd?: string): any; + static get PROTOCOL(): string; /** - * `Path` class constructor. - * @protected - * @param {string} pathname - * @param {string} [cwd = Path.cwd()] + * Creates a `Message` instance from a variety of input. + * @param {string|URL|Message|Buffer|object} input + * @param {(object|string|URLSearchParams)=} [params] + * @param {(ArrayBuffer|Uint8Array|string)?} [bytes] + * @return {Message} + * @ignore */ - protected constructor(); - pattern: { - "__#2@#i": any; - "__#2@#n": {}; - "__#2@#t": {}; - "__#2@#e": {}; - "__#2@#s": {}; - test(t: {}, r: any): boolean; - exec(t: {}, r: any): { - inputs: any[] | {}[]; - }; - readonly protocol: any; - readonly username: any; - readonly password: any; - readonly hostname: any; - readonly port: any; - readonly pathname: any; - readonly search: any; - readonly hash: any; - }; - url: any; - get pathname(): any; - get protocol(): any; - get href(): any; + static from(input: string | URL | Message | Buffer | object, params?: (object | string | URLSearchParams) | undefined, bytes?: (ArrayBuffer | Uint8Array | string) | null): Message; /** - * `true` if the path is relative, otherwise `false. - * @type {boolean} + * Predicate to determine if `input` is valid for constructing + * a new `Message` instance. + * @param {string|URL|Message|Buffer|object} input + * @return {boolean} + * @ignore */ - get isRelative(): boolean; + static isValidInput(input: string | URL | Message | Buffer | object): boolean; /** - * The working value of this path. + * `Message` class constructor. + * @protected + * @param {string|URL} input + * @param {(object|Uint8Array)?} [bytes] + * @ignore */ - get value(): any; + protected constructor(); /** - * The original source, unresolved. - * @type {string} + * @type {Uint8Array?} + * @ignore */ - get source(): string; + bytes: Uint8Array | null; /** - * Computed parent path. + * Computed IPC message name. * @type {string} + * @ignore */ - get parent(): string; + get command(): string; /** - * Computed root in path. + * Computed IPC message name. * @type {string} + * @ignore */ - get root(): string; + get name(): string; /** - * Computed directory name in path. + * Computed `id` value for the command. * @type {string} + * @ignore */ - get dir(): string; + get id(): string; /** - * Computed base name in path. + * Computed `seq` (sequence) value for the command. * @type {string} + * @ignore */ - get base(): string; + get seq(): string; /** - * Computed base name in path without path extension. + * Computed message value potentially given in message parameters. + * This value is automatically decoded, but not treated as JSON. * @type {string} + * @ignore */ - get name(): string; + get value(): string; /** - * Computed extension name in path. - * @type {string} + * Computed `index` value for the command potentially referring to + * the window index the command is scoped to or originating from. If not + * specified in the message parameters, then this value defaults to `-1`. + * @type {number} + * @ignore */ - get ext(): string; + get index(): number; /** - * The computed drive, if given in the path. - * @type {string?} + * Computed value parsed as JSON. This value is `null` if the value is not present + * or it is invalid JSON. + * @type {object?} + * @ignore */ - get drive(): string; + get json(): any; /** - * @return {URL} + * Computed readonly object of message parameters. + * @type {object} + * @ignore */ - toURL(): URL; + get params(): any; /** - * Converts this `Path` instance to a string. - * @return {string} + * Gets unparsed message parameters. + * @type {Array>} + * @ignore */ - toString(): string; + get rawParams(): string[][]; /** + * Returns computed parameters as entries + * @return {Array>} * @ignore */ - inspect(): { - root: string; - dir: string; - base: string; - ext: string; - name: string; - }; + entries(): Array>; /** + * Set a parameter `value` by `key`. + * @param {string} key + * @param {any} value * @ignore */ - [Symbol.toStringTag](): string; - #private; - } - export default Path; - export type PathComponent = (string | Path | URL | { - pathname: string; - } | { - url: string; - }); - import { URL } from "socket:url/index"; -} -declare module "socket:path/well-known" { - /** - * Well known path to the user's "Downloads" folder. - * @type {?string} - */ - export const DOWNLOADS: string | null; - /** - * Well known path to the user's "Documents" folder. - * @type {?string} - */ - export const DOCUMENTS: string | null; - /** - * Well known path to the user's "Pictures" folder. - * @type {?string} - */ - export const PICTURES: string | null; + set(key: string, value: any): any; + /** + * Get a parameter value by `key`. + * @param {string} key + * @param {any=} [defaultValue] + * @return {any} + * @ignore + */ + get(key: string, defaultValue?: any | undefined): any; + /** + * Delete a parameter by `key`. + * @param {string} key + * @return {boolean} + * @ignore + */ + delete(key: string): boolean; + /** + * Computed parameter keys. + * @return {Array} + * @ignore + */ + keys(): Array; + /** + * Computed parameter values. + * @return {Array} + * @ignore + */ + values(): Array; + /** + * Predicate to determine if parameter `key` is present in parameters. + * @param {string} key + * @return {boolean} + * @ignore + */ + has(key: string): boolean; + } /** - * Well known path to the user's "Desktop" folder. - * @type {?string} + * A result type used internally for handling + * IPC result values from the native layer that are in the form + * of `{ err?, data? }`. The `data` and `err` properties on this + * type of object are in tuple form and be accessed at `[data?,err?]` + * @ignore */ - export const DESKTOP: string | null; + export class Result { + /** + * Creates a `Result` instance from input that may be an object + * like `{ err?, data? }`, an `Error` instance, or just `data`. + * @param {(object|Error|any)?} result + * @param {Error|object} [maybeError] + * @param {string} [maybeSource] + * @param {object|string|Headers} [maybeHeaders] + * @return {Result} + * @ignore + */ + static from(result: (object | Error | any) | null, maybeError?: Error | object, maybeSource?: string, maybeHeaders?: object | string | Headers): Result; + /** + * `Result` class constructor. + * @private + * @param {string?} [id = null] + * @param {Error?} [err = null] + * @param {object?} [data = null] + * @param {string?} [source = null] + * @param {(object|string|Headers)?} [headers = null] + * @ignore + */ + private constructor(); + /** + * The unique ID for this result. + * @type {string} + * @ignore + */ + id: string; + /** + * An optional error in the result. + * @type {Error?} + * @ignore + */ + err: Error | null; + /** + * Result data if given. + * @type {(string|object|Uint8Array)?} + * @ignore + */ + data: (string | object | Uint8Array) | null; + /** + * The source of this result. + * @type {string?} + * @ignore + */ + source: string | null; + /** + * Result headers, if given. + * @type {Headers?} + * @ignore + */ + headers: Headers | null; + /** + * Computed result length. + * @ignore + */ + get length(): any; + /** + * @ignore + */ + toJSON(): { + headers: { + [k: string]: string; + }; + source: string; + data: any; + err: { + name: string; + message: string; + stack?: string; + cause?: unknown; + type: any; + code: any; + }; + }; + /** + * Generator for an `Iterable` interface over this instance. + * @ignore + */ + [Symbol.iterator](): Generator; + } + export class IPCSearchParams extends URLSearchParams { + constructor(params: any, nonce?: any); + } /** - * Well known path to the user's "Videos" folder. - * @type {?string} + * @ignore */ - export const VIDEOS: string | null; + export const primordials: any; + export const ports: Map; + export class IPCMessagePort extends MessagePort { + static from(options?: any): any; + static create(options?: any): any; + get id(): any; + get started(): any; + get closed(): any; + set onmessage(onmessage: any); + get onmessage(): any; + set onmessageerror(onmessageerror: any); + get onmessageerror(): any; + close(purge?: boolean): void; + postMessage(message: any, optionsOrTransferList: any): void; + addEventListener(...args: any[]): any; + removeEventListener(...args: any[]): any; + dispatchEvent(event: any): any; + } + export class IPCMessageChannel extends MessageChannel { + constructor(options?: any); + get id(): any; + get port1(): any; + get port2(): any; + get channel(): any; + #private; + } + export default exports; + import { Buffer } from "socket:buffer"; + import { URL } from "socket:url/index"; + import * as exports from "socket:ipc"; + +} + +declare module "socket:os/constants" { + export type errno = number; /** - * Well known path to the user's "Music" folder. - * @type {?string} + * @typedef {number} errno + * @typedef {number} signal */ - export const MUSIC: string | null; /** - * Well known path to the application's "resources" folder. - * @type {?string} + * A container for all known "errno" constant values. + * Unsupported values have a default value of `0`. */ - export const RESOURCES: string | null; + export const errno: any; + export type signal = number; /** - * Well known path to the application's "home" folder. - * This may be the user's HOME directory or the application container sandbox. - * @type {?string} + * A container for all known "signal" constant values. + * Unsupported values have a default value of `0`. */ - export const HOME: string | null; + export const signal: any; namespace _default { - export { DOWNLOADS }; - export { DOCUMENTS }; - export { RESOURCES }; - export { PICTURES }; - export { DESKTOP }; - export { VIDEOS }; - export { MUSIC }; - export { HOME }; + export { errno }; + export { signal }; } export default _default; } -declare module "socket:path/win32" { - /** - * Computes current working directory for a path - * @param {string} - */ - export function cwd(): any; - /** - * Resolves path components to an absolute path. - * @param {...PathComponent} components - * @return {string} - */ - export function resolve(...components: PathComponent[]): string; + +declare module "socket:errno" { /** - * Joins path components. This function may not return an absolute path. - * @param {...PathComponent} components + * Converts an `errno` code to its corresponding string message. + * @param {import('./os/constants.js').errno} {code} * @return {string} */ - export function join(...components: PathComponent[]): string; + export function toString(code: any): string; /** - * Computes directory name of path. - * @param {PathComponent} path - * @return {string} + * Gets the code for a given 'errno' name. + * @param {string|number} name + * @return {errno} */ - export function dirname(path: PathComponent): string; + export function getCode(name: string | number): errno; /** - * Computes base name of path. - * @param {PathComponent} path - * @param {string} suffix + * Gets the name for a given 'errno' code * @return {string} + * @param {string|number} code */ - export function basename(path: PathComponent, suffix: string): string; + export function getName(code: string | number): string; /** - * Computes extension name of path. - * @param {PathComponent} path + * Gets the message for a 'errno' code. + * @param {number|string} code * @return {string} */ - export function extname(path: PathComponent): string; + export function getMessage(code: number | string): string; + /** + * @typedef {import('./os/constants.js').errno} errno + */ + export const E2BIG: any; + export const EACCES: any; + export const EADDRINUSE: any; + export const EADDRNOTAVAIL: any; + export const EAFNOSUPPORT: any; + export const EAGAIN: any; + export const EALREADY: any; + export const EBADF: any; + export const EBADMSG: any; + export const EBUSY: any; + export const ECANCELED: any; + export const ECHILD: any; + export const ECONNABORTED: any; + export const ECONNREFUSED: any; + export const ECONNRESET: any; + export const EDEADLK: any; + export const EDESTADDRREQ: any; + export const EDOM: any; + export const EDQUOT: any; + export const EEXIST: any; + export const EFAULT: any; + export const EFBIG: any; + export const EHOSTUNREACH: any; + export const EIDRM: any; + export const EILSEQ: any; + export const EINPROGRESS: any; + export const EINTR: any; + export const EINVAL: any; + export const EIO: any; + export const EISCONN: any; + export const EISDIR: any; + export const ELOOP: any; + export const EMFILE: any; + export const EMLINK: any; + export const EMSGSIZE: any; + export const EMULTIHOP: any; + export const ENAMETOOLONG: any; + export const ENETDOWN: any; + export const ENETRESET: any; + export const ENETUNREACH: any; + export const ENFILE: any; + export const ENOBUFS: any; + export const ENODATA: any; + export const ENODEV: any; + export const ENOENT: any; + export const ENOEXEC: any; + export const ENOLCK: any; + export const ENOLINK: any; + export const ENOMEM: any; + export const ENOMSG: any; + export const ENOPROTOOPT: any; + export const ENOSPC: any; + export const ENOSR: any; + export const ENOSTR: any; + export const ENOSYS: any; + export const ENOTCONN: any; + export const ENOTDIR: any; + export const ENOTEMPTY: any; + export const ENOTSOCK: any; + export const ENOTSUP: any; + export const ENOTTY: any; + export const ENXIO: any; + export const EOPNOTSUPP: any; + export const EOVERFLOW: any; + export const EPERM: any; + export const EPIPE: any; + export const EPROTO: any; + export const EPROTONOSUPPORT: any; + export const EPROTOTYPE: any; + export const ERANGE: any; + export const EROFS: any; + export const ESPIPE: any; + export const ESRCH: any; + export const ESTALE: any; + export const ETIME: any; + export const ETIMEDOUT: any; + export const ETXTBSY: any; + export const EWOULDBLOCK: any; + export const EXDEV: any; + export const strings: any; + export { constants }; + namespace _default { + export { constants }; + export { strings }; + export { toString }; + export { getCode }; + export { getMessage }; + } + export default _default; + export type errno = import("socket:os/constants").errno; + import { errno as constants } from "socket:os/constants"; +} + +declare module "socket:errors" { + export default exports; + export const ABORT_ERR: any; + export const ENCODING_ERR: any; + export const INVALID_ACCESS_ERR: any; + export const INDEX_SIZE_ERR: any; + export const NETWORK_ERR: any; + export const NOT_ALLOWED_ERR: any; + export const NOT_FOUND_ERR: any; + export const NOT_SUPPORTED_ERR: any; + export const OPERATION_ERR: any; + export const SECURITY_ERR: any; + export const TIMEOUT_ERR: any; /** - * Predicate helper to determine if path is absolute. - * @param {PathComponent} path - * @return {boolean} + * An `AbortError` is an error type thrown in an `onabort()` level 0 + * event handler on an `AbortSignal` instance. */ - export function isAbsolute(path: PathComponent): boolean; + export class AbortError extends Error { + /** + * The code given to an `ABORT_ERR` `DOMException` + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + */ + static get code(): any; + /** + * `AbortError` class constructor. + * @param {AbortSignal|string} reasonOrSignal + * @param {AbortSignal=} [signal] + */ + constructor(reason: any, signal?: AbortSignal | undefined, ...args: any[]); + signal: AbortSignal; + get name(): string; + get code(): string; + } /** - * Parses input `path` into a `Path` instance. - * @param {PathComponent} path - * @return {Path} + * An `BadRequestError` is an error type thrown in an `onabort()` level 0 + * event handler on an `BadRequestSignal` instance. */ - export function parse(path: PathComponent): Path; - /** - * Formats `Path` object into a string. - * @param {object|Path} path - * @return {string} - */ - export function format(path: object | Path): string; - /** - * Normalizes `path` resolving `..` and `.\` preserving trailing - * slashes. - * @param {string} path - */ - export function normalize(path: string): any; - /** - * Computes the relative path from `from` to `to`. - * @param {string} from - * @param {string} to - * @return {string} - */ - export function relative(from: string, to: string): string; - export default exports; - export namespace win32 { - let sep: "\\"; - let delimiter: ";"; + export class BadRequestError extends Error { + /** + * The default code given to a `BadRequestError` + */ + static get code(): number; + /** + * `BadRequestError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; } - export type PathComponent = import("socket:path/path").PathComponent; - import { Path } from "socket:path/path"; - import * as posix from "socket:path/posix"; - import { RESOURCES } from "socket:path/well-known"; - import { DOWNLOADS } from "socket:path/well-known"; - import { DOCUMENTS } from "socket:path/well-known"; - import { PICTURES } from "socket:path/well-known"; - import { DESKTOP } from "socket:path/well-known"; - import { VIDEOS } from "socket:path/well-known"; - import { MUSIC } from "socket:path/well-known"; - import * as exports from "socket:path/win32"; - - export { posix, Path, RESOURCES, DOWNLOADS, DOCUMENTS, PICTURES, DESKTOP, VIDEOS, MUSIC }; -} -declare module "socket:path/posix" { /** - * Computes current working directory for a path - * @param {string} - * @return {string} + * An `EncodingError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function cwd(): string; + export class EncodingError extends Error { + /** + * The code given to an `ENCODING_ERR` `DOMException`. + */ + static get code(): any; + /** + * `EncodingError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } /** - * Resolves path components to an absolute path. - * @param {...PathComponent} components - * @return {string} + * An error type derived from an `errno` code. */ - export function resolve(...components: PathComponent[]): string; + export class ErrnoError extends Error { + static get code(): string; + static errno: any; + /** + * `ErrnoError` class constructor. + * @param {import('./errno').errno|string} code + */ + constructor(code: import("socket:errno").errno | string, message?: any, ...args: any[]); + get name(): string; + get code(): number; + #private; + } /** - * Joins path components. This function may not return an absolute path. - * @param {...PathComponent} components - * @return {string} + * An `FinalizationRegistryCallbackError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function join(...components: PathComponent[]): string; + export class FinalizationRegistryCallbackError extends Error { + /** + * The default code given to an `FinalizationRegistryCallbackError` + */ + static get code(): number; + /** + * `FinalizationRegistryCallbackError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } /** - * Computes directory name of path. - * @param {PathComponent} path - * @return {string} + * An `IllegalConstructorError` is an error type thrown when a constructor is + * called for a class constructor when it shouldn't be. */ - export function dirname(path: PathComponent): string; + export class IllegalConstructorError extends TypeError { + /** + * The default code given to an `IllegalConstructorError` + */ + static get code(): number; + /** + * `IllegalConstructorError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } /** - * Computes base name of path. - * @param {PathComponent} path - * @param {string} suffix - * @return {string} + * An `IndexSizeError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function basename(path: PathComponent, suffix: string): string; + export class IndexSizeError extends Error { + /** + * The code given to an `INDEX_SIZE_ERR` `DOMException` + */ + static get code(): any; + /** + * `IndexSizeError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } + export const kInternalErrorCode: unique symbol; /** - * Computes extension name of path. - * @param {PathComponent} path - * @return {string} + * An `InternalError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function extname(path: PathComponent): string; + export class InternalError extends Error { + /** + * The default code given to an `InternalError` + */ + static get code(): number; + /** + * `InternalError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, code?: number, ...args: any[]); + get name(): string; + /** + * @param {number|string} + */ + set code(code: string | number); + /** + * @type {number|string} + */ + get code(): string | number; + [exports.kInternalErrorCode]: number; + } /** - * Predicate helper to determine if path is absolute. - * @param {PathComponent} path - * @return {boolean} + * An `InvalidAccessError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function isAbsolute(path: PathComponent): boolean; + export class InvalidAccessError extends Error { + /** + * The code given to an `INVALID_ACCESS_ERR` `DOMException` + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + */ + static get code(): any; + /** + * `InvalidAccessError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } /** - * Parses input `path` into a `Path` instance. - * @param {PathComponent} path - * @return {Path} + * An `NetworkError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function parse(path: PathComponent): Path; + export class NetworkError extends Error { + /** + * The code given to an `NETWORK_ERR` `DOMException` + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + */ + static get code(): any; + /** + * `NetworkError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } /** - * Formats `Path` object into a string. - * @param {object|Path} path - * @return {string} + * An `NotAllowedError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function format(path: object | Path): string; + export class NotAllowedError extends Error { + /** + * The code given to an `NOT_ALLOWED_ERR` `DOMException` + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + */ + static get code(): any; + /** + * `NotAllowedError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } /** - * Normalizes `path` resolving `..` and `./` preserving trailing - * slashes. - * @param {string} path + * An `NotFoundError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function normalize(path: string): any; + export class NotFoundError extends Error { + /** + * The code given to an `NOT_FOUND_ERR` `DOMException` + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + */ + static get code(): any; + /** + * `NotFoundError` class constructor. + * @param {string} message + * @param {number} [code] + */ + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } /** - * Computes the relative path from `from` to `to`. - * @param {string} from - * @param {string} to - * @return {string} + * An `NotSupportedError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export function relative(from: string, to: string): string; - export default exports; - export namespace posix { - let sep: "/"; - let delimiter: ":"; - } - export type PathComponent = import("socket:path/path").PathComponent; - import { Path } from "socket:path/path"; - import * as win32 from "socket:path/win32"; - import { RESOURCES } from "socket:path/well-known"; - import { DOWNLOADS } from "socket:path/well-known"; - import { DOCUMENTS } from "socket:path/well-known"; - import { PICTURES } from "socket:path/well-known"; - import { DESKTOP } from "socket:path/well-known"; - import { VIDEOS } from "socket:path/well-known"; - import { MUSIC } from "socket:path/well-known"; - import * as exports from "socket:path/posix"; - - export { win32, Path, RESOURCES, DOWNLOADS, DOCUMENTS, PICTURES, DESKTOP, VIDEOS, MUSIC }; -} -declare module "socket:path/index" { - export * as _default from "socket:path/index"; - - import * as posix from "socket:path/posix"; - import * as win32 from "socket:path/win32"; - import { Path } from "socket:path/path"; - import { RESOURCES } from "socket:path/well-known"; - import { DOWNLOADS } from "socket:path/well-known"; - import { DOCUMENTS } from "socket:path/well-known"; - import { PICTURES } from "socket:path/well-known"; - import { DESKTOP } from "socket:path/well-known"; - import { VIDEOS } from "socket:path/well-known"; - import { MUSIC } from "socket:path/well-known"; - import { HOME } from "socket:path/well-known"; - export { posix, win32, Path, RESOURCES, DOWNLOADS, DOCUMENTS, PICTURES, DESKTOP, VIDEOS, MUSIC, HOME }; -} -declare module "socket:path" { - export const sep: "/" | "\\"; - export const delimiter: ";" | ":"; - export const resolve: typeof posix.win32.resolve; - export const join: typeof posix.win32.join; - export const dirname: typeof posix.win32.dirname; - export const basename: typeof posix.win32.basename; - export const extname: typeof posix.win32.extname; - export const cwd: typeof posix.win32.cwd; - export const isAbsolute: typeof posix.win32.isAbsolute; - export const parse: typeof posix.win32.parse; - export const format: typeof posix.win32.format; - export const normalize: typeof posix.win32.normalize; - export const relative: typeof posix.win32.relative; - const _default: typeof posix | typeof posix.win32; - export default _default; - import { posix } from "socket:path/index"; - import { Path } from "socket:path/index"; - import { win32 } from "socket:path/index"; - import { RESOURCES } from "socket:path/index"; - import { DOWNLOADS } from "socket:path/index"; - import { DOCUMENTS } from "socket:path/index"; - import { PICTURES } from "socket:path/index"; - import { DESKTOP } from "socket:path/index"; - import { VIDEOS } from "socket:path/index"; - import { MUSIC } from "socket:path/index"; - import { HOME } from "socket:path/index"; - export { Path, posix, win32, RESOURCES, DOWNLOADS, DOCUMENTS, PICTURES, DESKTOP, VIDEOS, MUSIC, HOME }; -} -declare module "socket:diagnostics/channels" { - /** - * Normalizes a channel name to lower case replacing white space, - * hyphens (-), underscores (_), with dots (.). - * @ignore - */ - export function normalizeName(group: any, name: any): string; - /** - * Used to preallocate a minimum sized array of subscribers for - * a channel. - * @ignore - */ - export const MIN_CHANNEL_SUBSCRIBER_SIZE: 64; - /** - * A general interface for diagnostic channels that can be subscribed to. - */ - export class Channel { - constructor(name: any); - name: any; - group: any; - /** - * Computed subscribers for all channels in this group. - * @type {Array} - */ - get subscribers(): Function[]; - /** - * Accessor for determining if channel has subscribers. This - * is always `false` for `Channel instances and `true` for `ActiveChannel` - * instances. - */ - get hasSubscribers(): boolean; - /** - * Computed number of subscribers for this channel. - */ - get length(): number; - /** - * Resets channel state. - * @param {(boolean)} [shouldOrphan = false] - */ - reset(shouldOrphan?: (boolean)): void; - channel(name: any): Channel; - /** - * Adds an `onMessage` subscription callback to the channel. - * @return {boolean} - */ - subscribe(_: any, onMessage: any): boolean; - /** - * Removes an `onMessage` subscription callback from the channel. - * @param {function} onMessage - * @return {boolean} - */ - unsubscribe(_: any, onMessage: Function): boolean; - /** - * A no-op for `Channel` instances. This function always returns `false`. - * @param {string} name - * @param {object} message - * @return Promise - */ - publish(name: string, message: object): Promise; - /** - * Returns a string representation of the `ChannelRegistry`. - * @ignore - */ - toString(): string; + export class NotSupportedError extends Error { /** - * Iterator interface - * @ignore + * The code given to an `NOT_SUPPORTED_ERR` `DOMException` + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ - get [Symbol.iterator](): any[]; + static get code(): any; /** - * The `Channel` string tag. - * @ignore + * `NotSupportedError` class constructor. + * @param {string} message + * @param {number} [code] */ - [Symbol.toStringTag](): string; - #private; + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; } /** - * An `ActiveChannel` is a prototype implementation for a `Channel` - * that provides an interface what is considered an "active" channel. The - * `hasSubscribers` accessor always returns `true` for this class. + * An `ModuleNotFoundError` is an error type thrown when an imported or + * required module is not found. */ - export class ActiveChannel extends Channel { - unsubscribe(onMessage: any): boolean; + export class ModuleNotFoundError extends exports.NotFoundError { /** - * @param {object|any} message - * @return Promise + * `ModuleNotFoundError` class constructor. + * @param {string} message + * @param {string[]=} [requireStack] */ - publish(message: object | any): Promise; + constructor(message: string, requireStack?: string[] | undefined); + requireStack: string[]; } /** - * A container for a grouping of channels that are named and owned - * by this group. A `ChannelGroup` can also be a regular channel. + * An `OperationError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export class ChannelGroup extends Channel { - /** - * @param {Array} channels - * @param {string} name - */ - constructor(name: string, channels: Array); - channels: Channel[]; - /** - * Subscribe to a channel or selection of channels in this group. - * @param {string} name - * @return {boolean} - */ - subscribe(name: string, onMessage: any): boolean; - /** - * Unsubscribe from a channel or selection of channels in this group. - * @param {string} name - * @return {boolean} - */ - unsubscribe(name: string, onMessage: any): boolean; + export class OperationError extends Error { /** - * Gets or creates a channel for this group. - * @param {string} name - * @return {Channel} + * The code given to an `OPERATION_ERR` `DOMException` */ - channel(name: string): Channel; + static get code(): any; /** - * Select a test of channels from this group. - * The following syntax is supported: - * - One Channel: `group.channel` - * - All Channels: `*` - * - Many Channel: `group.*` - * - Collections: `['group.a', 'group.b', 'group.c'] or `group.a,group.b,group.c` - * @param {string|Array} keys - * @param {(boolean)} [hasSubscribers = false] - Enforce subscribers in selection - * @return {Array<{name: string, channel: Channel}>} + * `OperationError` class constructor. + * @param {string} message + * @param {number} [code] */ - select(keys: string | Array, hasSubscribers?: (boolean)): Array<{ - name: string; - channel: Channel; - }>; + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; } /** - * An object mapping of named channels to `WeakRef` instances. + * An `SecurityError` is an error type thrown when an internal exception + * has occurred, such as in the native IPC layer. */ - export const registry: { - /** - * Subscribes callback `onMessage` to channel of `name`. - * @param {string} name - * @param {function} onMessage - * @return {boolean} - */ - subscribe(name: string, onMessage: Function): boolean; - /** - * Unsubscribes callback `onMessage` from channel of `name`. - * @param {string} name - * @param {function} onMessage - * @return {boolean} - */ - unsubscribe(name: string, onMessage: Function): boolean; + export class SecurityError extends Error { /** - * Predicate to determine if a named channel has subscribers. - * @param {string} name + * The code given to an `SECURITY_ERR` `DOMException` */ - hasSubscribers(name: string): boolean; + static get code(): any; /** - * Get or set a channel by `name`. - * @param {string} name - * @return {Channel} + * `SecurityError` class constructor. + * @param {string} message + * @param {number} [code] */ - channel(name: string): Channel; + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; + } + /** + * An `TimeoutError` is an error type thrown when an operation timesout. + */ + export class TimeoutError extends Error { /** - * Creates a `ChannelGroup` for a set of channels - * @param {string} name - * @param {Array} [channels] - * @return {ChannelGroup} + * The code given to an `TIMEOUT_ERR` `DOMException` + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ - group(name: string, channels?: Array): ChannelGroup; + static get code(): any; /** - * Get a channel by name. The name is normalized. - * @param {string} name - * @return {Channel?} + * `TimeoutError` class constructor. + * @param {string} message */ - get(name: string): Channel | null; - /** - * Checks if a channel is known by name. The name is normalized. - * @param {string} name - * @return {boolean} - */ - has(name: string): boolean; - /** - * Set a channel by name. The name is normalized. - * @param {string} name - * @param {Channel} channel - * @return {Channel?} - */ - set(name: string, channel: Channel): Channel | null; - /** - * Removes a channel by `name` - * @return {boolean} - */ - remove(name: any): boolean; - /** - * Returns a string representation of the `ChannelRegistry`. - * @ignore - */ - toString(): string; - /** - * Returns a JSON representation of the `ChannelRegistry`. - * @return {object} - */ - toJSON(): object; - /** - * The `ChannelRegistry` string tag. - * @ignore - */ - [Symbol.toStringTag](): string; - }; - export default registry; -} -declare module "socket:diagnostics/metric" { - export class Metric { - init(): void; - update(value: any): void; - destroy(): void; - toJSON(): {}; - toString(): string; - [Symbol.iterator](): any; - [Symbol.toStringTag](): string; - } - export default Metric; -} -declare module "socket:diagnostics/window" { - export class RequestAnimationFrameMetric extends Metric { - constructor(options: any); - originalRequestAnimationFrame: typeof requestAnimationFrame; - requestAnimationFrame(callback: any): any; - sampleSize: any; - sampleTick: number; - channel: import("socket:diagnostics/channels").Channel; - value: { - rate: number; - samples: number; - }; - now: number; - samples: Uint8Array; - toJSON(): { - sampleSize: any; - sampleTick: number; - samples: number[]; - rate: number; - now: number; - }; - } - export class FetchMetric extends Metric { - constructor(options: any); - originalFetch: typeof fetch; - channel: import("socket:diagnostics/channels").Channel; - fetch(resource: any, options: any, extra: any): Promise; - } - export class XMLHttpRequestMetric extends Metric { - constructor(options: any); - channel: import("socket:diagnostics/channels").Channel; - patched: { - open: { - (method: string, url: string | URL): void; - (method: string, url: string | URL, async: boolean, username?: string, password?: string): void; - }; - send: (body?: Document | XMLHttpRequestBodyInit) => void; - }; - } - export class WorkerMetric extends Metric { - constructor(options: any); - GlobalWorker: { - new (scriptURL: string | URL, options?: WorkerOptions): Worker; - prototype: Worker; - }; - channel: import("socket:diagnostics/channels").Channel; - Worker: { - new (url: any, options: any, ...args: any[]): { - onmessage: (this: Worker, ev: MessageEvent) => any; - onmessageerror: (this: Worker, ev: MessageEvent) => any; - postMessage(message: any, transfer: Transferable[]): void; - postMessage(message: any, options?: StructuredSerializeOptions): void; - terminate(): void; - addEventListener(type: K, listener: (this: Worker, ev: WorkerEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K_1, listener: (this: Worker, ev: WorkerEventMap[K_1]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - dispatchEvent(event: Event): boolean; - onerror: (this: AbstractWorker, ev: ErrorEvent) => any; - }; - }; - } - export const metrics: { - requestAnimationFrame: RequestAnimationFrameMetric; - XMLHttpRequest: XMLHttpRequestMetric; - Worker: WorkerMetric; - fetch: FetchMetric; - channel: import("socket:diagnostics/channels").ChannelGroup; - subscribe(...args: any[]): boolean; - unsubscribe(...args: any[]): boolean; - start(which: any): void; - stop(which: any): void; - }; - namespace _default { - export { metrics }; + constructor(message: string, ...args: any[]); + get name(): string; + get code(): string; } - export default _default; - import { Metric } from "socket:diagnostics/metric"; + import * as exports from "socket:errors"; + } -declare module "socket:diagnostics/index" { + +declare module "socket:util/types" { /** - * @param {string} name - * @return {import('./channels.js').Channel} + * Returns `true` if input is a plan `Object` instance. + * @param {any} input + * @return {boolean} */ - export function channel(name: string): import("socket:diagnostics/channels").Channel; - export default exports; - import * as exports from "socket:diagnostics/index"; - import channels from "socket:diagnostics/channels"; - import window from "socket:diagnostics/window"; - - export { channels, window }; -} -declare module "socket:diagnostics" { - export * from "socket:diagnostics/index"; - export default exports; - import * as exports from "socket:diagnostics/index"; -} -declare module "socket:gc" { + export function isPlainObject(input: any): boolean; /** - * Track `object` ref to call `Symbol.for('gc.finalize')` method when - * environment garbage collects object. - * @param {object} object + * Returns `true` if input is an `AsyncFunction` + * @param {any} input * @return {boolean} */ - export function ref(object: object, ...args: any[]): boolean; + export function isAsyncFunction(input: any): boolean; /** - * Stop tracking `object` ref to call `Symbol.for('gc.finalize')` method when - * environment garbage collects object. - * @param {object} object + * Returns `true` if input is an `Function` + * @param {any} input * @return {boolean} */ - export function unref(object: object): boolean; + export function isFunction(input: any): boolean; /** - * An alias for `unref()` - * @param {object} object} + * Returns `true` if input is an `AsyncFunction` object. + * @param {any} input * @return {boolean} */ - export function retain(object: object): boolean; + export function isAsyncFunctionObject(input: any): boolean; /** - * Call finalize on `object` for `gc.finalizer` implementation. - * @param {object} object] - * @return {Promise} + * Returns `true` if input is an `Function` object. + * @param {any} input + * @return {boolean} */ - export function finalize(object: object, ...args: any[]): Promise; + export function isFunctionObject(input: any): boolean; /** - * Calls all pending finalization handlers forcefully. This function - * may have unintended consequences as objects be considered finalized - * and still strongly held (retained) somewhere. + * Always returns `false`. + * @param {any} input + * @return {boolean} */ - export function release(): Promise; - export const finalizers: WeakMap; - export const kFinalizer: unique symbol; - export const finalizer: symbol; - export const pool: Set; + export function isExternal(input: any): boolean; /** - * Static registry for objects to clean up underlying resources when they - * are gc'd by the environment. There is no guarantee that the `finalizer()` - * is called at any time. + * Returns `true` if input is a `Date` instance. + * @param {any} input + * @return {boolean} */ - export const registry: FinalizationRegistry; + export function isDate(input: any): boolean; /** - * Default exports which also acts a retained value to persist bound - * `Finalizer#handle()` functions from being gc'd before the - * `FinalizationRegistry` callback is called because `heldValue` must be - * strongly held (retained) in order for the callback to be called. + * Returns `true` if input is an `arguments` object. + * @param {any} input + * @return {boolean} */ - export const gc: any; - export default gc; + export function isArgumentsObject(input: any): boolean; /** - * A container for strongly (retain) referenced finalizer function - * with arguments weakly referenced to an object that will be - * garbage collected. + * Returns `true` if input is a `BigInt` object. + * @param {any} input + * @return {boolean} */ - export class Finalizer { - /** - * Creates a `Finalizer` from input. - */ - static from(handler: any): Finalizer; - /** - * `Finalizer` class constructor. - * @private - * @param {array} args - * @param {function} handle - */ - private constructor(); - args: any[]; - handle: any; - } -} -declare module "socket:stream" { - export function pipelinePromise(...streams: any[]): Promise; - export function pipeline(stream: any, ...streams: any[]): any; - export function isStream(stream: any): boolean; - export function isStreamx(stream: any): boolean; - export function isReadStreamx(stream: any): any; - export default exports; - export class FixedFIFO { - constructor(hwm: any); - buffer: any[]; - mask: number; - top: number; - btm: number; - next: any; - push(data: any): boolean; - shift(): any; - isEmpty(): boolean; - } - export class FastFIFO { - constructor(hwm: any); - hwm: any; - head: exports.FixedFIFO; - tail: exports.FixedFIFO; - push(val: any): void; - shift(): any; - isEmpty(): boolean; - } - export class WritableState { - constructor(stream: any, { highWaterMark, map, mapWritable, byteLength, byteLengthWritable }?: { - highWaterMark?: number; - map?: any; - mapWritable: any; - byteLength: any; - byteLengthWritable: any; - }); - stream: any; - queue: exports.FastFIFO; - highWaterMark: number; - buffered: number; - error: any; - pipeline: any; - byteLength: any; - map: any; - afterWrite: any; - afterUpdateNextTick: any; - get ended(): boolean; - push(data: any): boolean; - shift(): any; - end(data: any): void; - autoBatch(data: any, cb: any): any; - update(): void; - updateNonPrimary(): void; - updateNextTick(): void; - } - export class ReadableState { - constructor(stream: any, { highWaterMark, map, mapReadable, byteLength, byteLengthReadable }?: { - highWaterMark?: number; - map?: any; - mapReadable: any; - byteLength: any; - byteLengthReadable: any; - }); - stream: any; - queue: exports.FastFIFO; - highWaterMark: number; - buffered: number; - error: any; - pipeline: exports.Pipeline; - byteLength: any; - map: any; - pipeTo: any; - afterRead: any; - afterUpdateNextTick: any; - get ended(): boolean; - pipe(pipeTo: any, cb: any): void; - push(data: any): boolean; - shift(): any; - unshift(data: any): void; - read(): any; - drain(): void; - update(): void; - updateNonPrimary(): void; - updateNextTick(): void; - } - export class TransformState { - constructor(stream: any); - data: any; - afterTransform: any; - afterFinal: any; - } - export class Pipeline { - constructor(src: any, dst: any, cb: any); - from: any; - to: any; - afterPipe: any; - error: any; - pipeToFinished: boolean; - finished(): void; - done(stream: any, err: any): void; - } - export class Stream extends EventEmitter { - constructor(opts: any); - _duplexState: number; - _readableState: any; - _writableState: any; - _open(cb: any): void; - _destroy(cb: any): void; - _predestroy(): void; - get readable(): boolean; - get writable(): boolean; - get destroyed(): boolean; - get destroying(): boolean; - destroy(err: any): void; - on(name: any, fn: any): any; - } - export class Readable extends exports.Stream { - static _fromAsyncIterator(ite: any, opts: any): exports.Readable; - static from(data: any, opts: any): any; - static isBackpressured(rs: any): boolean; - static isPaused(rs: any): boolean; - _readableState: exports.ReadableState; - _read(cb: any): void; - pipe(dest: any, cb: any): any; - read(): any; - push(data: any): boolean; - unshift(data: any): void; - resume(): this; - pause(): this; - } - export class Writable extends exports.Stream { - static isBackpressured(ws: any): boolean; - _writableState: exports.WritableState; - _writev(batch: any, cb: any): void; - _write(data: any, cb: any): void; - _final(cb: any): void; - write(data: any): boolean; - end(data: any): this; - } - export class Duplex extends exports.Readable { - _writableState: exports.WritableState; - _writev(batch: any, cb: any): void; - _write(data: any, cb: any): void; - _final(cb: any): void; - write(data: any): boolean; - end(data: any): this; - } - export class Transform extends exports.Duplex { - _transformState: exports.TransformState; - _transform(data: any, cb: any): void; - _flush(cb: any): void; - } - export class PassThrough extends exports.Transform { - } - import * as exports from "socket:stream"; - import { EventEmitter } from "socket:events"; + export function isBigIntObject(input: any): boolean; + /** + * Returns `true` if input is a `Boolean` object. + * @param {any} input + * @return {boolean} + */ + export function isBooleanObject(input: any): boolean; + /** + * Returns `true` if input is a `Number` object. + * @param {any} input + * @return {boolean} + */ + export function isNumberObject(input: any): boolean; + /** + * Returns `true` if input is a `String` object. + * @param {any} input + * @return {boolean} + */ + export function isStringObject(input: any): boolean; + /** + * Returns `true` if input is a `Symbol` object. + * @param {any} input + * @return {boolean} + */ + export function isSymbolObject(input: any): boolean; + /** + * Returns `true` if input is native `Error` instance. + * @param {any} input + * @return {boolean} + */ + export function isNativeError(input: any): boolean; + /** + * Returns `true` if input is a `RegExp` instance. + * @param {any} input + * @return {boolean} + */ + export function isRegExp(input: any): boolean; + /** + * Returns `true` if input is a `GeneratorFunction`. + * @param {any} input + * @return {boolean} + */ + export function isGeneratorFunction(input: any): boolean; + /** + * Returns `true` if input is an `AsyncGeneratorFunction`. + * @param {any} input + * @return {boolean} + */ + export function isAsyncGeneratorFunction(input: any): boolean; + /** + * Returns `true` if input is an instance of a `Generator`. + * @param {any} input + * @return {boolean} + */ + export function isGeneratorObject(input: any): boolean; + /** + * Returns `true` if input is a `Promise` instance. + * @param {any} input + * @return {boolean} + */ + export function isPromise(input: any): boolean; + /** + * Returns `true` if input is a `Map` instance. + * @param {any} input + * @return {boolean} + */ + export function isMap(input: any): boolean; + /** + * Returns `true` if input is a `Set` instance. + * @param {any} input + * @return {boolean} + */ + export function isSet(input: any): boolean; + /** + * Returns `true` if input is an instance of an `Iterator`. + * @param {any} input + * @return {boolean} + */ + export function isIterator(input: any): boolean; + /** + * Returns `true` if input is an instance of an `AsyncIterator`. + * @param {any} input + * @return {boolean} + */ + export function isAsyncIterator(input: any): boolean; + /** + * Returns `true` if input is an instance of a `MapIterator`. + * @param {any} input + * @return {boolean} + */ + export function isMapIterator(input: any): boolean; + /** + * Returns `true` if input is an instance of a `SetIterator`. + * @param {any} input + * @return {boolean} + */ + export function isSetIterator(input: any): boolean; + /** + * Returns `true` if input is a `WeakMap` instance. + * @param {any} input + * @return {boolean} + */ + export function isWeakMap(input: any): boolean; + /** + * Returns `true` if input is a `WeakSet` instance. + * @param {any} input + * @return {boolean} + */ + export function isWeakSet(input: any): boolean; + /** + * Returns `true` if input is an `ArrayBuffer` instance. + * @param {any} input + * @return {boolean} + */ + export function isArrayBuffer(input: any): boolean; + /** + * Returns `true` if input is an `DataView` instance. + * @param {any} input + * @return {boolean} + */ + export function isDataView(input: any): boolean; + /** + * Returns `true` if input is a `SharedArrayBuffer`. + * This will always return `false` if a `SharedArrayBuffer` + * type is not available. + * @param {any} input + * @return {boolean} + */ + export function isSharedArrayBuffer(input: any): boolean; + /** + * Not supported. This function will return `false` always. + * @param {any} input + * @return {boolean} + */ + export function isProxy(input: any): boolean; + /** + * Returns `true` if input looks like a module namespace object. + * @param {any} input + * @return {boolean} + */ + export function isModuleNamespaceObject(input: any): boolean; + /** + * Returns `true` if input is an `ArrayBuffer` of `SharedArrayBuffer`. + * @param {any} input + * @return {boolean} + */ + export function isAnyArrayBuffer(input: any): boolean; + /** + * Returns `true` if input is a "boxed" primitive. + * @param {any} input + * @return {boolean} + */ + export function isBoxedPrimitive(input: any): boolean; + /** + * Returns `true` if input is an `ArrayBuffer` view. + * @param {any} input + * @return {boolean} + */ + export function isArrayBufferView(input: any): boolean; + /** + * Returns `true` if input is a `TypedArray` instance. + * @param {any} input + * @return {boolean} + */ + export function isTypedArray(input: any): boolean; + /** + * Returns `true` if input is an `Uint8Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isUint8Array(input: any): boolean; + /** + * Returns `true` if input is an `Uint8ClampedArray` instance. + * @param {any} input + * @return {boolean} + */ + export function isUint8ClampedArray(input: any): boolean; + /** + * Returns `true` if input is an `Uint16Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isUint16Array(input: any): boolean; + /** + * Returns `true` if input is an `Uint32Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isUint32Array(input: any): boolean; + /** + * Returns `true` if input is an Int8Array`` instance. + * @param {any} input + * @return {boolean} + */ + export function isInt8Array(input: any): boolean; + /** + * Returns `true` if input is an `Int16Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isInt16Array(input: any): boolean; + /** + * Returns `true` if input is an `Int32Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isInt32Array(input: any): boolean; + /** + * Returns `true` if input is an `Float32Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isFloat32Array(input: any): boolean; + /** + * Returns `true` if input is an `Float64Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isFloat64Array(input: any): boolean; + /** + * Returns `true` if input is an `BigInt64Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isBigInt64Array(input: any): boolean; + /** + * Returns `true` if input is an `BigUint64Array` instance. + * @param {any} input + * @return {boolean} + */ + export function isBigUint64Array(input: any): boolean; + /** + * @ignore + * @param {any} input + * @return {boolean} + */ + export function isKeyObject(input: any): boolean; + /** + * Returns `true` if input is a `CryptoKey` instance. + * @param {any} input + * @return {boolean} + */ + export function isCryptoKey(input: any): boolean; + /** + * Returns `true` if input is an `Array`. + * @param {any} input + * @return {boolean} + */ + export const isArray: any; + export default exports; + import * as exports from "socket:util/types"; } -declare module "socket:fs/stream" { - export const DEFAULT_STREAM_HIGH_WATER_MARK: number; + +declare module "socket:mime/index" { /** - * @typedef {import('./handle.js').FileHandle} FileHandle + * Look up a MIME type in various MIME databases. + * @param {string} query + * @return {Promise} */ + export function lookup(query: string): Promise; /** - * A `Readable` stream for a `FileHandle`. + * Look up a MIME type in various MIME databases synchronously. + * @param {string} query + * @return {DatabaseQueryResult[]} */ - export class ReadStream extends Readable { - end: any; - start: any; - handle: any; - buffer: ArrayBuffer; - signal: any; - timeout: any; - bytesRead: number; - shouldEmitClose: boolean; + export function lookupSync(query: string): DatabaseQueryResult[]; + /** + * A container for a database lookup query. + */ + export class DatabaseQueryResult { /** - * Sets file handle for the ReadStream. - * @param {FileHandle} handle + * `DatabaseQueryResult` class constructor. + * @ignore + * @param {Database|null} database + * @param {string} name + * @param {string} mime */ - setHandle(handle: FileHandle): void; + constructor(database: Database | null, name: string, mime: string); /** - * The max buffer size for the ReadStream. + * @type {string} */ - get highWaterMark(): number; + name: string; /** - * Relative or absolute path of the underlying `FileHandle`. + * @type {string} */ - get path(): any; + mime: string; /** - * `true` if the stream is in a pending state. + * @type {Database?} */ - get pending(): boolean; - _open(callback: any): Promise; - _read(callback: any): Promise; - } - export namespace ReadStream { - export { DEFAULT_STREAM_HIGH_WATER_MARK as highWaterMark }; + database: Database | null; } /** - * A `Writable` stream for a `FileHandle`. + * A container for MIME types by class (audio, video, text, etc) + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml} */ - export class WriteStream extends Writable { - start: any; - handle: any; - signal: any; - timeout: any; - bytesWritten: number; - shouldEmitClose: boolean; + export class Database { /** - * Sets file handle for the WriteStream. - * @param {FileHandle} handle + * `Database` class constructor. + * @param {string} name */ - setHandle(handle: FileHandle): void; + constructor(name: string); /** - * The max buffer size for the Writetream. + * The name of the MIME database. + * @type {string} */ - get highWaterMark(): number; - /** - * Relative or absolute path of the underlying `FileHandle`. - */ - get path(): any; - /** - * `true` if the stream is in a pending state. - */ - get pending(): boolean; - _open(callback: any): Promise; - _write(buffer: any, callback: any): any; - } - export namespace WriteStream { - export { DEFAULT_STREAM_HIGH_WATER_MARK as highWaterMark }; - } - export const FileReadStream: typeof exports.ReadStream; - export const FileWriteStream: typeof exports.WriteStream; - export default exports; - export type FileHandle = import("socket:fs/handle").FileHandle; - import { Readable } from "socket:stream"; - import { Writable } from "socket:stream"; - import * as exports from "socket:fs/stream"; - -} -declare module "socket:fs/constants" { - /** - * This flag can be used with uv_fs_copyfile() to return an error if the - * destination already exists. - */ - export const COPYFILE_EXCL: 1; - /** - * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. - * If copy-on-write is not supported, a fallback copy mechanism is used. - */ - export const COPYFILE_FICLONE: 2; - /** - * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. - * If copy-on-write is not supported, an error is returned. - */ - export const COPYFILE_FICLONE_FORCE: 4; - export const UV_DIRENT_UNKNOWN: any; - export const UV_DIRENT_FILE: any; - export const UV_DIRENT_DIR: any; - export const UV_DIRENT_LINK: any; - export const UV_DIRENT_FIFO: any; - export const UV_DIRENT_SOCKET: any; - export const UV_DIRENT_CHAR: any; - export const UV_DIRENT_BLOCK: any; - export const UV_FS_SYMLINK_DIR: any; - export const UV_FS_SYMLINK_JUNCTION: any; - export const O_RDONLY: any; - export const O_WRONLY: any; - export const O_RDWR: any; - export const O_APPEND: any; - export const O_ASYNC: any; - export const O_CLOEXEC: any; - export const O_CREAT: any; - export const O_DIRECT: any; - export const O_DIRECTORY: any; - export const O_DSYNC: any; - export const O_EXCL: any; - export const O_LARGEFILE: any; - export const O_NOATIME: any; - export const O_NOCTTY: any; - export const O_NOFOLLOW: any; - export const O_NONBLOCK: any; - export const O_NDELAY: any; - export const O_PATH: any; - export const O_SYNC: any; - export const O_TMPFILE: any; - export const O_TRUNC: any; - export const S_IFMT: any; - export const S_IFREG: any; - export const S_IFDIR: any; - export const S_IFCHR: any; - export const S_IFBLK: any; - export const S_IFIFO: any; - export const S_IFLNK: any; - export const S_IFSOCK: any; - export const S_IRWXU: any; - export const S_IRUSR: any; - export const S_IWUSR: any; - export const S_IXUSR: any; - export const S_IRWXG: any; - export const S_IRGRP: any; - export const S_IWGRP: any; - export const S_IXGRP: any; - export const S_IRWXO: any; - export const S_IROTH: any; - export const S_IWOTH: any; - export const S_IXOTH: any; - export const F_OK: any; - export const R_OK: any; - export const W_OK: any; - export const X_OK: any; - export default exports; - import * as exports from "socket:fs/constants"; - -} -declare module "socket:fs/flags" { - export function normalizeFlags(flags: any): any; - export default exports; - import * as exports from "socket:fs/flags"; - -} -declare module "socket:fs/stats" { - /** - * A container for various stats about a file or directory. - */ - export class Stats { + name: string; /** - * Creates a `Stats` instance from input, optionally with `BigInt` data types - * @param {object|Stats} [stat] - * @param {fromBigInt=} [fromBigInt = false] - * @return {Stats} + * The URL of the MIME database. + * @type {URL} */ - static from(stat?: object | Stats, fromBigInt?: any): Stats; + url: URL; /** - * `Stats` class constructor. - * @param {object|Stats} stat + * The mapping of MIME name to the MIME "content type" + * @type {Map} */ - constructor(stat: object | Stats); - dev: any; - ino: any; - mode: any; - nlink: any; - uid: any; - gid: any; - rdev: any; - size: any; - blksize: any; - blocks: any; - atimeMs: any; - mtimeMs: any; - ctimeMs: any; - birthtimeMs: any; - atime: Date; - mtime: Date; - ctime: Date; - birthtime: Date; + map: Map; /** - * Returns `true` if stats represents a directory. - * @return {Boolean} + * An index of MIME "content type" to the MIME name. + * @type {Map} */ - isDirectory(): boolean; + index: Map; /** - * Returns `true` if stats represents a file. - * @return {Boolean} + * An enumeration of all database entries. + * @return {Array>} */ - isFile(): boolean; + entries(): Array>; /** - * Returns `true` if stats represents a block device. - * @return {Boolean} + * Loads database MIME entries into internal map. + * @return {Promise} */ - isBlockDevice(): boolean; + load(): Promise; /** - * Returns `true` if stats represents a character device. - * @return {Boolean} + * Loads database MIME entries synchronously into internal map. */ - isCharacterDevice(): boolean; + loadSync(): void; /** - * Returns `true` if stats represents a symbolic link. - * @return {Boolean} + * Lookup MIME type by name or content type + * @param {string} query + * @return {Promise} */ - isSymbolicLink(): boolean; + lookup(query: string): Promise; /** - * Returns `true` if stats represents a FIFO. - * @return {Boolean} + * Lookup MIME type by name or content type synchronously. + * @param {string} query + * @return {Promise} */ - isFIFO(): boolean; + lookupSync(query: string): Promise; /** - * Returns `true` if stats represents a socket. - * @return {Boolean} + * Queries database map and returns an array of results + * @param {string} query + * @return {DatabaseQueryResult[]} */ - isSocket(): boolean; + query(query: string): DatabaseQueryResult[]; } - export default exports; - import * as exports from "socket:fs/stats"; - -} -declare module "socket:fs/fds" { - const _default: { - types: Map; - fds: Map; - ids: Map; - readonly size: number; - get(id: any): any; - syncOpenDescriptors(): Promise; - set(id: any, fd: any, type: any): void; - has(id: any): boolean; - fd(id: any): any; - id(fd: any): any; - release(id: any, closeDescriptor?: boolean): Promise; - retain(id: any): Promise; - delete(id: any): void; - clear(): void; - typeof(id: any): any; - entries(): IterableIterator<[any, any]>; - }; - export default _default; -} -declare module "socket:fs/handle" { - export const kOpening: unique symbol; - export const kClosing: unique symbol; - export const kClosed: unique symbol; /** - * A container for a descriptor tracked in `fds` and opened in the native layer. - * This class implements the Node.js `FileHandle` interface - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-filehandle} + * A database of MIME types for 'application/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#application} */ - export class FileHandle extends EventEmitter { - static get DEFAULT_ACCESS_MODE(): any; - static get DEFAULT_OPEN_FLAGS(): string; - static get DEFAULT_OPEN_MODE(): number; - /** - * Creates a `FileHandle` from a given `id` or `fd` - * @param {string|number|FileHandle|object} id - * @return {FileHandle} - */ - static from(id: string | number | FileHandle | object): FileHandle; - /** - * Determines if access to `path` for `mode` is possible. - * @param {string} path - * @param {number} [mode = 0o666] - * @param {object=} [options] - * @return {Promise} - */ - static access(path: string, mode?: number, options?: object | undefined): Promise; - /** - * Asynchronously open a file. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesopenpath-flags-mode} - * @param {string | Buffer | URL} path - * @param {string=} [flags = 'r'] - * @param {string|number=} [mode = 0o666] - * @param {object=} [options] - * @return {Promise} - */ - static open(path: string | Buffer | URL, flags?: string | undefined, mode?: (string | number) | undefined, options?: object | undefined): Promise; - /** - * `FileHandle` class constructor - * @ignore - * @param {object} options - */ - constructor(options: object); - flags: any; - path: any; - mode: any; - id: string; - fd: any; - /** - * `true` if the `FileHandle` instance has been opened. - * @type {boolean} - */ - get opened(): boolean; - /** - * `true` if the `FileHandle` is opening. - * @type {boolean} - */ - get opening(): boolean; + export const application: Database; + /** + * A database of MIME types for 'audio/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#audio} + */ + export const audio: Database; + /** + * A database of MIME types for 'font/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#font} + */ + export const font: Database; + /** + * A database of MIME types for 'image/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#image} + */ + export const image: Database; + /** + * A database of MIME types for 'model/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#model} + */ + export const model: Database; + /** + * A database of MIME types for 'multipart/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#multipart} + */ + export const multipart: Database; + /** + * A database of MIME types for 'text/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#text} + */ + export const text: Database; + /** + * A database of MIME types for 'video/' content types + * @type {Database} + * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#video} + */ + export const video: Database; + /** + * An array of known MIME databases. Custom databases can be added to this + * array in userspace for lookup with `mime.lookup()` + * @type {Database[]} + */ + export const databases: Database[]; + export class MIMEParams extends Map { + constructor(); + constructor(entries?: readonly (readonly [any, any])[]); + constructor(); + constructor(iterable?: Iterable); + } + export class MIMEType { + constructor(input: any); + set type(value: any); + get type(): any; + set subtype(value: any); + get subtype(): any; + get essence(): string; + get params(): any; + toString(): string; + toJSON(): string; + #private; + } + namespace _default { + export { Database }; + export { databases }; + export { lookup }; + export { lookupSync }; + export { MIMEParams }; + export { MIMEType }; + export { application }; + export { audio }; + export { font }; + export { image }; + export { model }; + export { multipart }; + export { text }; + export { video }; + } + export default _default; +} + +declare module "socket:mime" { + export * from "socket:mime/index"; + export default exports; + import * as exports from "socket:mime/index"; +} + +declare module "socket:util" { + export function debug(section: any): { + (...args: any[]): void; + enabled: boolean; + }; + export function hasOwnProperty(object: any, property: any): any; + export function isDate(object: any): boolean; + export function isTypedArray(object: any): boolean; + export function isArrayLike(input: any): boolean; + export function isError(object: any): boolean; + export function isSymbol(value: any): value is symbol; + export function isNumber(value: any): boolean; + export function isBoolean(value: any): boolean; + export function isArrayBufferView(buf: any): boolean; + export function isAsyncFunction(object: any): boolean; + export function isArgumentsObject(object: any): boolean; + export function isEmptyObject(object: any): boolean; + export function isObject(object: any): boolean; + export function isUndefined(value: any): boolean; + export function isNull(value: any): boolean; + export function isNullOrUndefined(value: any): boolean; + export function isPrimitive(value: any): boolean; + export function isRegExp(value: any): boolean; + export function isPlainObject(object: any): boolean; + export function isArrayBuffer(object: any): boolean; + export function isBufferLike(object: any): boolean; + export function isFunction(value: any): boolean; + export function isErrorLike(error: any): boolean; + export function isClass(value: any): boolean; + export function isBuffer(value: any): boolean; + export function isPromiseLike(object: any): boolean; + export function toString(object: any): any; + export function toBuffer(object: any, encoding?: any): any; + export function toProperCase(string: any): any; + export function splitBuffer(buffer: any, highWaterMark: any): any[]; + export function clamp(value: any, min: any, max: any): number; + export function promisify(original: any): any; + export function inspect(value: any, options: any): any; + export namespace inspect { + let ignore: symbol; + let custom: symbol; + } + export function format(format: any, ...args: any[]): string; + export function parseJSON(string: any): any; + export function parseHeaders(headers: any): string[][]; + export function noop(): void; + export function isValidPercentageValue(input: any): boolean; + export function compareBuffers(a: any, b: any): any; + export function inherits(Constructor: any, Super: any): void; + /** + * @ignore + * @param {string} source + * @return {boolean} + */ + export function isESMSource(source: string): boolean; + export function deprecate(...args: any[]): void; + export { types }; + export const TextDecoder: { + new (label?: string, options?: TextDecoderOptions): TextDecoder; + prototype: TextDecoder; + }; + export const TextEncoder: { + new (): TextEncoder; + prototype: TextEncoder; + }; + export const isArray: any; + export const inspectSymbols: symbol[]; + export class IllegalConstructor { + } + export const ESM_TEST_REGEX: RegExp; + export const MIMEType: typeof mime.MIMEType; + export const MIMEParams: typeof mime.MIMEParams; + export default exports; + import types from "socket:util/types"; + import mime from "socket:mime"; + import * as exports from "socket:util"; + +} + +declare module "socket:async/wrap" { + /** + * Returns `true` if a given function `fn` has the "async" wrapped tag, + * meaning it was "tagged" in a `wrap(fn)` call before, otherwise this + * function will return `false`. + * @ignore + * @param {function} fn + * @param {boolean} + */ + export function isTagged(fn: Function): boolean; + /** + * Tags a function `fn` as being "async wrapped" so subsequent calls to + * `wrap(fn)` do not wrap an already wrapped function. + * @ignore + * @param {function} fn + * @return {function} + */ + export function tag(fn: Function): Function; + /** + * Wraps a function `fn` that captures a snapshot of the current async + * context. This function is idempotent and will not wrap a function more + * than once. + * @ignore + * @param {function} fn + * @return {function} + */ + export function wrap(fn: Function): Function; + export const symbol: unique symbol; + export default wrap; +} + +declare module "socket:internal/async/hooks" { + export function dispatch(hook: any, asyncId: any, type: any, triggerAsyncId: any, resource: any): void; + export function getNextAsyncResourceId(): number; + export function executionAsyncResource(): any; + export function executionAsyncId(): any; + export function triggerAsyncId(): any; + export function getDefaultExecutionAsyncId(): any; + export function wrap(callback: any, type: any, asyncId?: number, triggerAsyncId?: any, resource?: any): (...args: any[]) => any; + export function getTopLevelAsyncResourceName(): any; + /** + * The default top level async resource ID + * @type {number} + */ + export const TOP_LEVEL_ASYNC_RESOURCE_ID: number; + export namespace state { + let defaultExecutionAsyncId: number; + } + export namespace hooks { + let init: any[]; + let before: any[]; + let after: any[]; + let destroy: any[]; + let promiseResolve: any[]; + } + /** + * A base class for the `AsyncResource` class or other higher level async + * resource classes. + */ + export class CoreAsyncResource { /** - * `true` if the `FileHandle` is closing. - * @type {boolean} + * `CoreAsyncResource` class constructor. + * @param {string} type + * @param {object|number=} [options] */ - get closing(): boolean; + constructor(type: string, options?: (object | number) | undefined); /** - * `true` if the `FileHandle` is closed. + * The `CoreAsyncResource` type. + * @type {string} */ - get closed(): boolean; + get type(): string; /** - * Appends to a file, if handle was opened with `O_APPEND`, otherwise this - * method is just an alias to `FileHandle#writeFile()`. - * @param {string|Buffer|TypedArray|Array} data - * @param {object=} [options] - * @param {string=} [options.encoding = 'utf8'] - * @param {object=} [options.signal] + * `true` if the `CoreAsyncResource` was destroyed, otherwise `false`. This + * value is only set to `true` if `emitDestroy()` was called, likely from + * destroying the resource manually. + * @type {boolean} */ - appendFile(data: string | Buffer | TypedArray | any[], options?: object | undefined): Promise; + get destroyed(): boolean; /** - * Change permissions of file handle. - * @param {number} mode - * @param {object=} [options] + * The unique async resource ID. + * @return {number} */ - chmod(mode: number, options?: object | undefined): Promise; + asyncId(): number; /** - * Change ownership of file handle. - * @param {number} uid - * @param {number} gid - * @param {object=} [options] + * The trigger async resource ID. + * @return {number} */ - chown(uid: number, gid: number, options?: object | undefined): Promise; + triggerAsyncId(): number; /** - * Close underlying file handle - * @param {object=} [options] + * Manually emits destroy hook for the resource. + * @return {CoreAsyncResource} */ - close(options?: object | undefined): Promise; + emitDestroy(): CoreAsyncResource; /** - * Creates a `ReadStream` for the underlying file. - * @param {object=} [options] - An options object + * Binds function `fn` with an optional this `thisArg` binding to run + * in the execution context of this `CoreAsyncResource`. + * @param {function} fn + * @param {object=} [thisArg] + * @return {function} */ - createReadStream(options?: object | undefined): ReadStream; + bind(fn: Function, thisArg?: object | undefined): Function; /** - * Creates a `WriteStream` for the underlying file. - * @param {object=} [options] - An options object + * Runs function `fn` in the execution context of this `CoreAsyncResource`. + * @param {function} fn + * @param {object=} [thisArg] + * @param {...any} [args] + * @return {any} */ - createWriteStream(options?: object | undefined): WriteStream; + runInAsyncScope(fn: Function, thisArg?: object | undefined, ...args?: any[]): any; + #private; + } + export class TopLevelAsyncResource extends CoreAsyncResource { + } + export const asyncContextVariable: Variable; + export const topLevelAsyncResource: TopLevelAsyncResource; + export default hooks; + import { Variable } from "socket:async/context"; +} + +declare module "socket:async/resource" { + /** + * @typedef {{ + * triggerAsyncId?: number, + * requireManualDestroy?: boolean + * }} AsyncResourceOptions + */ + /** + * A container that should be extended that represents a resource with + * an asynchronous execution context. + */ + export class AsyncResource extends CoreAsyncResource { /** - * @param {object=} [options] + * Binds function `fn` with an optional this `thisArg` binding to run + * in the execution context of an anonymous `AsyncResource`. + * @param {function} fn + * @param {object|string=} [type] + * @param {object=} [thisArg] + * @return {function} */ - datasync(): Promise; + static bind(fn: Function, type?: (object | string) | undefined, thisArg?: object | undefined): Function; /** - * Opens the underlying descriptor for the file handle. - * @param {object=} [options] + * `AsyncResource` class constructor. + * @param {string} type + * @param {AsyncResourceOptions|number=} [options] */ - open(options?: object | undefined): Promise; + constructor(type: string, options?: (AsyncResourceOptions | number) | undefined); + } + export default AsyncResource; + export type AsyncResourceOptions = { + triggerAsyncId?: number; + requireManualDestroy?: boolean; + }; + import { executionAsyncResource } from "socket:internal/async/hooks"; + import { executionAsyncId } from "socket:internal/async/hooks"; + import { triggerAsyncId } from "socket:internal/async/hooks"; + import { CoreAsyncResource } from "socket:internal/async/hooks"; + export { executionAsyncResource, executionAsyncId, triggerAsyncId }; +} + +declare module "socket:async/hooks" { + /** + * Factory for creating a `AsyncHook` instance. + * @param {AsyncHookCallbackOptions|AsyncHookCallbacks=} [callbacks] + * @return {AsyncHook} + */ + export function createHook(callbacks?: (AsyncHookCallbackOptions | AsyncHookCallbacks) | undefined): AsyncHook; + /** + * A container for `AsyncHooks` callbacks. + * @ignore + */ + export class AsyncHookCallbacks { /** - * Reads `length` bytes starting from `position` into `buffer` at - * `offset`. - * @param {Buffer|object} buffer - * @param {number=} [offset] - * @param {number=} [length] - * @param {number=} [position] - * @param {object=} [options] + * `AsyncHookCallbacks` class constructor. + * @ignore + * @param {AsyncHookCallbackOptions} [options] + */ + constructor(options?: AsyncHookCallbackOptions); + init(asyncId: any, type: any, triggerAsyncId: any, resource: any): void; + before(asyncId: any): void; + after(asyncId: any): void; + destroy(asyncId: any): void; + promiseResolve(asyncId: any): void; + } + /** + * A container for registering various callbacks for async resource hooks. + */ + export class AsyncHook { + /** + * @param {AsyncHookCallbackOptions|AsyncHookCallbacks=} [options] */ - read(buffer: Buffer | object, offset?: number | undefined, length?: number | undefined, position?: number | undefined, options?: object | undefined): Promise<{ - bytesRead: number; - buffer: any; - }>; + constructor(callbacks?: any); /** - * Reads the entire contents of a file and returns it as a buffer or a string - * specified of a given encoding specified at `options.encoding`. - * @param {object=} [options] - * @param {string=} [options.encoding = 'utf8'] - * @param {object=} [options.signal] + * @type {boolean} */ - readFile(options?: object | undefined): Promise; + get enabled(): boolean; /** - * Returns the stats of the underlying file. - * @param {object=} [options] - * @return {Promise} + * Enable the async hook. + * @return {AsyncHook} */ - stat(options?: object | undefined): Promise; + enable(): AsyncHook; /** - * Synchronize a file's in-core state with storage device - * @return {Promise} + * Disables the async hook + * @return {AsyncHook} */ - sync(): Promise; + disable(): AsyncHook; + #private; + } + export default createHook; + import { executionAsyncResource } from "socket:internal/async/hooks"; + import { executionAsyncId } from "socket:internal/async/hooks"; + import { triggerAsyncId } from "socket:internal/async/hooks"; + export { executionAsyncResource, executionAsyncId, triggerAsyncId }; +} + +declare module "socket:async/storage" { + /** + * A container for storing values that remain present during + * asynchronous operations. + */ + export class AsyncLocalStorage { /** - * @param {number} [offset = 0] - * @return {Promise} + * Binds function `fn` to run in the execution context of an + * anonymous `AsyncResource`. + * @param {function} fn + * @return {function} */ - truncate(offset?: number): Promise; + static bind(fn: Function): Function; /** - * Writes `length` bytes at `offset` in `buffer` to the underlying file - * at `position`. - * @param {Buffer|object} buffer - * @param {number} offset - * @param {number} length - * @param {number} position - * @param {object=} [options] + * Captures the current async context and returns a function that runs + * a function in that execution context. + * @return {function} */ - write(buffer: Buffer | object, offset: number, length: number, position: number, options?: object | undefined): Promise<{ - buffer: any; - bytesWritten: number; - }>; + static snapshot(): Function; /** - * Writes `data` to file. - * @param {string|Buffer|TypedArray|Array} data - * @param {object=} [options] - * @param {string=} [options.encoding = 'utf8'] - * @param {object=} [options.signal] + * @type {boolean} */ - writeFile(data: string | Buffer | TypedArray | any[], options?: object | undefined): Promise; - [exports.kOpening]: any; - [exports.kClosing]: any; - [exports.kClosed]: boolean; - } - /** - * A container for a directory handle tracked in `fds` and opened in the - * native layer. - */ - export class DirectoryHandle extends EventEmitter { + get enabled(): boolean; /** - * The max number of entries that can be bufferd with the `bufferSize` - * option. + * Disables the `AsyncLocalStorage` instance. When disabled, + * `getStore()` will always return `undefined`. */ - static get MAX_BUFFER_SIZE(): number; - static get MAX_ENTRIES(): number; + disable(): void; /** - * The default number of entries `Dirent` that are buffered - * for each read request. + * Enables the `AsyncLocalStorage` instance. */ - static get DEFAULT_BUFFER_SIZE(): number; + enable(): void; /** - * Creates a `FileHandle` from a given `id` or `fd` - * @param {string|number|DirectoryHandle|object} id - * @return {DirectoryHandle} + * Enables and sets the `AsyncLocalStorage` instance default store value. + * @param {any} store */ - static from(id: string | number | DirectoryHandle | object): DirectoryHandle; + enterWith(store: any): void; /** - * Asynchronously open a directory. - * @param {string | Buffer | URL} path - * @param {object=} [options] - * @return {Promise} + * Runs function `fn` in the current asynchronous execution context with + * a given `store` value and arguments given to `fn`. + * @param {any} store + * @param {function} fn + * @param {...any} args + * @return {any} */ - static open(path: string | Buffer | URL, options?: object | undefined): Promise; + run(store: any, fn: Function, ...args: any[]): any; + exit(fn: any, ...args: any[]): any; /** - * `DirectoryHandle` class constructor - * @private - * @param {object} options + * If the `AsyncLocalStorage` instance is enabled, it returns the current + * store value for this asynchronous execution context. + * @return {any|undefined} */ - private constructor(); - id: string; - path: any; - bufferSize: number; + getStore(): any | undefined; + #private; + } + export default AsyncLocalStorage; +} + +declare module "socket:async/deferred" { + /** + * Dispatched when a `Deferred` internal promise is resolved. + */ + export class DeferredResolveEvent extends Event { /** - * DirectoryHandle file descriptor id + * `DeferredResolveEvent` class constructor + * @ignore + * @param {string=} [type] + * @param {any=} [result] */ - get fd(): string; + constructor(type?: string | undefined, result?: any | undefined); /** - * `true` if the `DirectoryHandle` instance has been opened. - * @type {boolean} + * The `Deferred` promise result value. + * @type {any?} */ - get opened(): boolean; + result: any | null; + } + /** + * Dispatched when a `Deferred` internal promise is rejected. + */ + export class DeferredRejectEvent { /** - * `true` if the `DirectoryHandle` is opening. - * @type {boolean} + * `DeferredRejectEvent` class constructor + * @ignore + * @param {string=} [type] + * @param {Error=} [error] */ - get opening(): boolean; + constructor(type?: string | undefined, error?: Error | undefined); + } + /** + * A utility class for creating deferred promises. + */ + export class Deferred extends EventTarget { /** - * `true` if the `DirectoryHandle` is closing. - * @type {boolean} + * `Deferred` class constructor. + * @param {Deferred|Promise?} [promise] */ - get closing(): boolean; + constructor(promise?: Deferred | (Promise | null)); /** - * `true` if `DirectoryHandle` is closed. + * Function to resolve the associated promise. + * @type {function} */ - get closed(): boolean; + resolve: Function; /** - * Opens the underlying handle for a directory. - * @param {object=} options - * @return {Promise} + * Function to reject the associated promise. + * @type {function} */ - open(options?: object | undefined): Promise; + reject: Function; /** - * Close underlying directory handle - * @param {object=} [options] + * Attaches a fulfillment callback and a rejection callback to the promise, + * and returns a new promise resolving to the return value of the called + * callback. + * @param {function(any)=} [resolve] + * @param {function(Error)=} [reject] */ - close(options?: object | undefined): Promise; + then(resolve?: ((arg0: any) => any) | undefined, reject?: ((arg0: Error) => any) | undefined): Promise; /** - * Reads directory entries - * @param {object=} [options] - * @param {number=} [options.entries = DirectoryHandle.MAX_ENTRIES] + * Attaches a rejection callback to the promise, and returns a new promise + * resolving to the return value of the callback if it is called, or to its + * original fulfillment value if the promise is instead fulfilled. + * @param {function(Error)=} [callback] */ - read(options?: object | undefined): Promise; - [exports.kOpening]: any; - [exports.kClosing]: any; - [exports.kClosed]: boolean; + catch(callback?: ((arg0: Error) => any) | undefined): Promise; + /** + * Attaches a callback for when the promise is settled (fulfilled or rejected). + * @param {function(any?)} [callback] + */ + finally(callback?: (arg0: any | null) => any): Promise; + /** + * The promise associated with this Deferred instance. + * @type {Promise} + */ + get promise(): Promise; + /** + * A string representation of this Deferred instance. + * @type {string} + * @ignore + */ + get [Symbol.toStringTag](): string; + #private; } + export default Deferred; +} + +declare module "socket:async" { export default exports; - export type TypedArray = Uint8Array | Int8Array; - import { EventEmitter } from "socket:events"; - import { Buffer } from "socket:buffer"; - import { ReadStream } from "socket:fs/stream"; - import { WriteStream } from "socket:fs/stream"; - import { Stats } from "socket:fs/stats"; - import * as exports from "socket:fs/handle"; + import AsyncLocalStorage from "socket:async/storage"; + import AsyncResource from "socket:async/resource"; + import AsyncContext from "socket:async/context"; + import Deferred from "socket:async/deferred"; + import { executionAsyncResource } from "socket:async/hooks"; + import { executionAsyncId } from "socket:async/hooks"; + import { triggerAsyncId } from "socket:async/hooks"; + import { createHook } from "socket:async/hooks"; + import { AsyncHook } from "socket:async/hooks"; + import * as exports from "socket:async"; + export { AsyncLocalStorage, AsyncResource, AsyncContext, Deferred, executionAsyncResource, executionAsyncId, triggerAsyncId, createHook, AsyncHook }; } -declare module "socket:fs/dir" { + +declare module "socket:application/menu" { /** - * Sorts directory entries - * @param {string|Dirent} a - * @param {string|Dirent} b - * @return {number} + * Internal IPC for setting an application menu + * @ignore */ - export function sortDirectoryEntries(a: string | Dirent, b: string | Dirent): number; - export const kType: unique symbol; + export function setMenu(options: any, type: any): Promise; /** - * A containerr for a directory and its entries. This class supports scanning - * a directory entry by entry with a `read()` method. The `Symbol.asyncIterator` - * interface is exposed along with an AsyncGenerator `entries()` method. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdir} + * Internal IPC for setting an application context menu + * @ignore */ - export class Dir { - static from(fdOrHandle: any, options: any): exports.Dir; + export function setContextMenu(options: any): Promise; + /** + * A `Menu` is base class for a `ContextMenu`, `SystemMenu`, or `TrayMenu`. + */ + export class Menu extends EventTarget { /** - * `Dir` class constructor. - * @param {DirectoryHandle} handle - * @param {object=} options + * `Menu` class constructor. + * @ignore + * @param {string} type */ - constructor(handle: DirectoryHandle, options?: object | undefined); - path: any; - handle: DirectoryHandle; - encoding: any; - withFileTypes: boolean; + constructor(type: string); /** - * `true` if closed, otherwise `false`. + * The broadcast channel for this menu. * @ignore - * @type {boolean} + * @type {BroadcastChannel} */ - get closed(): boolean; + get channel(): BroadcastChannel; /** - * `true` if closing, otherwise `false`. + * The `Menu` instance type. + * @type {('context'|'system'|'tray')?} + */ + get type(): "tray" | "system" | "context"; + /** + * Setter for the level 1 'error'` event listener. * @ignore - * @type {boolean} + * @type {function(ErrorEvent)?} */ - get closing(): boolean; + set onerror(onerror: (arg0: ErrorEvent) => any); /** - * Closes container and underlying handle. - * @param {object|function} options - * @param {function=} callback + * Level 1 'error'` event listener. + * @type {function(ErrorEvent)?} */ - close(options?: object | Function, callback?: Function | undefined): Promise; + get onerror(): (arg0: ErrorEvent) => any; /** - * Reads and returns directory entry. - * @param {object|function} options - * @param {function=} callback - * @return {Dirent|string} + * Setter for the level 1 'menuitem'` event listener. + * @ignore + * @type {function(MenuItemEvent)?} */ - read(options: object | Function, callback?: Function | undefined): Dirent | string; + set onmenuitem(onmenuitem: (arg0: menuitemEvent) => any); /** - * AsyncGenerator which yields directory entries. - * @param {object=} options + * Level 1 'menuitem'` event listener. + * @type {function(menuitemEvent)?} */ - entries(options?: object | undefined): AsyncGenerator; + get onmenuitem(): (arg0: menuitemEvent) => any; /** - * `for await (...)` AsyncGenerator support. + * Set the menu layout for this `Menu` instance. + * @param {string|object} layoutOrOptions + * @param {object=} [options] */ - get [Symbol.asyncIterator](): (options?: object | undefined) => AsyncGenerator; + set(layoutOrOptions: string | object, options?: object | undefined): Promise; + #private; } /** - * A container for a directory entry. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdirent} + * A container for various `Menu` instances. */ - export class Dirent { - static get UNKNOWN(): any; - static get FILE(): any; - static get DIR(): any; - static get LINK(): any; - static get FIFO(): any; - static get SOCKET(): any; - static get CHAR(): any; - static get BLOCK(): any; - /** - * Creates `Dirent` instance from input. - * @param {object|string} name - * @param {(string|number)=} type - */ - static from(name: object | string, type?: (string | number) | undefined): exports.Dirent; - /** - * `Dirent` class constructor. - * @param {string} name - * @param {string|number} type - */ - constructor(name: string, type: string | number); - name: string; + export class MenuContainer extends EventTarget { /** - * Read only type. + * `MenuContainer` class constructor. + * @param {EventTarget} [sourceEventTarget] + * @param {object=} [options] */ - get type(): number; + constructor(sourceEventTarget?: EventTarget, options?: object | undefined); /** - * `true` if `Dirent` instance is a directory. + * Setter for the level 1 'error'` event listener. + * @ignore + * @type {function(ErrorEvent)?} */ - isDirectory(): boolean; + set onerror(onerror: (arg0: ErrorEvent) => any); /** - * `true` if `Dirent` instance is a file. + * Level 1 'error'` event listener. + * @type {function(ErrorEvent)?} */ - isFile(): boolean; + get onerror(): (arg0: ErrorEvent) => any; /** - * `true` if `Dirent` instance is a block device. + * Setter for the level 1 'menuitem'` event listener. + * @ignore + * @type {function(MenuItemEvent)?} */ - isBlockDevice(): boolean; + set onmenuitem(onmenuitem: (arg0: menuitemEvent) => any); /** - * `true` if `Dirent` instance is a character device. + * Level 1 'menuitem'` event listener. + * @type {function(menuitemEvent)?} */ - isCharacterDevice(): boolean; + get onmenuitem(): (arg0: menuitemEvent) => any; /** - * `true` if `Dirent` instance is a symbolic link. + * The `TrayMenu` instance for the application. + * @type {TrayMenu} */ - isSymbolicLink(): boolean; + get tray(): TrayMenu; /** - * `true` if `Dirent` instance is a FIFO. + * The `SystemMenu` instance for the application. + * @type {SystemMenu} */ - isFIFO(): boolean; + get system(): SystemMenu; /** - * `true` if `Dirent` instance is a socket. + * The `ContextMenu` instance for the application. + * @type {ContextMenu} */ - isSocket(): boolean; - [exports.kType]: number; + get context(): ContextMenu; + #private; } - export default exports; - import { DirectoryHandle } from "socket:fs/handle"; - import * as exports from "socket:fs/dir"; - -} -declare module "socket:hooks" { /** - * Wait for a hook event to occur. - * @template {Event | T extends Event} - * @param {string|function} nameOrFunction - * @return {Promise} + * A `Menu` instance that represents a context menu. */ - export function wait(nameOrFunction: string | Function): Promise; + export class ContextMenu extends Menu { + constructor(); + } /** - * Wait for the global Window, Document, and Runtime to be ready. - * The callback function is called exactly once. - * @param {function} callback - * @return {function} - */ - export function onReady(callback: Function): Function; - /** - * Wait for the global Window and Document to be ready. The callback - * function is called exactly once. - * @param {function} callback - * @return {function} - */ - export function onLoad(callback: Function): Function; - /** - * Wait for the runtime to be ready. The callback - * function is called exactly once. - * @param {function} callback - * @return {function} - */ - export function onInit(callback: Function): Function; - /** - * Calls callback when a global exception occurs. - * 'error', 'messageerror', and 'unhandledrejection' events are handled here. - * @param {function} callback - * @return {function} - */ - export function onError(callback: Function): Function; - /** - * Subscribes to the global data pipe calling callback when - * new data is emitted on the global Window. - * @param {function} callback - * @return {function} - */ - export function onData(callback: Function): Function; - /** - * Subscribes to global messages likely from an external `postMessage` - * invocation. - * @param {function} callback - * @return {function} - */ - export function onMessage(callback: Function): Function; - /** - * Calls callback when runtime is working online. - * @param {function} callback - * @return {function} - */ - export function onOnline(callback: Function): Function; - /** - * Calls callback when runtime is not working online. - * @param {function} callback - * @return {function} - */ - export function onOffline(callback: Function): Function; - /** - * Calls callback when runtime user preferred language has changed. - * @param {function} callback - * @return {function} - */ - export function onLanguageChange(callback: Function): Function; - /** - * Calls callback when an application permission has changed. - * @param {function} callback - * @return {function} - */ - export function onPermissionChange(callback: Function): Function; - /** - * Calls callback in response to a presented `Notification`. - * @param {function} callback - * @return {function} - */ - export function onNotificationResponse(callback: Function): Function; - /** - * Calls callback when a `Notification` is presented. - * @param {function} callback - * @return {function} - */ - export function onNotificationPresented(callback: Function): Function; - /** - * Calls callback when a `ApplicationURL` is opened. - * @param {function} callback - * @return {function} - */ - export function onApplicationURL(callback: Function): Function; - export const RUNTIME_INIT_EVENT_NAME: "__runtime_init__"; - export const GLOBAL_EVENTS: string[]; - /** - * An event dispatched when the runtime has been initialized. + * A `Menu` instance that represents the system menu. */ - export class InitEvent { + export class SystemMenu extends Menu { constructor(); } /** - * An event dispatched when the runtime global has been loaded. + * A `Menu` instance that represents the tray menu. */ - export class LoadEvent { + export class TrayMenu extends Menu { constructor(); } /** - * An event dispatched when the runtime is considered ready. + * The application tray menu. + * @type {TrayMenu} */ - export class ReadyEvent { - constructor(); - } + export const tray: TrayMenu; /** - * An event dispatched when the runtime has been initialized. + * The application system menu. + * @type {SystemMenu} */ - export class RuntimeInitEvent { - constructor(); - } + export const system: SystemMenu; /** - * An interface for registering callbacks for various hooks in - * the runtime. + * The application context menu. + * @type {ContextMenu} */ - export class Hooks extends EventTarget { + export const context: ContextMenu; + /** + * The application menus container. + * @type {MenuContainer} + */ + export const container: MenuContainer; + export default container; + import ipc from "socket:ipc"; +} + +declare module "socket:internal/events" { + /** + * An event dispatched when an application URL is opening the application. + */ + export class ApplicationURLEvent extends Event { /** - * @ignore + * `ApplicationURLEvent` class constructor. + * @param {string=} [type] + * @param {object=} [options] */ - static GLOBAL_EVENTS: string[]; + constructor(type?: string | undefined, options?: object | undefined); /** - * @ignore + * `true` if the application URL is valid (parses correctly). + * @type {boolean} */ - static InitEvent: typeof InitEvent; + get isValid(): boolean; /** - * @ignore + * Data associated with the `ApplicationURLEvent`. + * @type {?any} */ - static LoadEvent: typeof LoadEvent; + get data(): any; /** - * @ignore + * The original source URI + * @type {?string} */ - static ReadyEvent: typeof ReadyEvent; + get source(): string; /** - * @ignore + * The `URL` for the `ApplicationURLEvent`. + * @type {?URL} */ - static RuntimeInitEvent: typeof RuntimeInitEvent; + get url(): URL; /** - * An array of all global events listened to in various hooks + * String tag name for an `ApplicationURLEvent` instance. + * @type {string} */ - get globalEvents(): string[]; + get [Symbol.toStringTag](): string; + #private; + } + /** + * An event dispacted for a registered global hotkey expression. + */ + export class HotKeyEvent extends MessageEvent { /** - * Reference to global object - * @type {object} + * `HotKeyEvent` class constructor. + * @ignore + * @param {string=} [type] + * @param {object=} [data] */ - get global(): any; + constructor(type?: string | undefined, data?: object | undefined); /** - * Returns `document` in global. - * @type {Document} + * The global unique ID for this hotkey binding. + * @type {number?} */ - get document(): Document; + get id(): number; /** - * Returns `document` in global. - * @type {Window} + * The computed hash for this hotkey binding. + * @type {number?} */ - get window(): Window; + get hash(): number; /** - * Predicate for determining if the global document is ready. - * @type {boolean} + * The normalized hotkey expression as a sequence of tokens. + * @type {string[]} */ - get isDocumentReady(): boolean; + get sequence(): string[]; /** - * Predicate for determining if the global object is ready. - * @type {boolean} + * The original expression of the hotkey binding. + * @type {string?} */ - get isGlobalReady(): boolean; + get expression(): string; + } + /** + * An event dispacted when a menu item is selected. + */ + export class MenuItemEvent extends MessageEvent { /** - * Predicate for determining if the runtime is ready. - * @type {boolean} + * `MenuItemEvent` class constructor + * @ignore + * @param {string=} [type] + * @param {object=} [data] + * @param {import('../application/menu.js').Menu} menu */ - get isRuntimeReady(): boolean; + constructor(type?: string | undefined, data?: object | undefined, menu?: import("socket:application/menu").Menu); /** - * Predicate for determining if everything is ready. - * @type {boolean} + * The `Menu` this event has been dispatched for. + * @type {import('../application/menu.js').Menu?} */ - get isReady(): boolean; + get menu(): import("socket:application/menu").Menu; /** - * Predicate for determining if the runtime is working online. - * @type {boolean} + * The title of the menu item. + * @type {string?} */ - get isOnline(): boolean; + get title(): string; /** - * Predicate for determining if the runtime is in a Worker context. - * @type {boolean} + * An optional tag value for the menu item that may also be the + * parent menu item title. + * @type {string?} */ - get isWorkerContext(): boolean; + get tag(): string; /** - * Predicate for determining if the runtime is in a Window context. - * @type {boolean} + * The parent title of the menu item. + * @type {string?} */ - get isWindowContext(): boolean; + get parent(): string; + #private; + } + /** + * An event dispacted when the application receives an OS signal + */ + export class SignalEvent extends MessageEvent { /** - * Wait for a hook event to occur. - * @template {Event | T extends Event} - * @param {string|function} nameOrFunction - * @param {WaitOptions=} [options] - * @return {Promise} + * `SignalEvent` class constructor + * @ignore + * @param {string=} [type] + * @param {object=} [options] */ - wait(nameOrFunction: string | Function, options?: WaitOptions | undefined): Promise; + constructor(type?: string | undefined, options?: object | undefined); /** - * Wait for the global Window, Document, and Runtime to be ready. - * The callback function is called exactly once. - * @param {function} callback - * @return {function} + * The code of the signal. + * @type {import('../process/signal.js').signal} */ - onReady(callback: Function): Function; + get code(): any; /** - * Wait for the global Window and Document to be ready. The callback - * function is called exactly once. - * @param {function} callback - * @return {function} + * The name of the signal. + * @type {string} */ - onLoad(callback: Function): Function; - /** - * Wait for the runtime to be ready. The callback - * function is called exactly once. - * @param {function} callback - * @return {function} - */ - onInit(callback: Function): Function; - /** - * Calls callback when a global exception occurs. - * 'error', 'messageerror', and 'unhandledrejection' events are handled here. - * @param {function} callback - * @return {function} - */ - onError(callback: Function): Function; - /** - * Subscribes to the global data pipe calling callback when - * new data is emitted on the global Window. - * @param {function} callback - * @return {function} - */ - onData(callback: Function): Function; - /** - * Subscribes to global messages likely from an external `postMessage` - * invocation. - * @param {function} callback - * @return {function} - */ - onMessage(callback: Function): Function; - /** - * Calls callback when runtime is working online. - * @param {function} callback - * @return {function} - */ - onOnline(callback: Function): Function; - /** - * Calls callback when runtime is not working online. - * @param {function} callback - * @return {function} - */ - onOffline(callback: Function): Function; - /** - * Calls callback when runtime user preferred language has changed. - * @param {function} callback - * @return {function} - */ - onLanguageChange(callback: Function): Function; - /** - * Calls callback when an application permission has changed. - * @param {function} callback - * @return {function} - */ - onPermissionChange(callback: Function): Function; - /** - * Calls callback in response to a displayed `Notification`. - * @param {function} callback - * @return {function} - */ - onNotificationResponse(callback: Function): Function; - /** - * Calls callback when a `Notification` is presented. - * @param {function} callback - * @return {function} - */ - onNotificationPresented(callback: Function): Function; + get name(): string; /** - * Calls callback when a `ApplicationURL` is opened. - * @param {function} callback - * @return {function} + * An optional message describing the signal + * @type {string} */ - onApplicationURL(callback: Function): Function; + get message(): string; #private; } - export default hooks; - export type WaitOptions = { - signal?: AbortSignal; - }; + namespace _default { + export { ApplicationURLEvent }; + export { MenuItemEvent }; + export { SignalEvent }; + export { HotKeyEvent }; + } + export default _default; +} + +declare module "socket:path/well-known" { /** - * `Hooks` single instance. - * @ignore + * Well known path to the user's "Downloads" folder. + * @type {?string} */ - const hooks: Hooks; -} -declare module "socket:fs/watcher" { + export const DOWNLOADS: string | null; /** - * A container for a file system path watcher. + * Well known path to the user's "Documents" folder. + * @type {?string} */ - export class Watcher extends EventEmitter { - /** - * `Watcher` class constructor. - * @ignore - * @param {string} path - * @param {object=} [options] - * @param {AbortSignal=} [options.signal} - * @param {string|number|bigint=} [options.id] - * @param {string=} [options.encoding = 'utf8'] - */ - constructor(path: string, options?: object | undefined); - /** - * The underlying `fs.Watcher` resource id. - * @ignore - * @type {string} - */ - id: string; - /** - * The path the `fs.Watcher` is watching - * @type {string} - */ - path: string; - /** - * `true` if closed, otherwise `false. - * @type {boolean} - */ - closed: boolean; - /** - * `true` if aborted, otherwise `false`. - * @type {boolean} - */ - aborted: boolean; - /** - * The encoding of the `filename` - * @type {'utf8'|'buffer'} - */ - encoding: 'utf8' | 'buffer'; - /** - * A `AbortController` `AbortSignal` for async aborts. - * @type {AbortSignal?} - */ - signal: AbortSignal | null; - /** - * Internal event listener cancellation. - * @ignore - * @type {function?} - */ - stopListening: Function | null; - /** - * Internal starter for watcher. - * @ignore - */ - start(): Promise; - /** - * Closes watcher and stops listening for changes. - * @return {Promise} - */ - close(): Promise; - /** - * Implements the `AsyncIterator` (`Symbol.asyncIterator`) iterface. - * @ignore - * @return {AsyncIterator<{ eventType: string, filename: string }>} - */ - [Symbol.asyncIterator](): AsyncIterator<{ - eventType: string; - filename: string; - }>; - } - export default Watcher; - import { EventEmitter } from "socket:events"; -} -declare module "socket:fs/promises" { + export const DOCUMENTS: string | null; /** - * Asynchronously check access a file. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesaccesspath-mode} - * @param {string | Buffer | URL} path - * @param {string?} [mode] - * @param {object?} [options] + * Well known path to the user's "Pictures" folder. + * @type {?string} */ - export function access(path: string | Buffer | URL, mode?: string | null, options?: object | null): Promise; + export const PICTURES: string | null; /** - * @see {@link https://nodejs.org/api/fs.html#fspromiseschmodpath-mode} - * @param {string | Buffer | URL} path - * @param {number} mode - * @returns {Promise} + * Well known path to the user's "Desktop" folder. + * @type {?string} */ - export function chmod(path: string | Buffer | URL, mode: number): Promise; + export const DESKTOP: string | null; /** - * Changes ownership of file or directory at `path` with `uid` and `gid`. - * @param {string} path - * @param {number} uid - * @param {number} gid - * @return {Promise} + * Well known path to the user's "Videos" folder. + * @type {?string} */ - export function chown(path: string, uid: number, gid: number): Promise; + export const VIDEOS: string | null; /** - * Asynchronously copies `src` to `dest` calling `callback` upon success or error. - * @param {string} src - The source file path. - * @param {string} dest - The destination file path. - * @param {number} flags - Modifiers for copy operation. - * @return {Promise} + * Well known path to the user's "Music" folder. + * @type {?string} */ - export function copyFile(src: string, dest: string, flags: number): Promise; + export const MUSIC: string | null; /** - * Chages ownership of link at `path` with `uid` and `gid. - * @param {string} path - * @param {number} uid - * @param {number} gid - * @return {Promise} + * Well known path to the application's "resources" folder. + * @type {?string} */ - export function lchown(path: string, uid: number, gid: number): Promise; + export const RESOURCES: string | null; /** - * Creates a link to `dest` from `dest`. - * @param {string} src - * @param {string} dest - * @return {Promise} + * Well known path to the application's "config" folder. + * @type {?string} */ - export function link(src: string, dest: string): Promise; + export const CONFIG: string | null; /** - * Asynchronously creates a directory. - * - * @param {string} path - The path to create - * @param {object} [options] - The optional options argument can be an integer specifying mode (permission and sticky bits), or an object with a mode property and a recursive property indicating whether parent directories should be created. Calling fs.mkdir() when path is a directory that exists results in an error only when recursive is false. - * @param {boolean} [options.recursive=false] - Recursively create missing path segments. - * @param {number} [options.mode=0o777] - Set the mode of directory, or missing path segments when recursive is true. - * @return {Promise} - Upon success, fulfills with undefined if recursive is false, or the first directory path created if recursive is true. + * Well known path to the application's public "media" folder. + * @type {?string} */ - export function mkdir(path: string, options?: { - recursive?: boolean; - mode?: number; - }): Promise; + export const MEDIA: string | null; /** - * Asynchronously open a file. - * @see {@link https://nodejs.org/api/fs.html#fspromisesopenpath-flags-mode } - * - * @param {string | Buffer | URL} path - * @param {string=} flags - default: 'r' - * @param {number=} mode - default: 0o666 - * @return {Promise} + * Well known path to the application's "data" folder. + * @type {?string} */ - export function open(path: string | Buffer | URL, flags?: string | undefined, mode?: number | undefined): Promise; + export const DATA: string | null; /** - * @see {@link https://nodejs.org/api/fs.html#fspromisesopendirpath-options} - * @param {string | Buffer | URL} path - * @param {object?} [options] - * @param {string?} [options.encoding = 'utf8'] - * @param {number?} [options.bufferSize = 32] - * @return {Promise} + * Well known path to the application's "log" folder. + * @type {?string} */ - export function opendir(path: string | Buffer | URL, options?: object | null): Promise; + export const LOG: string | null; /** - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreaddirpath-options} - * @param {string | Buffer | URL} path - * @param {object?} options - * @param {string?} [options.encoding = 'utf8'] - * @param {boolean?} [options.withFileTypes = false] + * Well known path to the application's "tmp" folder. + * @type {?string} */ - export function readdir(path: string | Buffer | URL, options: object | null): Promise; + export const TMP: string | null; /** - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreadfilepath-options} - * @param {string} path - * @param {object?} [options] - * @param {(string|null)?} [options.encoding = null] - * @param {string?} [options.flag = 'r'] - * @param {AbortSignal?} [options.signal] - * @return {Promise} + * Well known path to the application's "home" folder. + * This may be the user's HOME directory or the application container sandbox. + * @type {?string} */ - export function readFile(path: string, options?: object | null): Promise; + export const HOME: string | null; + namespace _default { + export { DOWNLOADS }; + export { DOCUMENTS }; + export { RESOURCES }; + export { PICTURES }; + export { DESKTOP }; + export { VIDEOS }; + export { CONFIG }; + export { MEDIA }; + export { MUSIC }; + export { HOME }; + export { DATA }; + export { LOG }; + export { TMP }; + } + export default _default; +} + +declare module "socket:os" { /** - * Reads link at `path` - * @param {string} path - * @return {Promise} + * Returns the operating system CPU architecture for which Socket was compiled. + * @returns {string} - 'arm64', 'ia32', 'x64', or 'unknown' */ - export function readlink(path: string): Promise; + export function arch(): string; /** - * Computes real path for `path` - * @param {string} path - * @return {Promise} + * Returns an array of objects containing information about each CPU/core. + * @returns {Array} cpus - An array of objects containing information about each CPU/core. + * The properties of the objects are: + * - model `` - CPU model name. + * - speed `` - CPU clock speed (in MHz). + * - times `` - An object containing the fields user, nice, sys, idle, irq representing the number of milliseconds the CPU has spent in each mode. + * - user `` - Time spent by this CPU or core in user mode. + * - nice `` - Time spent by this CPU or core in user mode with low priority (nice). + * - sys `` - Time spent by this CPU or core in system mode. + * - idle `` - Time spent by this CPU or core in idle mode. + * - irq `` - Time spent by this CPU or core in IRQ mode. + * @see {@link https://nodejs.org/api/os.html#os_os_cpus} */ - export function realpath(path: string): Promise; + export function cpus(): Array; /** - * Renames file or directory at `src` to `dest`. - * @param {string} src - * @param {string} dest - * @return {Promise} + * Returns an object containing network interfaces that have been assigned a network address. + * @returns {object} - An object containing network interfaces that have been assigned a network address. + * Each key on the returned object identifies a network interface. The associated value is an array of objects that each describe an assigned network address. + * The properties available on the assigned network address object include: + * - address `` - The assigned IPv4 or IPv6 address. + * - netmask `` - The IPv4 or IPv6 network mask. + * - family `` - The address family ('IPv4' or 'IPv6'). + * - mac `` - The MAC address of the network interface. + * - internal `` - Indicates whether the network interface is a loopback interface. + * - scopeid `` - The numeric scope ID (only specified when family is 'IPv6'). + * - cidr `` - The CIDR notation of the interface. + * @see {@link https://nodejs.org/api/os.html#os_os_networkinterfaces} */ - export function rename(src: string, dest: string): Promise; + export function networkInterfaces(): object; /** - * Removes directory at `path`. - * @param {string} path - * @return {Promise} + * Returns the operating system platform. + * @returns {string} - 'android', 'cygwin', 'freebsd', 'linux', 'darwin', 'ios', 'openbsd', 'win32', or 'unknown' + * @see {@link https://nodejs.org/api/os.html#os_os_platform} + * The returned value is equivalent to `process.platform`. */ - export function rmdir(path: string): Promise; + export function platform(): string; /** - * @see {@link https://nodejs.org/api/fs.html#fspromisesstatpath-options} - * @param {string | Buffer | URL} path - * @param {object?} [options] - * @param {boolean?} [options.bigint = false] - * @return {Promise} + * Returns the operating system name. + * @returns {string} - 'CYGWIN_NT', 'Mac', 'Darwin', 'FreeBSD', 'Linux', 'OpenBSD', 'Windows_NT', 'Win32', or 'Unknown' + * @see {@link https://nodejs.org/api/os.html#os_os_type} */ - export function stat(path: string | Buffer | URL, options?: object | null): Promise; + export function type(): string; /** - * Creates a symlink of `src` at `dest`. - * @param {string} src - * @param {string} dest - * @return {Promise} + * @returns {boolean} - `true` if the operating system is Windows. */ - export function symlink(src: string, dest: string, type?: any): Promise; + export function isWindows(): boolean; /** - * Unlinks (removes) file at `path`. - * @param {string} path - * @return {Promise} + * @returns {string} - The operating system's default directory for temporary files. */ - export function unlink(path: string): Promise; + export function tmpdir(): string; /** - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromiseswritefilefile-data-options} - * @param {string | Buffer | URL | FileHandle} path - filename or FileHandle - * @param {string|Buffer|Array|DataView|TypedArray} data - * @param {object?} [options] - * @param {string|null} [options.encoding = 'utf8'] - * @param {number} [options.mode = 0o666] - * @param {string} [options.flag = 'w'] - * @param {AbortSignal?} [options.signal] - * @return {Promise} + * Get resource usage. */ - export function writeFile(path: string | Buffer | URL | FileHandle, data: string | Buffer | any[] | DataView | TypedArray, options?: object | null): Promise; + export function rusage(): any; /** - * Watch for changes at `path` calling `callback` - * @param {string} - * @param {function|object=} [options] - * @param {string=} [options.encoding = 'utf8'] - * @param {AbortSignal=} [options.signal] - * @return {Watcher} + * Returns the system uptime in seconds. + * @returns {number} - The system uptime in seconds. */ - export function watch(path: any, options?: (Function | object) | undefined): Watcher; - export type Stats = any; - export default exports; - export type Buffer = import("socket:buffer").Buffer; - export type TypedArray = Uint8Array | Int8Array; - import { FileHandle } from "socket:fs/handle"; - import { Dir } from "socket:fs/dir"; - import { Stats } from "socket:fs/stats"; - import { Watcher } from "socket:fs/watcher"; - import * as constants from "socket:fs/constants"; - import { DirectoryHandle } from "socket:fs/handle"; - import { Dirent } from "socket:fs/dir"; - import fds from "socket:fs/fds"; - import { ReadStream } from "socket:fs/stream"; - import { WriteStream } from "socket:fs/stream"; - import * as exports from "socket:fs/promises"; - - export { constants, Dir, DirectoryHandle, Dirent, fds, FileHandle, ReadStream, Watcher, WriteStream }; -} -declare module "socket:fs/index" { + export function uptime(): number; /** - * Asynchronously check access a file for a given mode calling `callback` - * upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} - * @param {string | Buffer | URL} path - * @param {string?|function(Error?)?} [mode = F_OK(0)] - * @param {function(Error?)?} [callback] + * Returns the operating system name. + * @returns {string} - The operating system name. */ - export function access(path: string | Buffer | URL, mode: any, callback?: ((arg0: Error | null) => any) | null): void; + export function uname(): string; /** + * It's implemented in process.hrtime.bigint() * @ignore */ - export function appendFile(path: any, data: any, options: any, callback: any): void; - /** - * - * Asynchronously changes the permissions of a file. - * No arguments other than a possible exception are given to the completion callback - * - * @see {@link https://nodejs.org/api/fs.html#fschmodpath-mode-callback} - * - * @param {string | Buffer | URL} path - * @param {number} mode - * @param {function(Error?)} callback - */ - export function chmod(path: string | Buffer | URL, mode: number, callback: (arg0: Error | null) => any): void; - /** - * Changes ownership of file or directory at `path` with `uid` and `gid`. - * @param {string} path - * @param {number} uid - * @param {number} gid - * @param {function} callback - */ - export function chown(path: string, uid: number, gid: number, callback: Function): void; - /** - * Asynchronously close a file descriptor calling `callback` upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsclosefd-callback} - * @param {number} fd - * @param {function(Error?)?} [callback] - */ - export function close(fd: number, callback?: ((arg0: Error | null) => any) | null): void; - /** - * Asynchronously copies `src` to `dest` calling `callback` upon success or error. - * @param {string} src - The source file path. - * @param {string} dest - The destination file path. - * @param {number} flags - Modifiers for copy operation. - * @param {function(Error=)=} [callback] - The function to call after completion. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscopyfilesrc-dest-mode-callback} - */ - export function copyFile(src: string, dest: string, flags: number, callback?: ((arg0: Error | undefined) => any) | undefined): void; - /** - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} - * @param {string | Buffer | URL} path - * @param {object?} [options] - * @returns {ReadStream} - */ - export function createReadStream(path: string | Buffer | URL, options?: object | null): ReadStream; + export function hrtime(): any; /** - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} - * @param {string | Buffer | URL} path - * @param {object?} [options] - * @returns {WriteStream} + * Node.js doesn't have this method. + * @ignore */ - export function createWriteStream(path: string | Buffer | URL, options?: object | null): WriteStream; + export function availableMemory(): any; /** - * Invokes the callback with the for the file descriptor. See - * the POSIX fstat(2) documentation for more detail. - * - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsfstatfd-options-callback} - * - * @param {number} fd - A file descriptor. - * @param {object?|function?} [options] - An options object. - * @param {function?} callback - The function to call after completion. + * The host operating system. This value can be one of: + * - android + * - android-emulator + * - iphoneos + * - iphone-simulator + * - linux + * - macosx + * - unix + * - unknown + * - win32 + * @ignore + * @return {'android'|'android-emulator'|'iphoneos'|iphone-simulator'|'linux'|'macosx'|unix'|unknown'|win32'} */ - export function fstat(fd: number, options: any, callback: Function | null): void; + export function host(): "android" | "android-emulator" | "iphoneos" | iphone; /** - * Request that all data for the open file descriptor is flushed - * to the storage device. - * @param {number} fd - A file descriptor. - * @param {function} callback - The function to call after completion. + * Returns the home directory of the current user. + * @return {string} */ - export function fsync(fd: number, callback: Function): void; + export function homedir(): string; + export { constants }; /** - * Truncates the file up to `offset` bytes. - * @param {number} fd - A file descriptor. - * @param {number=|function} [offset = 0] - * @param {function?} callback - The function to call after completion. + * @type {string} + * The operating system's end-of-line marker. `'\r\n'` on Windows and `'\n'` on POSIX. */ - export function ftruncate(fd: number, offset: any, callback: Function | null): void; + export const EOL: string; + export default exports; + import constants from "socket:os/constants"; + import * as exports from "socket:os"; + +} + +declare module "socket:process/signal" { /** - * Chages ownership of link at `path` with `uid` and `gid. - * @param {string} path - * @param {number} uid - * @param {number} gid - * @param {function} callback + * Converts an `signal` code to its corresponding string message. + * @param {import('./os/constants.js').signal} {code} + * @return {string} */ - export function lchown(path: string, uid: number, gid: number, callback: Function): void; + export function toString(code: any): string; /** - * Creates a link to `dest` from `src`. - * @param {string} src - * @param {string} dest - * @param {function} + * Gets the code for a given 'signal' name. + * @param {string|number} name + * @return {signal} */ - export function link(src: string, dest: string, callback: any): void; + export function getCode(name: string | number): signal; /** - * @ignore + * Gets the name for a given 'signal' code + * @return {string} + * @param {string|number} code */ - export function mkdir(path: any, options: any, callback: any): void; + export function getName(code: string | number): string; /** - * Asynchronously open a file calling `callback` upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} - * @param {string | Buffer | URL} path - * @param {string?} [flags = 'r'] - * @param {string?} [mode = 0o666] - * @param {object?|function?} [options] - * @param {function(Error?, number?)?} [callback] + * Gets the message for a 'signal' code. + * @param {number|string} code + * @return {string} */ - export function open(path: string | Buffer | URL, flags?: string | null, mode?: string | null, options?: any, callback?: ((arg0: Error | null, arg1: number | null) => any) | null): void; - /** - * Asynchronously open a directory calling `callback` upon success or error. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} - * @param {string | Buffer | URL} path - * @param {object?|function(Error?, Dir?)} [options] - * @param {string?} [options.encoding = 'utf8'] - * @param {boolean?} [options.withFileTypes = false] - * @param {function(Error?, Dir?)?} callback - */ - export function opendir(path: string | Buffer | URL, options: {}, callback: ((arg0: Error | null, arg1: Dir | null) => any) | null): void; + export function getMessage(code: number | string): string; + /** + * Add a signal event listener. + * @param {string|number} signal + * @param {function(SignalEvent)} callback + * @param {{ once?: boolean }=} [options] + */ + export function addEventListener(signalName: any, callback: (arg0: SignalEvent) => any, options?: { + once?: boolean; + } | undefined): void; + /** + * Remove a signal event listener. + * @param {string|number} signal + * @param {function(SignalEvent)} callback + * @param {{ once?: boolean }=} [options] + */ + export function removeEventListener(signalName: any, callback: (arg0: SignalEvent) => any, options?: { + once?: boolean; + } | undefined): void; + export { constants }; + export const channel: BroadcastChannel; + export const SIGHUP: any; + export const SIGINT: any; + export const SIGQUIT: any; + export const SIGILL: any; + export const SIGTRAP: any; + export const SIGABRT: any; + export const SIGIOT: any; + export const SIGBUS: any; + export const SIGFPE: any; + export const SIGKILL: any; + export const SIGUSR1: any; + export const SIGSEGV: any; + export const SIGUSR2: any; + export const SIGPIPE: any; + export const SIGALRM: any; + export const SIGTERM: any; + export const SIGCHLD: any; + export const SIGCONT: any; + export const SIGSTOP: any; + export const SIGTSTP: any; + export const SIGTTIN: any; + export const SIGTTOU: any; + export const SIGURG: any; + export const SIGXCPU: any; + export const SIGXFSZ: any; + export const SIGVTALRM: any; + export const SIGPROF: any; + export const SIGWINCH: any; + export const SIGIO: any; + export const SIGINFO: any; + export const SIGSYS: any; + export const strings: { + [x: number]: string; + }; + namespace _default { + export { addEventListener }; + export { removeEventListener }; + export { constants }; + export { channel }; + export { strings }; + export { toString }; + export { getName }; + export { getCode }; + export { getMessage }; + export { SIGHUP }; + export { SIGINT }; + export { SIGQUIT }; + export { SIGILL }; + export { SIGTRAP }; + export { SIGABRT }; + export { SIGIOT }; + export { SIGBUS }; + export { SIGFPE }; + export { SIGKILL }; + export { SIGUSR1 }; + export { SIGSEGV }; + export { SIGUSR2 }; + export { SIGPIPE }; + export { SIGALRM }; + export { SIGTERM }; + export { SIGCHLD }; + export { SIGCONT }; + export { SIGSTOP }; + export { SIGTSTP }; + export { SIGTTIN }; + export { SIGTTOU }; + export { SIGURG }; + export { SIGXCPU }; + export { SIGXFSZ }; + export { SIGVTALRM }; + export { SIGPROF }; + export { SIGWINCH }; + export { SIGIO }; + export { SIGINFO }; + export { SIGSYS }; + } + export default _default; + export type signal = any; + import { SignalEvent } from "socket:internal/events"; + import { signal as constants } from "socket:os/constants"; +} + +declare module "socket:internal/streams/web" { + export class ByteLengthQueuingStrategy { + constructor(e: any); + _byteLengthQueuingStrategyHighWaterMark: any; + get highWaterMark(): any; + get size(): (e: any) => any; + } + export class CountQueuingStrategy { + constructor(e: any); + _countQueuingStrategyHighWaterMark: any; + get highWaterMark(): any; + get size(): () => number; + } + export class ReadableByteStreamController { + get byobRequest(): any; + get desiredSize(): number; + close(): void; + enqueue(e: any): void; + error(e?: any): void; + _pendingPullIntos: v; + [T](e: any): any; + [C](e: any): any; + [P](): void; + } + export class ReadableStream { + static from(e: any): any; + constructor(e?: {}, t?: {}); + get locked(): boolean; + cancel(e?: any): any; + getReader(e?: any): ReadableStreamBYOBReader | ReadableStreamDefaultReader; + pipeThrough(e: any, t?: {}): any; + pipeTo(e: any, t?: {}): any; + tee(): any; + values(e?: any): any; + } + export class ReadableStreamBYOBReader { + constructor(e: any); + _readIntoRequests: v; + get closed(): any; + cancel(e?: any): any; + read(e: any, t?: {}): any; + releaseLock(): void; + } + export class ReadableStreamBYOBRequest { + get view(): any; + respond(e: any): void; + respondWithNewView(e: any): void; + } + export class ReadableStreamDefaultController { + get desiredSize(): number; + close(): void; + enqueue(e?: any): void; + error(e?: any): void; + [T](e: any): any; + [C](e: any): void; + [P](): void; + } + export class ReadableStreamDefaultReader { + constructor(e: any); + _readRequests: v; + get closed(): any; + cancel(e?: any): any; + read(): any; + releaseLock(): void; + } + export class TransformStream { + constructor(e?: {}, t?: {}, r?: {}); + get readable(): any; + get writable(): any; + } + export class TransformStreamDefaultController { + get desiredSize(): number; + enqueue(e?: any): void; + error(e?: any): void; + terminate(): void; + } + export class WritableStream { + constructor(e?: {}, t?: {}); + get locked(): boolean; + abort(e?: any): any; + close(): any; + getWriter(): WritableStreamDefaultWriter; + } + export class WritableStreamDefaultController { + get abortReason(): any; + get signal(): any; + error(e?: any): void; + [w](e: any): any; + [R](): void; + } + export class WritableStreamDefaultWriter { + constructor(e: any); + _ownerWritableStream: any; + get closed(): any; + get desiredSize(): number; + get ready(): any; + abort(e?: any): any; + close(): any; + releaseLock(): void; + write(e?: any): any; + } + class v { + _cursor: number; + _size: number; + _front: { + _elements: any[]; + _next: any; + }; + _back: { + _elements: any[]; + _next: any; + }; + get length(): number; + push(e: any): void; + shift(): any; + forEach(e: any): void; + peek(): any; + } + const T: unique symbol; + const C: unique symbol; + const P: unique symbol; + const w: unique symbol; + const R: unique symbol; + export {}; +} + +declare module "socket:internal/streams" { + const _default: any; + export default _default; + import { ReadableStream } from "socket:internal/streams/web"; + import { ReadableStreamBYOBReader } from "socket:internal/streams/web"; + import { ReadableByteStreamController } from "socket:internal/streams/web"; + import { ReadableStreamBYOBRequest } from "socket:internal/streams/web"; + import { ReadableStreamDefaultController } from "socket:internal/streams/web"; + import { ReadableStreamDefaultReader } from "socket:internal/streams/web"; + import { WritableStream } from "socket:internal/streams/web"; + import { WritableStreamDefaultController } from "socket:internal/streams/web"; + import { WritableStreamDefaultWriter } from "socket:internal/streams/web"; + import { TransformStream } from "socket:internal/streams/web"; + import { TransformStreamDefaultController } from "socket:internal/streams/web"; + import { ByteLengthQueuingStrategy } from "socket:internal/streams/web"; + import { CountQueuingStrategy } from "socket:internal/streams/web"; + export { ReadableStream, ReadableStreamBYOBReader, ReadableByteStreamController, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, TransformStream, TransformStreamDefaultController, ByteLengthQueuingStrategy, CountQueuingStrategy }; +} + +declare module "socket:stream/web" { + export const TextEncoderStream: typeof UnsupportedStreamInterface; + export const TextDecoderStream: { + new (label?: string, options?: TextDecoderOptions): TextDecoderStream; + prototype: TextDecoderStream; + } | typeof UnsupportedStreamInterface; + export const CompressionStream: { + new (format: CompressionFormat): CompressionStream; + prototype: CompressionStream; + } | typeof UnsupportedStreamInterface; + export const DecompressionStream: { + new (format: CompressionFormat): DecompressionStream; + prototype: DecompressionStream; + } | typeof UnsupportedStreamInterface; + export default exports; + import { ReadableStream } from "socket:internal/streams"; + import { ReadableStreamBYOBReader } from "socket:internal/streams"; + import { ReadableByteStreamController } from "socket:internal/streams"; + import { ReadableStreamBYOBRequest } from "socket:internal/streams"; + import { ReadableStreamDefaultController } from "socket:internal/streams"; + import { ReadableStreamDefaultReader } from "socket:internal/streams"; + import { WritableStream } from "socket:internal/streams"; + import { WritableStreamDefaultController } from "socket:internal/streams"; + import { WritableStreamDefaultWriter } from "socket:internal/streams"; + import { TransformStream } from "socket:internal/streams"; + import { TransformStreamDefaultController } from "socket:internal/streams"; + import { ByteLengthQueuingStrategy } from "socket:internal/streams"; + import { CountQueuingStrategy } from "socket:internal/streams"; + class UnsupportedStreamInterface { + } + import * as exports from "socket:stream/web"; + + export { ReadableStream, ReadableStreamBYOBReader, ReadableByteStreamController, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, TransformStream, TransformStreamDefaultController, ByteLengthQueuingStrategy, CountQueuingStrategy }; +} + +declare module "socket:stream" { + export function pipelinePromise(...streams: any[]): Promise; + export function pipeline(stream: any, ...streams: any[]): any; + export function isStream(stream: any): boolean; + export function isStreamx(stream: any): boolean; + export function getStreamError(stream: any): any; + export function isReadStreamx(stream: any): any; + export { web }; + export class FixedFIFO { + constructor(hwm: any); + buffer: any[]; + mask: number; + top: number; + btm: number; + next: any; + clear(): void; + push(data: any): boolean; + shift(): any; + peek(): any; + isEmpty(): boolean; + } + export class FIFO { + constructor(hwm: any); + hwm: any; + head: FixedFIFO; + tail: FixedFIFO; + length: number; + clear(): void; + push(val: any): void; + shift(): any; + peek(): any; + isEmpty(): boolean; + } + export class WritableState { + constructor(stream: any, { highWaterMark, map, mapWritable, byteLength, byteLengthWritable }?: { + highWaterMark?: number; + map?: any; + mapWritable: any; + byteLength: any; + byteLengthWritable: any; + }); + stream: any; + queue: FIFO; + highWaterMark: number; + buffered: number; + error: any; + pipeline: any; + drains: any; + byteLength: any; + map: any; + afterWrite: any; + afterUpdateNextTick: any; + get ended(): boolean; + push(data: any): boolean; + shift(): any; + end(data: any): void; + autoBatch(data: any, cb: any): any; + update(): void; + updateNonPrimary(): void; + continueUpdate(): boolean; + updateCallback(): void; + updateNextTick(): void; + } + export class ReadableState { + constructor(stream: any, { highWaterMark, map, mapReadable, byteLength, byteLengthReadable }?: { + highWaterMark?: number; + map?: any; + mapReadable: any; + byteLength: any; + byteLengthReadable: any; + }); + stream: any; + queue: FIFO; + highWaterMark: number; + buffered: number; + readAhead: boolean; + error: any; + pipeline: Pipeline; + byteLength: any; + map: any; + pipeTo: any; + afterRead: any; + afterUpdateNextTick: any; + get ended(): boolean; + pipe(pipeTo: any, cb: any): void; + push(data: any): boolean; + shift(): any; + unshift(data: any): void; + read(): any; + drain(): void; + update(): void; + updateNonPrimary(): void; + continueUpdate(): boolean; + updateCallback(): void; + updateNextTick(): void; + } + export class TransformState { + constructor(stream: any); + data: any; + afterTransform: any; + afterFinal: any; + } + export class Pipeline { + constructor(src: any, dst: any, cb: any); + from: any; + to: any; + afterPipe: any; + error: any; + pipeToFinished: boolean; + finished(): void; + done(stream: any, err: any): void; + } + export class Stream extends EventEmitter { + constructor(opts: any); + _duplexState: number; + _readableState: any; + _writableState: any; + _open(cb: any): void; + _destroy(cb: any): void; + _predestroy(): void; + get readable(): boolean; + get writable(): boolean; + get destroyed(): boolean; + get destroying(): boolean; + destroy(err: any): void; + } + export class Readable extends Stream { + static _fromAsyncIterator(ite: any, opts: any): Readable; + static from(data: any, opts: any): any; + static isBackpressured(rs: any): boolean; + static isPaused(rs: any): boolean; + _readableState: ReadableState; + _read(cb: any): void; + pipe(dest: any, cb: any): any; + read(): any; + push(data: any): boolean; + unshift(data: any): void; + resume(): this; + pause(): this; + } + export class Writable extends Stream { + static isBackpressured(ws: any): boolean; + static drained(ws: any): Promise; + _writableState: WritableState; + _writev(batch: any, cb: any): void; + _write(data: any, cb: any): void; + _final(cb: any): void; + write(data: any): boolean; + end(data: any): this; + } + export class Duplex extends Readable { + _writableState: WritableState; + _writev(batch: any, cb: any): void; + _write(data: any, cb: any): void; + _final(cb: any): void; + write(data: any): boolean; + end(data: any): this; + } + export class Transform extends Duplex { + _transformState: TransformState; + _transform(data: any, cb: any): void; + _flush(cb: any): void; + } + export class PassThrough extends Transform { + } + const _default: typeof Stream & { + web: typeof web; + Readable: typeof Readable; + Writable: typeof Writable; + Duplex: typeof Duplex; + Transform: typeof Transform; + PassThrough: typeof PassThrough; + pipeline: typeof pipeline & { + [x: symbol]: typeof pipelinePromise; + }; + }; + export default _default; + import web from "socket:stream/web"; + import { EventEmitter } from "socket:events"; +} + +declare module "socket:tty" { + export function WriteStream(fd: any): Writable; + export function ReadStream(fd: any): Readable; + export function isatty(fd: any): boolean; + namespace _default { + export { WriteStream }; + export { ReadStream }; + export { isatty }; + } + export default _default; + import { Writable } from "socket:stream"; + import { Readable } from "socket:stream"; +} + +declare module "socket:process" { /** - * Asynchronously read from an open file descriptor. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback} - * @param {number} fd - * @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to. - * @param {number} offset - The position in buffer to write the data to. - * @param {number} length - The number of bytes to read. - * @param {number | BigInt | null} position - Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged. - * @param {function(Error?, number?, Buffer?)} callback + * Adds callback to the 'nextTick' queue. + * @param {Function} callback */ - export function read(fd: number, buffer: object | Buffer | TypedArray, offset: number, length: number, position: number | BigInt | null, options: any, callback: (arg0: Error | null, arg1: number | null, arg2: Buffer | null) => any): void; + export function nextTick(callback: Function, ...args: any[]): void; /** - * Asynchronously read all entries in a directory. - * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} - * @param {string | Buffer | URL } path - * @param {object?|function(Error?, object[])} [options] - * @param {string?} [options.encoding ? 'utf8'] - * @param {boolean?} [options.withFileTypes ? false] - * @param {function(Error?, object[])} callback + * Computed high resolution time as a `BigInt`. + * @param {Array?} [time] + * @return {bigint} */ - export function readdir(path: string | Buffer | URL, options: {}, callback: (arg0: Error | null, arg1: object[]) => any): void; + export function hrtime(time?: Array | null): bigint; + export namespace hrtime { + function bigint(): any; + } /** - * @param {string | Buffer | URL | number } path - * @param {object?|function(Error?, Buffer?)} [options] - * @param {string?} [options.encoding ? 'utf8'] - * @param {string?} [options.flag ? 'r'] - * @param {AbortSignal?} [options.signal] - * @param {function(Error?, Buffer?)} callback + * @param {number=} [code=0] - The exit code. Default: 0. */ - export function readFile(path: string | Buffer | URL | number, options: {}, callback: (arg0: Error | null, arg1: Buffer | null) => any): void; + export function exit(code?: number | undefined): Promise; /** - * Reads link at `path` - * @param {string} path - * @param {function(err, string)} callback + * Returns an object describing the memory usage of the Node.js process measured in bytes. + * @returns {Object} */ - export function readlink(path: string, callback: (arg0: err, arg1: string) => any): void; + export function memoryUsage(): any; + export namespace memoryUsage { + function rss(): any; + } + export class ProcessEnvironmentEvent extends Event { + constructor(type: any, key: any, value: any); + key: any; + value: any; + } + export class ProcessEnvironment extends EventTarget { + get [Symbol.toStringTag](): string; + } + export const env: ProcessEnvironment; + export default process; + const process: any; +} + +declare module "socket:path/path" { /** - * Computes real path for `path` - * @param {string} path - * @param {function(err, string)} callback + * The path.resolve() method resolves a sequence of paths or path segments into an absolute path. + * @param {strig} ...paths + * @returns {string} + * @see {@link https://nodejs.org/api/path.html#path_path_resolve_paths} */ - export function realpath(path: string, callback: (arg0: err, arg1: string) => any): void; + export function resolve(options: any, ...components: any[]): string; /** - * Renames file or directory at `src` to `dest`. - * @param {string} src - * @param {string} dest - * @param {function} callback + * Computes current working directory for a path + * @param {object=} [opts] + * @param {boolean=} [opts.posix] Set to `true` to force POSIX style path + * @return {string} */ - export function rename(src: string, dest: string, callback: Function): void; + export function cwd(opts?: object | undefined): string; /** - * Removes directory at `path`. - * @param {string} path - * @param {function} callback + * Computed location origin. Defaults to `socket:///` if not available. + * @return {string} */ - export function rmdir(path: string, callback: Function): void; + export function origin(): string; /** - * - * @param {string | Buffer | URL | number } path - filename or file descriptor - * @param {object?} options - * @param {string?} [options.encoding ? 'utf8'] - * @param {string?} [options.flag ? 'r'] - * @param {AbortSignal?} [options.signal] - * @param {function(Error?, Stats?)} callback + * Computes the relative path from `from` to `to`. + * @param {object} options + * @param {PathComponent} from + * @param {PathComponent} to + * @return {string} */ - export function stat(path: string | Buffer | URL | number, options: object | null, callback: (arg0: Error | null, arg1: Stats | null) => any): void; + export function relative(options: object, from: PathComponent, to: PathComponent): string; /** - * Creates a symlink of `src` at `dest`. - * @param {string} src - * @param {string} dest + * Joins path components. This function may not return an absolute path. + * @param {object} options + * @param {...PathComponent} components + * @return {string} */ - export function symlink(src: string, dest: string, type: any, callback: any): void; + export function join(options: object, ...components: PathComponent[]): string; /** - * Unlinks (removes) file at `path`. - * @param {string} path - * @param {function} callback + * Computes directory name of path. + * @param {object} options + * @param {...PathComponent} components + * @return {string} */ - export function unlink(path: string, callback: Function): void; + export function dirname(options: object, path: any): string; /** - * @see {@url https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fswritefilefile-data-options-callback} - * @param {string | Buffer | URL | number } path - filename or file descriptor - * @param {string | Buffer | TypedArray | DataView | object } data - * @param {object?} options - * @param {string?} [options.encoding ? 'utf8'] - * @param {string?} [options.mode ? 0o666] - * @param {string?} [options.flag ? 'w'] - * @param {AbortSignal?} [options.signal] - * @param {function(Error?)} callback + * Computes base name of path. + * @param {object} options + * @param {...PathComponent} components + * @return {string} */ - export function writeFile(path: string | Buffer | URL | number, data: string | Buffer | TypedArray | DataView | object, options: object | null, callback: (arg0: Error | null) => any): void; + export function basename(options: object, path: any): string; /** - * Watch for changes at `path` calling `callback` - * @param {string} - * @param {function|object=} [options] - * @param {string=} [options.encoding = 'utf8'] - * @param {?function} [callback] - * @return {Watcher} + * Computes extension name of path. + * @param {object} options + * @param {PathComponent} path + * @return {string} */ - export function watch(path: any, options?: (Function | object) | undefined, callback?: Function | null): Watcher; - export default exports; - export type Buffer = import("socket:buffer").Buffer; - export type TypedArray = Uint8Array | Int8Array; - import { ReadStream } from "socket:fs/stream"; - import { WriteStream } from "socket:fs/stream"; - import { Dir } from "socket:fs/dir"; - import { Stats } from "socket:fs/stats"; - import { Watcher } from "socket:fs/watcher"; - import * as constants from "socket:fs/constants"; - import { DirectoryHandle } from "socket:fs/handle"; - import { Dirent } from "socket:fs/dir"; - import fds from "socket:fs/fds"; - import { FileHandle } from "socket:fs/handle"; - import * as promises from "socket:fs/promises"; - import * as exports from "socket:fs/index"; - - export { constants, Dir, DirectoryHandle, Dirent, fds, FileHandle, promises, ReadStream, Stats, Watcher, WriteStream }; -} -declare module "socket:fs" { - export * from "socket:fs/index"; - export default exports; - import * as exports from "socket:fs/index"; -} -declare module "socket:external/libsodium/index" { - const _default: any; - export default _default; -} -declare module "socket:crypto/sodium" { - export {}; -} -declare module "socket:crypto" { + export function extname(options: object, path: PathComponent): string; /** - * Generate cryptographically strong random values into the `buffer` - * @param {TypedArray} buffer - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues} - * @return {TypedArray} + * Computes normalized path + * @param {object} options + * @param {PathComponent} path + * @return {string} */ - export function getRandomValues(buffer: TypedArray, ...args: any[]): TypedArray; + export function normalize(options: object, path: PathComponent): string; /** - * Generate a random 64-bit number. - * @returns {BigInt} - A random 64-bit number. + * Formats `Path` object into a string. + * @param {object} options + * @param {object|Path} path + * @return {string} */ - export function rand64(): BigInt; + export function format(options: object, path: object | Path): string; /** - * Generate `size` random bytes. - * @param {number} size - The number of bytes to generate. The size must not be larger than 2**31 - 1. - * @returns {Buffer} - A promise that resolves with an instance of socket.Buffer with random bytes. + * Parses input `path` into a `Path` instance. + * @param {PathComponent} path + * @return {object} */ - export function randomBytes(size: number): Buffer; + export function parse(path: PathComponent): object; /** - * @param {string} algorithm - `SHA-1` | `SHA-256` | `SHA-384` | `SHA-512` - * @param {Buffer | TypedArray | DataView} message - An instance of socket.Buffer, TypedArray or Dataview. - * @returns {Promise} - A promise that resolves with an instance of socket.Buffer with the hash. + * @typedef {(string|Path|URL|{ pathname: string }|{ url: string)} PathComponent */ - export function createDigest(algorithm: string, buf: any): Promise; /** - * A murmur3 hash implementation based on https://github.com/jwerle/murmurhash.c - * that works on strings and `ArrayBuffer` views (typed arrays) - * @param {string|Uint8Array|ArrayBuffer} value - * @param {number=} [seed = 0] - * @return {number} + * A container for a parsed Path. */ - export function murmur3(value: string | Uint8Array | ArrayBuffer, seed?: number | undefined): number; + export class Path { + /** + * Creates a `Path` instance from `input` and optional `cwd`. + * @param {PathComponent} input + * @param {string} [cwd] + */ + static from(input: PathComponent, cwd?: string): any; + /** + * `Path` class constructor. + * @protected + * @param {string} pathname + * @param {string} [cwd = Path.cwd()] + */ + protected constructor(); + pattern: { + "__#11@#i": any; + "__#11@#n": {}; + "__#11@#t": {}; + "__#11@#e": {}; + "__#11@#s": {}; + "__#11@#l": boolean; + test(t: {}, r: any): boolean; + exec(t: {}, r: any): { + inputs: any[] | {}[]; + }; + readonly protocol: any; + readonly username: any; + readonly password: any; + readonly hostname: any; + readonly port: any; + readonly pathname: any; + readonly search: any; + readonly hash: any; + readonly hasRegExpGroups: boolean; + }; + url: any; + get pathname(): any; + get protocol(): any; + get href(): any; + /** + * `true` if the path is relative, otherwise `false. + * @type {boolean} + */ + get isRelative(): boolean; + /** + * The working value of this path. + */ + get value(): any; + /** + * The original source, unresolved. + * @type {string} + */ + get source(): string; + /** + * Computed parent path. + * @type {string} + */ + get parent(): string; + /** + * Computed root in path. + * @type {string} + */ + get root(): string; + /** + * Computed directory name in path. + * @type {string} + */ + get dir(): string; + /** + * Computed base name in path. + * @type {string} + */ + get base(): string; + /** + * Computed base name in path without path extension. + * @type {string} + */ + get name(): string; + /** + * Computed extension name in path. + * @type {string} + */ + get ext(): string; + /** + * The computed drive, if given in the path. + * @type {string?} + */ + get drive(): string; + /** + * @return {URL} + */ + toURL(): URL; + /** + * Converts this `Path` instance to a string. + * @return {string} + */ + toString(): string; + /** + * @ignore + */ + inspect(): { + root: string; + dir: string; + base: string; + ext: string; + name: string; + }; + /** + * @ignore + */ + [Symbol.toStringTag](): string; + #private; + } + export default Path; + export type PathComponent = (string | Path | URL | { + pathname: string; + } | { + url: string; + }); + import { URL } from "socket:url/index"; +} + +declare module "socket:path/mounts" { + const _default: {}; + export default _default; +} + +declare module "socket:path/win32" { /** - * @typedef {Uint8Array|Int8Array} TypedArray + * Computes current working directory for a path + * @param {string} */ + export function cwd(): any; /** - * WebCrypto API - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto} - */ - export let webcrypto: any; - /** - * A promise that resolves when all internals to be loaded/ready. - * @type {Promise} + * Resolves path components to an absolute path. + * @param {...PathComponent} components + * @return {string} */ - export const ready: Promise; + export function resolve(...components: PathComponent[]): string; /** - * Maximum total size of random bytes per page + * Joins path components. This function may not return an absolute path. + * @param {...PathComponent} components + * @return {string} */ - export const RANDOM_BYTES_QUOTA: number; + export function join(...components: PathComponent[]): string; /** - * Maximum total size for random bytes. + * Computes directory name of path. + * @param {PathComponent} path + * @return {string} */ - export const MAX_RANDOM_BYTES: 281474976710655; + export function dirname(path: PathComponent): string; /** - * Maximum total amount of allocated per page of bytes (max/quota) + * Computes base name of path. + * @param {PathComponent} path + * @param {string=} [suffix] + * @return {string} */ - export const MAX_RANDOM_BYTES_PAGES: number; - export default exports; - export type TypedArray = Uint8Array | Int8Array; - import { Buffer } from "socket:buffer"; - export namespace sodium { - let ready: Promise; - } - import * as exports from "socket:crypto"; - -} -declare module "socket:ipc" { - export function maybeMakeError(error: any, caller: any): any; + export function basename(path: PathComponent, suffix?: string | undefined): string; /** - * Parses `seq` as integer value - * @param {string|number} seq - * @param {object=} [options] - * @param {boolean} [options.bigint = false] - * @ignore + * Computes extension name of path. + * @param {PathComponent} path + * @return {string} */ - export function parseSeq(seq: string | number, options?: object | undefined): number | bigint; + export function extname(path: PathComponent): string; /** - * If `debug.enabled === true`, then debug output will be printed to console. - * @param {(boolean)} [enable] + * Predicate helper to determine if path is absolute. + * @param {PathComponent} path * @return {boolean} - * @ignore */ - export function debug(enable?: (boolean)): boolean; - export namespace debug { - let enabled: any; - function log(...args: any[]): any; - } + export function isAbsolute(path: PathComponent): boolean; /** - * @ignore + * Parses input `path` into a `Path` instance. + * @param {PathComponent} path + * @return {Path} */ - export function postMessage(message: any, ...args: any[]): Promise; + export function parse(path: PathComponent): Path; /** - * Waits for the native IPC layer to be ready and exposed on the - * global window object. - * @ignore + * Formats `Path` object into a string. + * @param {object|Path} path + * @return {string} */ - export function ready(): Promise; + export function format(path: object | Path): string; /** - * Sends a synchronous IPC command over XHR returning a `Result` - * upon success or error. - * @param {string} command - * @param {any?} [value] - * @param {object?} [options] - * @return {Result} - * @ignore + * Normalizes `path` resolving `..` and `.\` preserving trailing + * slashes. + * @param {string} path */ - export function sendSync(command: string, value?: any | null, options?: object | null): Result; + export function normalize(path: string): any; /** - * Emit event to be dispatched on `window` object. - * @param {string} name - * @param {any} value - * @param {EventTarget=} [target = window] - * @param {Object=} options + * Computes the relative path from `from` to `to`. + * @param {string} from + * @param {string} to + * @return {string} */ - export function emit(name: string, value: any, target?: EventTarget | undefined, options?: any | undefined): Promise; + export function relative(from: string, to: string): string; + export default exports; + export namespace win32 { + let sep: "\\"; + let delimiter: ";"; + } + export type PathComponent = import("socket:path/path").PathComponent; + import { Path } from "socket:path/path"; + import * as mounts from "socket:path/mounts"; + import * as posix from "socket:path/posix"; + import { DOWNLOADS } from "socket:path/well-known"; + import { DOCUMENTS } from "socket:path/well-known"; + import { RESOURCES } from "socket:path/well-known"; + import { PICTURES } from "socket:path/well-known"; + import { DESKTOP } from "socket:path/well-known"; + import { VIDEOS } from "socket:path/well-known"; + import { CONFIG } from "socket:path/well-known"; + import { MEDIA } from "socket:path/well-known"; + import { MUSIC } from "socket:path/well-known"; + import { HOME } from "socket:path/well-known"; + import { DATA } from "socket:path/well-known"; + import { LOG } from "socket:path/well-known"; + import { TMP } from "socket:path/well-known"; + import * as exports from "socket:path/win32"; + + export { mounts, posix, Path, DOWNLOADS, DOCUMENTS, RESOURCES, PICTURES, DESKTOP, VIDEOS, CONFIG, MEDIA, MUSIC, HOME, DATA, LOG, TMP }; +} + +declare module "socket:path/posix" { /** - * Resolves a request by `seq` with possible value. - * @param {string} seq - * @param {any} value - * @ignore + * Computes current working directory for a path + * @param {string} + * @return {string} */ - export function resolve(seq: string, value: any): Promise; + export function cwd(): string; /** - * Sends an async IPC command request with parameters. - * @param {string} command - * @param {any=} value - * @param {object=} [options] - * @param {boolean=} [options.cache=false] - * @param {boolean=} [options.bytes=false] - * @return {Promise} + * Resolves path components to an absolute path. + * @param {...PathComponent} components + * @return {string} */ - export function send(command: string, value?: any | undefined, options?: object | undefined): Promise; + export function resolve(...components: PathComponent[]): string; /** - * Sends an async IPC command request with parameters and buffered bytes. - * @param {string} command - * @param {any=} value - * @param {(Buffer|Uint8Array|ArrayBuffer|string|Array)=} buffer - * @param {object=} options - * @ignore + * Joins path components. This function may not return an absolute path. + * @param {...PathComponent} components + * @return {string} */ - export function write(command: string, value?: any | undefined, buffer?: (Buffer | Uint8Array | ArrayBuffer | string | any[]) | undefined, options?: object | undefined): Promise; + export function join(...components: PathComponent[]): string; /** - * Sends an async IPC command request with parameters requesting a response - * with buffered bytes. - * @param {string} command - * @param {any=} value - * @param {object=} options - * @ignore + * Computes directory name of path. + * @param {PathComponent} path + * @return {string} */ - export function request(command: string, value?: any | undefined, options?: object | undefined): Promise; + export function dirname(path: PathComponent): string; /** - * Factory for creating a proxy based IPC API. - * @param {string} domain - * @param {(function|object)=} ctx - * @param {string=} [ctx.default] - * @return {Proxy} - * @ignore + * Computes base name of path. + * @param {PathComponent} path + * @param {string=} [suffix] + * @return {string} */ - export function createBinding(domain: string, ctx?: (Function | object) | undefined): ProxyConstructor; + export function basename(path: PathComponent, suffix?: string | undefined): string; /** - * Represents an OK IPC status. - * @ignore + * Computes extension name of path. + * @param {PathComponent} path + * @return {string} */ - export const OK: 0; + export function extname(path: PathComponent): string; /** - * Represents an ERROR IPC status. - * @ignore + * Predicate helper to determine if path is absolute. + * @param {PathComponent} path + * @return {boolean} */ - export const ERROR: 1; + export function isAbsolute(path: PathComponent): boolean; /** - * Timeout in milliseconds for IPC requests. - * @ignore + * Parses input `path` into a `Path` instance. + * @param {PathComponent} path + * @return {Path} */ - export const TIMEOUT: number; + export function parse(path: PathComponent): Path; /** - * Symbol for the `ipc.debug.enabled` property - * @ignore + * Formats `Path` object into a string. + * @param {object|Path} path + * @return {string} */ - export const kDebugEnabled: unique symbol; + export function format(path: object | Path): string; /** - * @ignore + * Normalizes `path` resolving `..` and `./` preserving trailing + * slashes. + * @param {string} path */ - export class Headers extends globalThis.Headers { - /** - * @ignore - */ - static from(input: any): any; - /** - * @ignore - */ - get length(): number; - /** - * @ignore - */ - toJSON(): { - [k: string]: string; - }; - } - const Message_base: any; + export function normalize(path: string): any; /** - * A container for a IPC message based on a `ipc://` URI scheme. - * @ignore + * Computes the relative path from `from` to `to`. + * @param {string} from + * @param {string} to + * @return {string} */ - export class Message extends Message_base { - [x: string]: any; - /** - * The expected protocol for an IPC message. - * @ignore - */ - static get PROTOCOL(): string; - /** - * Creates a `Message` instance from a variety of input. - * @param {string|URL|Message|Buffer|object} input - * @param {(object|string|URLSearchParams)=} [params] - * @param {(ArrayBuffer|Uint8Array|string)?} [bytes] - * @return {Message} - * @ignore - */ - static from(input: string | URL | Message | Buffer | object, params?: (object | string | URLSearchParams) | undefined, bytes?: (ArrayBuffer | Uint8Array | string) | null): Message; - /** - * Predicate to determine if `input` is valid for constructing - * a new `Message` instance. - * @param {string|URL|Message|Buffer|object} input - * @return {boolean} - * @ignore - */ - static isValidInput(input: string | URL | Message | Buffer | object): boolean; - /** - * `Message` class constructor. - * @protected - * @param {string|URL} input - * @param {(object|Uint8Array)?} [bytes] - * @ignore - */ - protected constructor(); - /** - * @type {Uint8Array?} - * @ignore - */ - bytes: Uint8Array | null; - /** - * Computed IPC message name. - * @type {string} - * @ignore - */ - get command(): string; - /** - * Computed IPC message name. - * @type {string} - * @ignore - */ - get name(): string; - /** - * Computed `id` value for the command. - * @type {string} - * @ignore - */ - get id(): string; - /** - * Computed `seq` (sequence) value for the command. - * @type {string} - * @ignore - */ - get seq(): string; - /** - * Computed message value potentially given in message parameters. - * This value is automatically decoded, but not treated as JSON. - * @type {string} - * @ignore - */ - get value(): string; + export function relative(from: string, to: string): string; + export default exports; + export namespace posix { + let sep: "/"; + let delimiter: ":"; + } + export type PathComponent = import("socket:path/path").PathComponent; + import { Path } from "socket:path/path"; + import * as mounts from "socket:path/mounts"; + import * as win32 from "socket:path/win32"; + import { DOWNLOADS } from "socket:path/well-known"; + import { DOCUMENTS } from "socket:path/well-known"; + import { RESOURCES } from "socket:path/well-known"; + import { PICTURES } from "socket:path/well-known"; + import { DESKTOP } from "socket:path/well-known"; + import { VIDEOS } from "socket:path/well-known"; + import { CONFIG } from "socket:path/well-known"; + import { MEDIA } from "socket:path/well-known"; + import { MUSIC } from "socket:path/well-known"; + import { HOME } from "socket:path/well-known"; + import { DATA } from "socket:path/well-known"; + import { LOG } from "socket:path/well-known"; + import { TMP } from "socket:path/well-known"; + import * as exports from "socket:path/posix"; + + export { mounts, win32, Path, DOWNLOADS, DOCUMENTS, RESOURCES, PICTURES, DESKTOP, VIDEOS, CONFIG, MEDIA, MUSIC, HOME, DATA, LOG, TMP }; +} + +declare module "socket:path/index" { + export default exports; + import * as mounts from "socket:path/mounts"; + import * as posix from "socket:path/posix"; + import * as win32 from "socket:path/win32"; + import { Path } from "socket:path/path"; + import { DOWNLOADS } from "socket:path/well-known"; + import { DOCUMENTS } from "socket:path/well-known"; + import { RESOURCES } from "socket:path/well-known"; + import { PICTURES } from "socket:path/well-known"; + import { DESKTOP } from "socket:path/well-known"; + import { VIDEOS } from "socket:path/well-known"; + import { CONFIG } from "socket:path/well-known"; + import { MEDIA } from "socket:path/well-known"; + import { MUSIC } from "socket:path/well-known"; + import { HOME } from "socket:path/well-known"; + import { DATA } from "socket:path/well-known"; + import { LOG } from "socket:path/well-known"; + import { TMP } from "socket:path/well-known"; + import * as exports from "socket:path/index"; + + export { mounts, posix, win32, Path, DOWNLOADS, DOCUMENTS, RESOURCES, PICTURES, DESKTOP, VIDEOS, CONFIG, MEDIA, MUSIC, HOME, DATA, LOG, TMP }; +} + +declare module "socket:path" { + export const sep: "\\" | "/"; + export const delimiter: ":" | ";"; + export const resolve: typeof posix.win32.resolve; + export const join: typeof posix.win32.join; + export const dirname: typeof posix.win32.dirname; + export const basename: typeof posix.win32.basename; + export const extname: typeof posix.win32.extname; + export const cwd: typeof posix.win32.cwd; + export const isAbsolute: typeof posix.win32.isAbsolute; + export const parse: typeof posix.win32.parse; + export const format: typeof posix.win32.format; + export const normalize: typeof posix.win32.normalize; + export const relative: typeof posix.win32.relative; + const _default: typeof posix | typeof posix.win32; + export default _default; + import { posix } from "socket:path/index"; + import { Path } from "socket:path/index"; + import { win32 } from "socket:path/index"; + import { mounts } from "socket:path/index"; + import { DOWNLOADS } from "socket:path/index"; + import { DOCUMENTS } from "socket:path/index"; + import { RESOURCES } from "socket:path/index"; + import { PICTURES } from "socket:path/index"; + import { DESKTOP } from "socket:path/index"; + import { VIDEOS } from "socket:path/index"; + import { CONFIG } from "socket:path/index"; + import { MEDIA } from "socket:path/index"; + import { MUSIC } from "socket:path/index"; + import { HOME } from "socket:path/index"; + import { DATA } from "socket:path/index"; + import { LOG } from "socket:path/index"; + import { TMP } from "socket:path/index"; + export { Path, posix, win32, mounts, DOWNLOADS, DOCUMENTS, RESOURCES, PICTURES, DESKTOP, VIDEOS, CONFIG, MEDIA, MUSIC, HOME, DATA, LOG, TMP }; +} + +declare module "socket:fs/constants" { + /** + * This flag can be used with uv_fs_copyfile() to return an error if the + * destination already exists. + */ + export const COPYFILE_EXCL: 1; + /** + * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. + * If copy-on-write is not supported, a fallback copy mechanism is used. + */ + export const COPYFILE_FICLONE: 2; + /** + * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. + * If copy-on-write is not supported, an error is returned. + */ + export const COPYFILE_FICLONE_FORCE: 4; + export const UV_DIRENT_UNKNOWN: any; + export const UV_DIRENT_FILE: any; + export const UV_DIRENT_DIR: any; + export const UV_DIRENT_LINK: any; + export const UV_DIRENT_FIFO: any; + export const UV_DIRENT_SOCKET: any; + export const UV_DIRENT_CHAR: any; + export const UV_DIRENT_BLOCK: any; + export const UV_FS_SYMLINK_DIR: any; + export const UV_FS_SYMLINK_JUNCTION: any; + export const UV_FS_O_FILEMAP: any; + export const O_RDONLY: any; + export const O_WRONLY: any; + export const O_RDWR: any; + export const O_APPEND: any; + export const O_ASYNC: any; + export const O_CLOEXEC: any; + export const O_CREAT: any; + export const O_DIRECT: any; + export const O_DIRECTORY: any; + export const O_DSYNC: any; + export const O_EXCL: any; + export const O_LARGEFILE: any; + export const O_NOATIME: any; + export const O_NOCTTY: any; + export const O_NOFOLLOW: any; + export const O_NONBLOCK: any; + export const O_NDELAY: any; + export const O_PATH: any; + export const O_SYNC: any; + export const O_TMPFILE: any; + export const O_TRUNC: any; + export const S_IFMT: any; + export const S_IFREG: any; + export const S_IFDIR: any; + export const S_IFCHR: any; + export const S_IFBLK: any; + export const S_IFIFO: any; + export const S_IFLNK: any; + export const S_IFSOCK: any; + export const S_IRWXU: any; + export const S_IRUSR: any; + export const S_IWUSR: any; + export const S_IXUSR: any; + export const S_IRWXG: any; + export const S_IRGRP: any; + export const S_IWGRP: any; + export const S_IXGRP: any; + export const S_IRWXO: any; + export const S_IROTH: any; + export const S_IWOTH: any; + export const S_IXOTH: any; + export const F_OK: any; + export const R_OK: any; + export const W_OK: any; + export const X_OK: any; + export default exports; + import * as exports from "socket:fs/constants"; + +} + +declare module "socket:fs/stream" { + export const DEFAULT_STREAM_HIGH_WATER_MARK: number; + /** + * @typedef {import('./handle.js').FileHandle} FileHandle + */ + /** + * A `Readable` stream for a `FileHandle`. + */ + export class ReadStream extends Readable { + end: any; + start: any; + handle: any; + buffer: ArrayBuffer; + signal: any; + timeout: any; + bytesRead: number; + shouldEmitClose: boolean; /** - * Computed `index` value for the command potentially referring to - * the window index the command is scoped to or originating from. If not - * specified in the message parameters, then this value defaults to `-1`. - * @type {number} - * @ignore + * Sets file handle for the ReadStream. + * @param {FileHandle} handle */ - get index(): number; + setHandle(handle: FileHandle): void; /** - * Computed value parsed as JSON. This value is `null` if the value is not present - * or it is invalid JSON. - * @type {object?} - * @ignore + * The max buffer size for the ReadStream. */ - get json(): any; + get highWaterMark(): number; /** - * Computed readonly object of message parameters. - * @type {object} - * @ignore + * Relative or absolute path of the underlying `FileHandle`. */ - get params(): any; + get path(): any; /** - * Gets unparsed message parameters. - * @type {Array>} - * @ignore + * `true` if the stream is in a pending state. */ - get rawParams(): string[][]; + get pending(): boolean; + _open(callback: any): Promise; + _read(callback: any): Promise; + } + export namespace ReadStream { + export { DEFAULT_STREAM_HIGH_WATER_MARK as highWaterMark }; + } + /** + * A `Writable` stream for a `FileHandle`. + */ + export class WriteStream extends Writable { + start: any; + handle: any; + signal: any; + timeout: any; + bytesWritten: number; + shouldEmitClose: boolean; /** - * Returns computed parameters as entries - * @return {Array>} - * @ignore + * Sets file handle for the WriteStream. + * @param {FileHandle} handle */ - entries(): Array>; + setHandle(handle: FileHandle): void; /** - * Set a parameter `value` by `key`. - * @param {string} key - * @param {any} value - * @ignore + * The max buffer size for the Writetream. */ - set(key: string, value: any): any; + get highWaterMark(): number; /** - * Get a parameter value by `key`. - * @param {string} key - * @param {any} defaultValue - * @return {any} - * @ignore + * Relative or absolute path of the underlying `FileHandle`. */ - get(key: string, defaultValue: any): any; + get path(): any; /** - * Delete a parameter by `key`. - * @param {string} key - * @return {boolean} - * @ignore + * `true` if the stream is in a pending state. */ - delete(key: string): boolean; - /** - * Computed parameter keys. - * @return {Array} - * @ignore - */ - keys(): Array; - /** - * Computed parameter values. - * @return {Array} - * @ignore - */ - values(): Array; - /** - * Predicate to determine if parameter `key` is present in parameters. - * @param {string} key - * @return {boolean} - * @ignore - */ - has(key: string): boolean; + get pending(): boolean; + _open(callback: any): Promise; + _write(buffer: any, callback: any): any; + } + export namespace WriteStream { + export { DEFAULT_STREAM_HIGH_WATER_MARK as highWaterMark }; } + export const FileReadStream: typeof exports.ReadStream; + export const FileWriteStream: typeof exports.WriteStream; + export default exports; + export type FileHandle = import("socket:fs/handle").FileHandle; + import { Readable } from "socket:stream"; + import { Writable } from "socket:stream"; + import * as exports from "socket:fs/stream"; + +} + +declare module "socket:fs/flags" { + export function normalizeFlags(flags: any): any; + export default exports; + import * as exports from "socket:fs/flags"; + +} + +declare module "socket:diagnostics/channels" { /** - * A result type used internally for handling - * IPC result values from the native layer that are in the form - * of `{ err?, data? }`. The `data` and `err` properties on this - * type of object are in tuple form and be accessed at `[data?,err?]` + * Normalizes a channel name to lower case replacing white space, + * hyphens (-), underscores (_), with dots (.). * @ignore */ - export class Result { + export function normalizeName(group: any, name: any): string; + /** + * Used to preallocate a minimum sized array of subscribers for + * a channel. + * @ignore + */ + export const MIN_CHANNEL_SUBSCRIBER_SIZE: 64; + /** + * A general interface for diagnostic channels that can be subscribed to. + */ + export class Channel { + constructor(name: any); + name: any; + group: any; /** - * Creates a `Result` instance from input that may be an object - * like `{ err?, data? }`, an `Error` instance, or just `data`. - * @param {(object|Error|any)?} result - * @param {Error|object} [maybeError] - * @param {string} [maybeSource] - * @param {object|string|Headers} [maybeHeaders] - * @return {Result} - * @ignore + * Computed subscribers for all channels in this group. + * @type {Array} */ - static from(result: (object | Error | any) | null, maybeError?: Error | object, maybeSource?: string, maybeHeaders?: object | string | Headers): Result; + get subscribers(): Function[]; /** - * `Result` class constructor. - * @private - * @param {string?} [id = null] - * @param {Error?} [err = null] - * @param {object?} [data = null] - * @param {string?} [source = null] - * @param {(object|string|Headers)?} [headers = null] - * @ignore + * Accessor for determining if channel has subscribers. This + * is always `false` for `Channel instances and `true` for `ActiveChannel` + * instances. */ - private constructor(); + get hasSubscribers(): boolean; /** - * The unique ID for this result. - * @type {string} - * @ignore + * Computed number of subscribers for this channel. */ - id: string; + get length(): number; /** - * An optional error in the result. - * @type {Error?} - * @ignore + * Resets channel state. + * @param {(boolean)} [shouldOrphan = false] */ - err: Error | null; + reset(shouldOrphan?: (boolean)): void; + channel(name: any): Channel; /** - * Result data if given. - * @type {(string|object|Uint8Array)?} - * @ignore + * Adds an `onMessage` subscription callback to the channel. + * @return {boolean} */ - data: (string | object | Uint8Array) | null; + subscribe(_: any, onMessage: any): boolean; /** - * The source of this result. - * @type {string?} - * @ignore + * Removes an `onMessage` subscription callback from the channel. + * @param {function} onMessage + * @return {boolean} */ - source: string | null; + unsubscribe(_: any, onMessage: Function): boolean; /** - * Result headers, if given. - * @type {Headers?} - * @ignore + * A no-op for `Channel` instances. This function always returns `false`. + * @param {string|object} name + * @param {object=} [message] + * @return Promise */ - headers: Headers | null; + publish(name: string | object, message?: object | undefined): Promise; /** - * Computed result length. + * Returns a string representation of the `ChannelRegistry`. * @ignore */ - get length(): any; + toString(): any; /** + * Iterator interface * @ignore */ - toJSON(): { - headers: { - [k: string]: string; - }; - source: string; - data: any; - err: { - name: string; - message: string; - stack?: string; - cause?: unknown; - type: any; - code: any; - }; - }; + get [Symbol.iterator](): any[]; /** - * Generator for an `Iterable` interface over this instance. + * The `Channel` string tag. * @ignore */ - [Symbol.iterator](): Generator; + [Symbol.toStringTag](): string; + #private; } /** - * @ignore - */ - export const primordials: any; - export default exports; - import { Buffer } from "socket:buffer"; - import { URL } from "socket:url/index"; - import * as exports from "socket:ipc"; - -} -declare module "socket:application/menu" { - /** - * Internal IPC for setting an application menu - * @ignore - */ - export function setMenu(options: any, type: any): Promise; - /** - * Internal IPC for setting an application context menu - * @ignore - */ - export function setContextMenu(options: any): Promise; - /** - * A `Menu` is base class for a `ContextMenu`, `SystemMenu`, or `TrayMenu`. + * An `ActiveChannel` is a prototype implementation for a `Channel` + * that provides an interface what is considered an "active" channel. The + * `hasSubscribers` accessor always returns `true` for this class. */ - export class Menu extends EventTarget { - /** - * `Menu` class constructor. - * @ignore - * @param {string} type - */ - constructor(type: string); + export class ActiveChannel extends Channel { + unsubscribe(onMessage: any): boolean; /** - * The `Menu` instance type. - * @type {('context'|'system'|'tray')?} + * @param {object|any} message + * @return Promise */ - get type(): "tray" | "system" | "context"; + publish(message: object | any): Promise; + } + /** + * A container for a grouping of channels that are named and owned + * by this group. A `ChannelGroup` can also be a regular channel. + */ + export class ChannelGroup extends Channel { /** - * Setter for the level 1 'error'` event listener. - * @ignore - * @type {function(ErrorEvent)?} + * @param {Array} channels + * @param {string} name */ - set onerror(onerror: (arg0: ErrorEvent) => any); + constructor(name: string, channels: Array); + channels: Channel[]; /** - * Level 1 'error'` event listener. - * @type {function(ErrorEvent)?} + * Subscribe to a channel or selection of channels in this group. + * @param {string} name + * @return {boolean} */ - get onerror(): (arg0: ErrorEvent) => any; + subscribe(name: string, onMessage: any): boolean; /** - * Setter for the level 1 'menuitem'` event listener. - * @ignore - * @type {function(MenuItemEvent)?} + * Unsubscribe from a channel or selection of channels in this group. + * @param {string} name + * @return {boolean} */ - set onmenuitem(onmenuitem: (arg0: menuitemEvent) => any); + unsubscribe(name: string, onMessage: any): boolean; /** - * Level 1 'menuitem'` event listener. - * @type {function(menuitemEvent)?} + * Gets or creates a channel for this group. + * @param {string} name + * @return {Channel} */ - get onmenuitem(): (arg0: menuitemEvent) => any; + channel(name: string): Channel; /** - * Set the menu layout for this `Menu` instance. - * @param {string|object} layoutOrOptions - * @param {object=} [options] + * Select a test of channels from this group. + * The following syntax is supported: + * - One Channel: `group.channel` + * - All Channels: `*` + * - Many Channel: `group.*` + * - Collections: `['group.a', 'group.b', 'group.c'] or `group.a,group.b,group.c` + * @param {string|Array} keys + * @param {(boolean)} [hasSubscribers = false] - Enforce subscribers in selection + * @return {Array<{name: string, channel: Channel}>} */ - set(layoutOrOptions: string | object, options?: object | undefined): Promise; - #private; + select(keys: string | Array, hasSubscribers?: (boolean)): Array<{ + name: string; + channel: Channel; + }>; } /** - * A container for various `Menu` instances. + * An object mapping of named channels to `WeakRef` instances. */ - export class MenuContainer extends EventTarget { + export const registry: { /** - * `MenuContainer` class constructor. - * @param {EventTarget} [sourceEventTarget] - * @param {object=} [options] + * Subscribes callback `onMessage` to channel of `name`. + * @param {string} name + * @param {function} onMessage + * @return {boolean} */ - constructor(sourceEventTarget?: EventTarget, options?: object | undefined); + subscribe(name: string, onMessage: Function): boolean; /** - * Setter for the level 1 'error'` event listener. - * @ignore - * @type {function(ErrorEvent)?} - */ - set onerror(onerror: (arg0: ErrorEvent) => any); - /** - * Level 1 'error'` event listener. - * @type {function(ErrorEvent)?} - */ - get onerror(): (arg0: ErrorEvent) => any; - /** - * Setter for the level 1 'menuitem'` event listener. - * @ignore - * @type {function(MenuItemEvent)?} + * Unsubscribes callback `onMessage` from channel of `name`. + * @param {string} name + * @param {function} onMessage + * @return {boolean} */ - set onmenuitem(onmenuitem: (arg0: menuitemEvent) => any); + unsubscribe(name: string, onMessage: Function): boolean; /** - * Level 1 'menuitem'` event listener. - * @type {function(menuitemEvent)?} + * Predicate to determine if a named channel has subscribers. + * @param {string} name */ - get onmenuitem(): (arg0: menuitemEvent) => any; + hasSubscribers(name: string): boolean; /** - * The `TrayMenu` instance for the application. - * @type {TrayMenu} + * Get or set a channel by `name`. + * @param {string} name + * @return {Channel} */ - get tray(): TrayMenu; + channel(name: string): Channel; /** - * The `SystemMenu` instance for the application. - * @type {SystemMenu} + * Creates a `ChannelGroup` for a set of channels + * @param {string} name + * @param {Array} [channels] + * @return {ChannelGroup} */ - get system(): SystemMenu; + group(name: string, channels?: Array): ChannelGroup; /** - * The `ContextMenu` instance for the application. - * @type {ContextMenu} + * Get a channel by name. The name is normalized. + * @param {string} name + * @return {Channel?} */ - get context(): ContextMenu; - #private; - } - /** - * A `Menu` instance that represents a context menu. - */ - export class ContextMenu extends Menu { - constructor(); - } - /** - * A `Menu` instance that represents the system menu. - */ - export class SystemMenu extends Menu { - constructor(); - } - /** - * A `Menu` instance that represents the tray menu. - */ - export class TrayMenu extends Menu { - constructor(); - } - /** - * The application tray menu. - * @type {TrayMenu} - */ - export const tray: TrayMenu; - /** - * The application system menu. - * @type {SystemMenu} - */ - export const system: SystemMenu; - /** - * The application context menu. - * @type {ContextMenu} - */ - export const context: ContextMenu; - /** - * The application menus container. - * @type {MenuContainer} - */ - export const container: MenuContainer; - export default container; - import ipc from "socket:ipc"; -} -declare module "socket:internal/events" { - /** - * An event dispatched when an application URL is opening the application. - */ - export class ApplicationURLEvent extends Event { + get(name: string): Channel | null; /** - * `ApplicationURLEvent` class constructor. - * @param {string=} [type] - * @param {object=} [options] + * Checks if a channel is known by name. The name is normalized. + * @param {string} name + * @return {boolean} */ - constructor(type?: string | undefined, options?: object | undefined); + has(name: string): boolean; /** - * `true` if the application URL is valid (parses correctly). - * @type {boolean} + * Set a channel by name. The name is normalized. + * @param {string} name + * @param {Channel} channel + * @return {Channel?} */ - get isValid(): boolean; + set(name: string, channel: Channel): Channel | null; /** - * Data associated with the `ApplicationURLEvent`. - * @type {?any} + * Removes a channel by `name` + * @return {boolean} */ - get data(): any; + remove(name: any): boolean; /** - * The original source URI - * @type {?string} + * Returns a string representation of the `ChannelRegistry`. + * @ignore */ - get source(): string; + toString(): any; /** - * The `URL` for the `ApplicationURLEvent`. - * @type {?URL} + * Returns a JSON representation of the `ChannelRegistry`. + * @return {object} */ - get url(): URL; + toJSON(): object; /** - * String tag name for an `ApplicationURLEvent` instance. - * @type {string} + * The `ChannelRegistry` string tag. + * @ignore */ - get [Symbol.toStringTag](): string; - #private; + [Symbol.toStringTag](): string; + }; + export default registry; +} + +declare module "socket:diagnostics/metric" { + export class Metric { + init(): void; + update(value: any): void; + destroy(): void; + toJSON(): {}; + toString(): string; + [Symbol.iterator](): any; + [Symbol.toStringTag](): string; + } + export default Metric; +} + +declare module "socket:diagnostics/window" { + export class RequestAnimationFrameMetric extends Metric { + constructor(options: any); + originalRequestAnimationFrame: typeof requestAnimationFrame; + requestAnimationFrame(callback: any): any; + sampleSize: any; + sampleTick: number; + channel: import("socket:diagnostics/channels").Channel; + value: { + rate: number; + samples: number; + }; + now: number; + samples: Uint8Array; + toJSON(): { + sampleSize: any; + sampleTick: number; + samples: number[]; + rate: number; + now: number; + }; + } + export class FetchMetric extends Metric { + constructor(options: any); + originalFetch: typeof fetch; + channel: import("socket:diagnostics/channels").Channel; + fetch(resource: any, options: any, extra: any): Promise; + } + export class XMLHttpRequestMetric extends Metric { + constructor(options: any); + channel: import("socket:diagnostics/channels").Channel; + patched: { + open: { + (method: string, url: string | URL): void; + (method: string, url: string | URL, async: boolean, username?: string | null, password?: string | null): void; + }; + send: (body?: Document | XMLHttpRequestBodyInit | null) => void; + }; + } + export class WorkerMetric extends Metric { + constructor(options: any); + GlobalWorker: { + new (scriptURL: string | URL, options?: WorkerOptions): Worker; + prototype: Worker; + } | { + new (): {}; + }; + channel: import("socket:diagnostics/channels").Channel; + Worker: { + new (url: any, options: any, ...args: any[]): {}; + }; + } + export const metrics: { + requestAnimationFrame: RequestAnimationFrameMetric; + XMLHttpRequest: XMLHttpRequestMetric; + Worker: WorkerMetric; + fetch: FetchMetric; + channel: import("socket:diagnostics/channels").ChannelGroup; + subscribe(...args: any[]): boolean; + unsubscribe(...args: any[]): boolean; + start(which: any): void; + stop(which: any): void; + }; + namespace _default { + export { metrics }; } + export default _default; + import { Metric } from "socket:diagnostics/metric"; +} + +declare module "socket:diagnostics/runtime" { /** - * An event dispacted for a registered global hotkey expression. + * Queries runtime diagnostics. + * @return {Promise} */ - export class HotKeyEvent extends MessageEvent { + export function query(type: any): Promise; + /** + * A base container class for diagnostic information. + */ + export class Diagnostic { /** - * `HotKeyEvent` class constructor. - * @ignore - * @param {string=} [type] - * @param {object=} [data] + * A container for handles related to the diagnostics */ - constructor(type?: string | undefined, data?: object | undefined); + static Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; /** - * The global unique ID for this hotkey binding. - * @type {number?} - */ - get id(): number; + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + } + /** + * A container for libuv diagnostics + */ + export class UVDiagnostic extends Diagnostic { + /** + * A container for libuv metrics. + */ + static Metrics: { + new (): { + /** + * The number of event loop iterations. + * @type {number} + */ + loopCount: number; + /** + * Number of events that have been processed by the event handler. + * @type {number} + */ + events: number; + /** + * Number of events that were waiting to be processed when the + * event provider was called. + * @type {number} + */ + eventsWaiting: number; + }; + }; /** - * The computed hash for this hotkey binding. - * @type {number?} - */ - get hash(): number; + * Known libuv metrics for this diagnostic. + * @type {UVDiagnostic.Metrics} + */ + metrics: { + new (): { + /** + * The number of event loop iterations. + * @type {number} + */ + loopCount: number; + /** + * Number of events that have been processed by the event handler. + * @type {number} + */ + events: number; + /** + * Number of events that were waiting to be processed when the + * event provider was called. + * @type {number} + */ + eventsWaiting: number; + }; + }; /** - * The normalized hotkey expression as a sequence of tokens. - * @type {string[]} + * The current idle time of the libuv loop + * @type {number} */ - get sequence(): string[]; + idleTime: number; /** - * The original expression of the hotkey binding. - * @type {string?} + * The number of active requests in the libuv loop + * @type {number} */ - get expression(): string; + activeRequests: number; } /** - * An event dispacted when a menu item is selected. + * A container for Core Post diagnostics. */ - export class MenuItemEvent extends MessageEvent { - /** - * `MenuItemEvent` class constructor - * @ignore - * @param {string=} [type] - * @param {object=} [data] - * @param {import('../application/menu.js').Menu} menu - */ - constructor(type?: string | undefined, data?: object | undefined, menu?: import('../application/menu.js').Menu); - /** - * The `Menu` this event has been dispatched for. - * @type {import('../application/menu.js').Menu?} - */ - get menu(): import("socket:application/menu").Menu; - /** - * The title of the menu item. - * @type {string?} - */ - get title(): string; - /** - * An optional tag value for the menu item that may also be the - * parent menu item title. - * @type {string?} - */ - get tag(): string; - /** - * The parent title of the menu item. - * @type {string?} - */ - get parent(): string; - #private; + export class PostsDiagnostic extends Diagnostic { } - namespace _default { - export { ApplicationURLEvent }; - export { MenuItemEvent }; - export { HotKeyEvent }; - } - export default _default; -} -declare module "socket:window/hotkey" { /** - * Normalizes an expression string. - * @param {string} expression - * @return {string} + * A container for child process diagnostics. */ - export function normalizeExpression(expression: string): string; + export class ChildProcessDiagnostic extends Diagnostic { + } /** - * Bind a global hotkey expression. - * @param {string} expression - * @param {{ passive?: boolean }} [options] - * @return {Promise} - */ - export function bind(expression: string, options?: { - passive?: boolean; - }): Promise; + * A container for AI diagnostics. + */ + export class AIDiagnostic extends Diagnostic { + /** + * A container for AI LLM diagnostics. + */ + static LLMDiagnostic: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * Known AI LLM diagnostics. + * @type {AIDiagnostic.LLMDiagnostic} + */ + llm: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + } /** - * Bind a global hotkey expression. - * @param {string} expression - * @param {object=} [options] - * @return {Promise} - */ - export function unbind(id: any, options?: object | undefined): Promise; + * A container for various filesystem diagnostics. + */ + export class FSDiagnostic extends Diagnostic { + /** + * A container for filesystem watcher diagnostics. + */ + static WatchersDiagnostic: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for filesystem descriptors diagnostics. + */ + static DescriptorsDiagnostic: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * Known FS watcher diagnostics. + * @type {FSDiagnostic.WatchersDiagnostic} + */ + watchers: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * @type {FSDiagnostic.DescriptorsDiagnostic} + */ + descriptors: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + } /** - * Get all known globally register hotkey bindings. - * @param {object=} [options] - * @return {Promise} - */ - export function getBindings(options?: object | undefined): Promise; + * A container for various timers diagnostics. + */ + export class TimersDiagnostic extends Diagnostic { + /** + * A container for core timeout timer diagnostics. + */ + static TimeoutDiagnostic: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for core interval timer diagnostics. + */ + static IntervalDiagnostic: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for core immediate timer diagnostics. + */ + static ImmediateDiagnostic: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * @type {TimersDiagnostic.TimeoutDiagnostic} + */ + timeout: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * @type {TimersDiagnostic.IntervalDiagnostic} + */ + interval: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * @type {TimersDiagnostic.ImmediateDiagnostic} + */ + immediate: { + new (): { + /** + * Known handles for this diagnostics. + * @type {Diagnostic.Handles} + */ + handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + /** + * A container for handles related to the diagnostics + */ + Handles: { + new (): { + /** + * The nunmber of handles in this diagnostics. + * @type {number} + */ + count: number; + /** + * A set of known handle IDs + * @type {string[]} + */ + ids: string[]; + }; + }; + }; + } /** - * Get all known possible keyboard modifier and key mappings for - * expression bindings. - * @param {object=} [options] - * @return {Promise<{ keys: object, modifiers: object }>} + * A container for UDP diagnostics. */ - export function getMappings(options?: object | undefined): Promise<{ - keys: object; - modifiers: object; - }>; + export class UDPDiagnostic extends Diagnostic { + } /** - * Adds an event listener to the global active bindings. This function is just - * proxy to `bindings.addEventListener`. - * @param {string} type - * @param {function(Event)} listener - * @param {(boolean|object)=} [optionsOrUseCapture] + * A container for various queried runtime diagnostics. */ - export function addEventListener(type: string, listener: (arg0: Event) => any, optionsOrUseCapture?: (boolean | object) | undefined): void; + export class QueryDiagnostic { + posts: PostsDiagnostic; + childProcess: ChildProcessDiagnostic; + ai: AIDiagnostic; + fs: FSDiagnostic; + timers: TimersDiagnostic; + udp: UDPDiagnostic; + uv: UVDiagnostic; + } + namespace _default { + export { query }; + } + export default _default; +} + +declare module "socket:diagnostics/index" { /** - * Removes an event listener to the global active bindings. This function is - * just a proxy to `bindings.removeEventListener` - * @param {string} type - * @param {function(Event)} listener - * @param {(boolean|object)=} [optionsOrUseCapture] + * @param {string} name + * @return {import('./channels.js').Channel} */ - export function removeEventListener(type: string, listener: (arg0: Event) => any, optionsOrUseCapture?: (boolean | object) | undefined): void; + export function channel(name: string): import("socket:diagnostics/channels").Channel; + export default exports; + import * as exports from "socket:diagnostics/index"; + import channels from "socket:diagnostics/channels"; + import window from "socket:diagnostics/window"; + import runtime from "socket:diagnostics/runtime"; + + export { channels, window, runtime }; +} + +declare module "socket:diagnostics" { + export * from "socket:diagnostics/index"; + export default exports; + import * as exports from "socket:diagnostics/index"; +} + +declare module "socket:fs/stats" { /** - * A high level bindings container map that dispatches events. + * A container for various stats about a file or directory. */ - export class Bindings extends EventTarget { + export class Stats { /** - * `Bindings` class constructor. - * @ignore - * @param {EventTarget} [sourceEventTarget] + * Creates a `Stats` instance from input, optionally with `BigInt` data types + * @param {object|Stats} [stat] + * @param {fromBigInt=} [fromBigInt = false] + * @return {Stats} */ - constructor(sourceEventTarget?: EventTarget); + static from(stat?: object | Stats, fromBigInt?: any | undefined): Stats; /** - * Global `HotKeyEvent` event listener for `Binding` instance event dispatch. - * @ignore - * @param {import('../internal/events.js').HotKeyEvent} event + * `Stats` class constructor. + * @param {object|Stats} stat */ - onHotKey(event: import('../internal/events.js').HotKeyEvent): boolean; + constructor(stat: object | Stats); + dev: any; + ino: any; + mode: any; + nlink: any; + uid: any; + gid: any; + rdev: any; + size: any; + blksize: any; + blocks: any; + atimeMs: any; + mtimeMs: any; + ctimeMs: any; + birthtimeMs: any; + atime: Date; + mtime: Date; + ctime: Date; + birthtime: Date; /** - * The number of `Binding` instances in the mapping. - * @type {number} + * Returns `true` if stats represents a directory. + * @return {Boolean} */ - get size(): number; + isDirectory(): boolean; /** - * Setter for the level 1 'error'` event listener. - * @ignore - * @type {function(ErrorEvent)?} + * Returns `true` if stats represents a file. + * @return {Boolean} */ - set onerror(onerror: (arg0: ErrorEvent) => any); + isFile(): boolean; /** - * Level 1 'error'` event listener. - * @type {function(ErrorEvent)?} + * Returns `true` if stats represents a block device. + * @return {Boolean} */ - get onerror(): (arg0: ErrorEvent) => any; + isBlockDevice(): boolean; /** - * Setter for the level 1 'hotkey'` event listener. - * @ignore - * @type {function(HotKeyEvent)?} + * Returns `true` if stats represents a character device. + * @return {Boolean} */ - set onhotkey(onhotkey: (arg0: hotkeyEvent) => any); + isCharacterDevice(): boolean; /** - * Level 1 'hotkey'` event listener. - * @type {function(hotkeyEvent)?} + * Returns `true` if stats represents a symbolic link. + * @return {Boolean} */ - get onhotkey(): (arg0: hotkeyEvent) => any; + isSymbolicLink(): boolean; /** - * Initializes bindings from global context. - * @ignore - * @return {Promise} + * Returns `true` if stats represents a FIFO. + * @return {Boolean} */ - init(): Promise; + isFIFO(): boolean; /** - * Get a binding by `id` - * @param {number} id - * @return {Binding} + * Returns `true` if stats represents a socket. + * @return {Boolean} */ - get(id: number): Binding; + isSocket(): boolean; + } + export default exports; + import * as exports from "socket:fs/stats"; + +} + +declare module "socket:fs/fds" { + const _default: { + types: Map; + fds: Map; + ids: Map; + readonly size: number; + get(id: any): any; + syncOpenDescriptors(): Promise; + set(id: any, fd: any, type: any): void; + has(id: any): boolean; + fd(id: any): any; + id(fd: any): any; + release(id: any, closeDescriptor?: boolean): Promise; + retain(id: any): Promise; + delete(id: any): void; + clear(): void; + typeof(id: any): any; + entries(): IterableIterator<[any, any]>; + }; + export default _default; +} + +declare module "socket:fs/handle" { + export const kOpening: unique symbol; + export const kClosing: unique symbol; + export const kClosed: unique symbol; + /** + * A container for a descriptor tracked in `fds` and opened in the native layer. + * This class implements the Node.js `FileHandle` interface + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-filehandle} + */ + export class FileHandle extends EventEmitter { + static get DEFAULT_ACCESS_MODE(): any; + static get DEFAULT_OPEN_FLAGS(): string; + static get DEFAULT_OPEN_MODE(): number; /** - * Set a `binding` a by `id`. - * @param {number} id - * @param {Binding} binding + * Creates a `FileHandle` from a given `id` or `fd` + * @param {string|number|FileHandle|object|FileSystemFileHandle} id + * @return {FileHandle} */ - set(id: number, binding: Binding): void; + static from(id: string | number | FileHandle | object | FileSystemFileHandle): FileHandle; /** - * Delete a binding by `id` - * @param {number} id - * @return {boolean} + * Determines if access to `path` for `mode` is possible. + * @param {string} path + * @param {number} [mode = 0o666] + * @param {object=} [options] + * @return {Promise} */ - delete(id: number): boolean; + static access(path: string, mode?: number, options?: object | undefined): Promise; /** - * Returns `true` if a binding exists in the mapping, otherwise `false`. - * @return {boolean} + * Asynchronously open a file. + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesopenpath-flags-mode} + * @param {string | Buffer | URL} path + * @param {string=} [flags = 'r'] + * @param {string|number=} [mode = 0o666] + * @param {object=} [options] + * @return {Promise} */ - has(id: any): boolean; + static open(path: string | Buffer | URL, flags?: string | undefined, mode?: (string | number) | undefined, options?: object | undefined): Promise; /** - * Known `Binding` values in the mapping. - * @return {{ next: function(): { value: Binding|undefined, done: boolean } }} + * `FileHandle` class constructor + * @ignore + * @param {object} options */ - values(): { - next: () => { - value: Binding | undefined; - done: boolean; - }; - }; + constructor(options: object); + flags: any; + path: any; + mode: any; + id: string; + fd: any; /** - * Known `Binding` keys in the mapping. - * @return {{ next: function(): { value: number|undefined, done: boolean } }} + * `true` if the `FileHandle` instance has been opened. + * @type {boolean} */ - keys(): { - next: () => { - value: number | undefined; - done: boolean; - }; - }; + get opened(): boolean; /** - * Known `Binding` ids in the mapping. - * @return {{ next: function(): { value: number|undefined, done: boolean } }} + * `true` if the `FileHandle` is opening. + * @type {boolean} */ - ids(): { - next: () => { - value: number | undefined; - done: boolean; - }; - }; + get opening(): boolean; /** - * Known `Binding` ids and values in the mapping. - * @return {{ next: function(): { value: [number, Binding]|undefined, done: boolean } }} + * `true` if the `FileHandle` is closing. + * @type {boolean} */ - entries(): { - next: () => { - value: [number, Binding] | undefined; - done: boolean; - }; - }; + get closing(): boolean; /** - * Bind a global hotkey expression. - * @param {string} expression - * @return {Promise} + * `true` if the `FileHandle` is closed. */ - bind(expression: string): Promise; + get closed(): boolean; /** - * Bind a global hotkey expression. - * @param {string} expression - * @return {Promise} + * Appends to a file, if handle was opened with `O_APPEND`, otherwise this + * method is just an alias to `FileHandle#writeFile()`. + * @param {string|Buffer|TypedArray|Array} data + * @param {object=} [options] + * @param {string=} [options.encoding = 'utf8'] + * @param {object=} [options.signal] */ - unbind(expression: string): Promise; + appendFile(data: string | Buffer | TypedArray | any[], options?: object | undefined): Promise; /** - * Returns an array of all active bindings for the application. - * @return {Promise} + * Change permissions of file handle. + * @param {number} mode + * @param {object=} [options] */ - active(): Promise; + chmod(mode: number, options?: object | undefined): Promise; /** - * Resets all active bindings in the application. - * @param {boolean=} [currentContextOnly] - * @return {Promise} + * Change ownership of file handle. + * @param {number} uid + * @param {number} gid + * @param {object=} [options] */ - reset(currentContextOnly?: boolean | undefined): Promise; + chown(uid: number, gid: number, options?: object | undefined): Promise; /** - * Implements the `Iterator` protocol for each currently registered - * active binding in this window context. The `AsyncIterator` protocol - * will probe for all gloally active bindings. - * @return {Iterator} + * Close underlying file handle + * @param {object=} [options] */ - [Symbol.iterator](): Iterator; + close(options?: object | undefined): Promise; /** - * Implements the `AsyncIterator` protocol for each globally active - * binding registered to the application. This differs from the `Iterator` - * protocol as this will probe for _all_ active bindings in the entire - * application context. - * @return {AsyncGenerator} + * Creates a `ReadStream` for the underlying file. + * @param {object=} [options] - An options object */ - [Symbol.asyncIterator](): AsyncGenerator; - #private; - } - /** - * An `EventTarget` container for a hotkey binding. - */ - export class Binding extends EventTarget { + createReadStream(options?: object | undefined): ReadStream; /** - * `Binding` class constructor. - * @ignore - * @param {object} data + * Creates a `WriteStream` for the underlying file. + * @param {object=} [options] - An options object */ - constructor(data: object); + createWriteStream(options?: object | undefined): WriteStream; /** - * `true` if the binding is valid, otherwise `false`. - * @type {boolean} + * @param {object=} [options] */ - get isValid(): boolean; + datasync(): Promise; /** - * `true` if the binding is considered active, otherwise `false`. - * @type {boolean} + * Opens the underlying descriptor for the file handle. + * @param {object=} [options] */ - get isActive(): boolean; + open(options?: object | undefined): Promise; /** - * The global unique ID for this binding. - * @type {number?} + * Reads `length` bytes starting from `position` into `buffer` at + * `offset`. + * @param {Buffer|object} buffer + * @param {number=} [offset] + * @param {number=} [length] + * @param {number=} [position] + * @param {object=} [options] */ - get id(): number; + read(buffer: Buffer | object, offset?: number | undefined, length?: number | undefined, position?: number | undefined, options?: object | undefined): Promise<{ + bytesRead: number; + buffer: any; + }>; /** - * The computed hash for this binding expression. - * @type {number?} + * Reads the entire contents of a file and returns it as a buffer or a string + * specified of a given encoding specified at `options.encoding`. + * @param {object=} [options] + * @param {string=} [options.encoding = 'utf8'] + * @param {object=} [options.signal] */ - get hash(): number; + readFile(options?: object | undefined): Promise; /** - * The normalized expression as a sequence of tokens. - * @type {string[]} + * Returns the stats of the underlying file. + * @param {object=} [options] + * @return {Promise} */ - get sequence(): string[]; + stat(options?: object | undefined): Promise; /** - * The original expression of the binding. - * @type {string?} + * Returns the stats of the underlying symbolic link. + * @param {object=} [options] + * @return {Promise} */ - get expression(): string; + lstat(options?: object | undefined): Promise; /** - * Setter for the level 1 'hotkey'` event listener. - * @ignore - * @type {function(HotKeyEvent)?} + * Synchronize a file's in-core state with storage device + * @return {Promise} */ - set onhotkey(onhotkey: (arg0: hotkeyEvent) => any); + sync(): Promise; /** - * Level 1 'hotkey'` event listener. - * @type {function(hotkeyEvent)?} + * @param {number} [offset = 0] + * @return {Promise} */ - get onhotkey(): (arg0: hotkeyEvent) => any; - /** - * Binds this hotkey expression. - * @return {Promise} - */ - bind(): Promise; + truncate(offset?: number): Promise; /** - * Unbinds this hotkey expression. - * @return {Promise} + * Writes `length` bytes at `offset` in `buffer` to the underlying file + * at `position`. + * @param {Buffer|object} buffer + * @param {number} offset + * @param {number} length + * @param {number} position + * @param {object=} [options] */ - unbind(): Promise; + write(buffer: Buffer | object, offset: number, length: number, position: number, options?: object | undefined): Promise; /** - * Implements the `AsyncIterator` protocol for async 'hotkey' events - * on this binding instance. - * @return {AsyncGenerator} + * Writes `data` to file. + * @param {string|Buffer|TypedArray|Array} data + * @param {object=} [options] + * @param {string=} [options.encoding = 'utf8'] + * @param {object=} [options.signal] */ - [Symbol.asyncIterator](): AsyncGenerator; + writeFile(data: string | Buffer | TypedArray | any[], options?: object | undefined): Promise; + [exports.kOpening]: any; + [exports.kClosing]: any; + [exports.kClosed]: boolean; #private; } /** - * A container for all the bindings currently bound - * by this window context. - * @type {Bindings} - */ - export const bindings: Bindings; - export default bindings; -} -declare module "socket:window" { - /** - * @param {string} url - * @return {string} - * @ignore - */ - export function formatURL(url: string): string; - /** - * @class ApplicationWindow - * Represents a window in the application + * A container for a directory handle tracked in `fds` and opened in the + * native layer. */ - export class ApplicationWindow { - static constants: typeof statuses; - static hotkey: import("socket:window/hotkey").Bindings; - constructor({ index, ...options }: { - [x: string]: any; - index: any; - }); + export class DirectoryHandle extends EventEmitter { /** - * Get the index of the window - * @return {number} - the index of the window + * The max number of entries that can be bufferd with the `bufferSize` + * option. */ - get index(): number; + static get MAX_BUFFER_SIZE(): number; + static get MAX_ENTRIES(): number; /** - * @type {import('./window/hotkey.js').default} + * The default number of entries `Dirent` that are buffered + * for each read request. */ - get hotkey(): import("socket:window/hotkey").Bindings; + static get DEFAULT_BUFFER_SIZE(): number; /** - * Get the size of the window - * @return {{ width: number, height: number }} - the size of the window + * Creates a `DirectoryHandle` from a given `id` or `fd` + * @param {string|number|DirectoryHandle|object|FileSystemDirectoryHandle} id + * @param {object} options + * @return {DirectoryHandle} */ - getSize(): { - width: number; - height: number; - }; + static from(id: string | number | DirectoryHandle | object | FileSystemDirectoryHandle, options: object): DirectoryHandle; /** - * Get the title of the window - * @return {string} - the title of the window + * Asynchronously open a directory. + * @param {string | Buffer | URL} path + * @param {object=} [options] + * @return {Promise} */ - getTitle(): string; + static open(path: string | Buffer | URL, options?: object | undefined): Promise; /** - * Get the status of the window - * @return {string} - the status of the window + * `DirectoryHandle` class constructor + * @private + * @param {object} options */ - getStatus(): string; + private constructor(); + id: string; + path: any; + bufferSize: number; /** - * Close the window - * @return {Promise} - the options of the window + * DirectoryHandle file descriptor id */ - close(): Promise; + get fd(): string; /** - * Shows the window - * @return {Promise} + * `true` if the `DirectoryHandle` instance has been opened. + * @type {boolean} */ - show(): Promise; + get opened(): boolean; /** - * Hides the window - * @return {Promise} + * `true` if the `DirectoryHandle` is opening. + * @type {boolean} */ - hide(): Promise; + get opening(): boolean; /** - * Maximize the window - * @return {Promise} + * `true` if the `DirectoryHandle` is closing. + * @type {boolean} */ - maximize(): Promise; + get closing(): boolean; /** - * Minimize the window - * @return {Promise} + * `true` if `DirectoryHandle` is closed. */ - minimize(): Promise; + get closed(): boolean; /** - * Restore the window - * @return {Promise} + * Opens the underlying handle for a directory. + * @param {object=} options + * @return {Promise} */ - restore(): Promise; + open(options?: object | undefined): Promise; /** - * Sets the title of the window - * @param {string} title - the title of the window - * @return {Promise} + * Close underlying directory handle + * @param {object=} [options] */ - setTitle(title: string): Promise; + close(options?: object | undefined): Promise; /** - * Sets the size of the window - * @param {object} opts - an options object - * @param {(number|string)=} opts.width - the width of the window - * @param {(number|string)=} opts.height - the height of the window - * @return {Promise} - * @throws {Error} - if the width or height is invalid + * Reads directory entries + * @param {object=} [options] + * @param {number=} [options.entries = DirectoryHandle.MAX_ENTRIES] */ - setSize(opts: { - width?: (number | string) | undefined; - height?: (number | string) | undefined; - }): Promise; + read(options?: object | undefined): Promise; + [exports.kOpening]: any; + [exports.kClosing]: any; + [exports.kClosed]: boolean; + #private; + } + export default exports; + export type TypedArray = Uint8Array | Int8Array; + import { EventEmitter } from "socket:events"; + import { Buffer } from "socket:buffer"; + import { ReadStream } from "socket:fs/stream"; + import { WriteStream } from "socket:fs/stream"; + import { Stats } from "socket:fs/stats"; + import * as exports from "socket:fs/handle"; + +} + +declare module "socket:fs/dir" { + /** + * Sorts directory entries + * @param {string|Dirent} a + * @param {string|Dirent} b + * @return {number} + */ + export function sortDirectoryEntries(a: string | Dirent, b: string | Dirent): number; + export const kType: unique symbol; + /** + * A containerr for a directory and its entries. This class supports scanning + * a directory entry by entry with a `read()` method. The `Symbol.asyncIterator` + * interface is exposed along with an AsyncGenerator `entries()` method. + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdir} + */ + export class Dir { + static from(fdOrHandle: any, options: any): exports.Dir; /** - * Navigate the window to a given path - * @param {object} path - file path - * @return {Promise} + * `Dir` class constructor. + * @param {DirectoryHandle} handle + * @param {object=} options */ - navigate(path: object): Promise; + constructor(handle: DirectoryHandle, options?: object | undefined); + path: any; + handle: DirectoryHandle; + encoding: any; + withFileTypes: boolean; /** - * Opens the Web Inspector for the window - * @return {Promise} + * `true` if closed, otherwise `false`. + * @ignore + * @type {boolean} */ - showInspector(): Promise; + get closed(): boolean; /** - * Sets the background color of the window - * @param {object} opts - an options object - * @param {number} opts.red - the red value - * @param {number} opts.green - the green value - * @param {number} opts.blue - the blue value - * @param {number} opts.alpha - the alpha value - * @return {Promise} + * `true` if closing, otherwise `false`. + * @ignore + * @type {boolean} */ - setBackgroundColor(opts: { - red: number; - green: number; - blue: number; - alpha: number; - }): Promise; + get closing(): boolean; /** - * Opens a native context menu. - * @param {object} options - an options object - * @return {Promise} + * Closes container and underlying handle. + * @param {object|function} options + * @param {function=} callback */ - setContextMenu(options: object): Promise; + close(options?: object | Function, callback?: Function | undefined): Promise; /** - * Shows a native open file dialog. - * @param {object} options - an options object - * @return {Promise} - an array of file paths + * Closes container and underlying handle + * synchronously. + * @param {object=} [options] */ - showOpenFilePicker(options: object): Promise; + closeSync(options?: object | undefined): void; /** - * Shows a native save file dialog. - * @param {object} options - an options object - * @return {Promise} - an array of file paths + * Reads and returns directory entry. + * @param {object|function} options + * @param {function=} callback + * @return {Promise} */ - showSaveFilePicker(options: object): Promise; + read(options: object | Function, callback?: Function | undefined): Promise; /** - * Shows a native directory dialog. - * @param {object} options - an options object - * @return {Promise} - an array of file paths + * Reads and returns directory entry synchronously. + * @param {object|function} options + * @return {Dirent[]|string[]} */ - showDirectoryFilePicker(options: object): Promise; + readSync(options?: object | Function): Dirent[] | string[]; /** - * This is a high-level API that you should use instead of `ipc.send` when - * you want to send a message to another window or to the backend. - * - * @param {object} options - an options object - * @param {number=} options.window - the window to send the message to - * @param {boolean=} [options.backend = false] - whether to send the message to the backend - * @param {string} options.event - the event to send - * @param {(string|object)=} options.value - the value to send - * @returns + * AsyncGenerator which yields directory entries. + * @param {object=} options */ - send(options: { - window?: number | undefined; - backend?: boolean | undefined; - event: string; - value?: (string | object) | undefined; - }): Promise; + entries(options?: object | undefined): AsyncGenerator; /** - * Post a message to a window - * TODO(@jwerle): research using `BroadcastChannel` instead - * @param {object} message - * @return {Promise} + * `for await (...)` AsyncGenerator support. */ - postMessage(message: object): Promise; + get [Symbol.asyncIterator](): (options?: object | undefined) => AsyncGenerator; + } + /** + * A container for a directory entry. + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdirent} + */ + export class Dirent { + static get UNKNOWN(): any; + static get FILE(): any; + static get DIR(): any; + static get LINK(): any; + static get FIFO(): any; + static get SOCKET(): any; + static get CHAR(): any; + static get BLOCK(): any; /** - * Opens an URL in the default browser. - * @param {object} options - * @returns {Promise} + * Creates `Dirent` instance from input. + * @param {object|string} name + * @param {(string|number)=} type */ - openExternal(options: object): Promise; + static from(name: object | string, type?: (string | number) | undefined): exports.Dirent; /** - * Adds a listener to the window. - * @param {string} event - the event to listen to - * @param {function(*): void} cb - the callback to call - * @returns {void} + * `Dirent` class constructor. + * @param {string} name + * @param {string|number} type */ - addListener(event: string, cb: (arg0: any) => void): void; + constructor(name: string, type: string | number); + name: string; /** - * Adds a listener to the window. An alias for `addListener`. - * @param {string} event - the event to listen to - * @param {function(*): void} cb - the callback to call - * @returns {void} - * @see addListener + * Read only type. */ - on(event: string, cb: (arg0: any) => void): void; + get type(): number; /** - * Adds a listener to the window. The listener is removed after the first call. - * @param {string} event - the event to listen to - * @param {function(*): void} cb - the callback to call - * @returns {void} + * `true` if `Dirent` instance is a directory. */ - once(event: string, cb: (arg0: any) => void): void; + isDirectory(): boolean; /** - * Removes a listener from the window. - * @param {string} event - the event to remove the listener from - * @param {function(*): void} cb - the callback to remove - * @returns {void} + * `true` if `Dirent` instance is a file. */ - removeListener(event: string, cb: (arg0: any) => void): void; + isFile(): boolean; /** - * Removes all listeners from the window. - * @param {string} event - the event to remove the listeners from - * @returns {void} + * `true` if `Dirent` instance is a block device. */ - removeAllListeners(event: string): void; + isBlockDevice(): boolean; /** - * Removes a listener from the window. An alias for `removeListener`. - * @param {string} event - the event to remove the listener from - * @param {function(*): void} cb - the callback to remove - * @returns {void} - * @see removeListener + * `true` if `Dirent` instance is a character device. */ - off(event: string, cb: (arg0: any) => void): void; - #private; + isCharacterDevice(): boolean; + /** + * `true` if `Dirent` instance is a symbolic link. + */ + isSymbolicLink(): boolean; + /** + * `true` if `Dirent` instance is a FIFO. + */ + isFIFO(): boolean; + /** + * `true` if `Dirent` instance is a socket. + */ + isSocket(): boolean; + [exports.kType]: number; } - export { hotkey }; - export default ApplicationWindow; + export default exports; + import { DirectoryHandle } from "socket:fs/handle"; + import * as exports from "socket:fs/dir"; + +} + +declare module "socket:hooks" { /** - * @ignore + * Wait for a hook event to occur. + * @template {Event | T extends Event} + * @param {string|function} nameOrFunction + * @return {Promise} */ - export const constants: typeof statuses; - import ipc from "socket:ipc"; - import * as statuses from "socket:window/constants"; - import hotkey from "socket:window/hotkey"; -} -declare module "socket:application" { + export function wait(nameOrFunction: string | Function): Promise; /** - * Returns the current window index - * @return {number} + * Wait for the global Window, Document, and Runtime to be ready. + * The callback function is called exactly once. + * @param {function} callback + * @return {function} */ - export function getCurrentWindowIndex(): number; + export function onReady(callback: Function): Function; /** - * Creates a new window and returns an instance of ApplicationWindow. - * @param {object} opts - an options object - * @param {number} opts.index - the index of the window - * @param {string} opts.path - the path to the HTML file to load into the window - * @param {string=} opts.title - the title of the window - * @param {(number|string)=} opts.width - the width of the window. If undefined, the window will have the main window width. - * @param {(number|string)=} opts.height - the height of the window. If undefined, the window will have the main window height. - * @param {(number|string)=} [opts.minWidth = 0] - the minimum width of the window - * @param {(number|string)=} [opts.minHeight = 0] - the minimum height of the window - * @param {(number|string)=} [opts.maxWidth = '100%'] - the maximum width of the window - * @param {(number|string)=} [opts.maxHeight = '100%'] - the maximum height of the window - * @param {boolean=} [opts.resizable=true] - whether the window is resizable - * @param {boolean=} [opts.frameless=false] - whether the window is frameless - * @param {boolean=} [opts.utility=false] - whether the window is utility (macOS only) - * @param {boolean=} [opts.canExit=false] - whether the window can exit the app - * @param {boolean=} [opts.headless=false] - whether the window will be headless or not (no frame) - * @return {Promise} + * Wait for the global Window and Document to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} */ - export function createWindow(opts: { - index: number; - path: string; - title?: string | undefined; - width?: (number | string) | undefined; - height?: (number | string) | undefined; - minWidth?: (number | string) | undefined; - minHeight?: (number | string) | undefined; - maxWidth?: (number | string) | undefined; - maxHeight?: (number | string) | undefined; - resizable?: boolean | undefined; - frameless?: boolean | undefined; - utility?: boolean | undefined; - canExit?: boolean | undefined; - headless?: boolean | undefined; - }): Promise; + export function onLoad(callback: Function): Function; /** - * Returns the current screen size. - * @returns {Promise<{ width: number, height: number }>} + * Wait for the runtime to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} */ - export function getScreenSize(): Promise<{ - width: number; - height: number; - }>; + export function onInit(callback: Function): Function; /** - * Returns the ApplicationWindow instances for the given indices or all windows if no indices are provided. - * @param {number[]} [indices] - the indices of the windows - * @return {Promise>} - * @throws {Error} - if indices is not an array of integer numbers + * Calls callback when a global exception occurs. + * 'error', 'messageerror', and 'unhandledrejection' events are handled here. + * @param {function} callback + * @return {function} */ - export function getWindows(indices?: number[]): Promise<{ - [x: number]: ApplicationWindow; - }>; + export function onError(callback: Function): Function; /** - * Returns the ApplicationWindow instance for the given index - * @param {number} index - the index of the window - * @throws {Error} - if index is not a valid integer number - * @returns {Promise} - the ApplicationWindow instance or null if the window does not exist + * Subscribes to the global data pipe calling callback when + * new data is emitted on the global Window. + * @param {function} callback + * @return {function} */ - export function getWindow(index: number): Promise; + export function onData(callback: Function): Function; /** - * Returns the ApplicationWindow instance for the current window. - * @return {Promise} + * Subscribes to global messages likely from an external `postMessage` + * invocation. + * @param {function} callback + * @return {function} */ - export function getCurrentWindow(): Promise; + export function onMessage(callback: Function): Function; /** - * Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. - * @param {number} [code = 0] - an exit code - * @return {Promise} + * Calls callback when runtime is working online. + * @param {function} callback + * @return {function} */ - export function exit(code?: number): Promise; + export function onOnline(callback: Function): Function; /** - * Set the native menu for the app. - * - * @param {object} options - an options object - * @param {string} options.value - the menu layout - * @param {number} options.index - the window to target (if applicable) - * @return {Promise} - * - * Socket Runtime provides a minimalist DSL that makes it easy to create - * cross platform native system and context menus. - * - * Menus are created at run time. They can be created from either the Main or - * Render process. The can be recreated instantly by calling the `setSystemMenu` method. - * - * The method takes a string. Here's an example of a menu. The semi colon is - * significant indicates the end of the menu. Use an underscore when there is no - * accelerator key. Modifiers are optional. And well known OS menu options like - * the edit menu will automatically get accelerators you dont need to specify them. - * - * - * ```js - * socket.application.setSystemMenu({ index: 0, value: ` - * App: - * Foo: f; - * - * Edit: - * Cut: x - * Copy: c - * Paste: v - * Delete: _ - * Select All: a; - * - * Other: - * Apple: _ - * Another Test: T - * !Im Disabled: I - * Some Thing: S + Meta - * --- - * Bazz: s + Meta, Control, Alt; - * `) - * ``` - * - * Separators - * - * To create a separator, use three dashes `---`. - * - * - * Accelerator Modifiers - * - * Accelerator modifiers are used as visual indicators but don't have a - * material impact as the actual key binding is done in the event listener. - * - * A capital letter implies that the accelerator is modified by the `Shift` key. - * - * Additional accelerators are `Meta`, `Control`, `Option`, each separated - * by commas. If one is not applicable for a platform, it will just be ignored. - * - * On MacOS `Meta` is the same as `Command`. - * - * - * Disabled Items - * - * If you want to disable a menu item just prefix the item with the `!` character. - * This will cause the item to appear disabled when the system menu renders. - * - * - * Submenus - * - * We feel like nested menus are an anti-pattern. We don't use them. If you have a - * strong argument for them and a very simple pull request that makes them work we - * may consider them. - * - * - * Event Handling - * - * When a menu item is activated, it raises the `menuItemSelected` event in - * the front end code, you can then communicate with your backend code if you - * want from there. - * - * For example, if the `Apple` item is selected from the `Other` menu... - * - * ```js - * window.addEventListener('menuItemSelected', event => { - * assert(event.detail.parent === 'Other') - * assert(event.detail.title === 'Apple') - * }) - * ``` - * + * Calls callback when runtime is not working online. + * @param {function} callback + * @return {function} */ - export function setSystemMenu(o: any): Promise; + export function onOffline(callback: Function): Function; /** - * An alias to setSystemMenu for creating a tary menu + * Calls callback when runtime user preferred language has changed. + * @param {function} callback + * @return {function} */ - export function setTrayMenu(o: any): Promise; + export function onLanguageChange(callback: Function): Function; /** - * Set the enabled state of the system menu. - * @param {object} value - an options object - * @return {Promise} + * Calls callback when an application permission has changed. + * @param {function} callback + * @return {function} */ - export function setSystemMenuItemEnabled(value: object): Promise; - export { menu }; + export function onPermissionChange(callback: Function): Function; /** - * Socket Runtime version. - * @type {object} - an object containing the version information + * Calls callback in response to a presented `Notification`. + * @param {function} callback + * @return {function} */ - export const runtimeVersion: object; + export function onNotificationResponse(callback: Function): Function; /** - * Runtime debug flag. - * @type {boolean} + * Calls callback when a `Notification` is presented. + * @param {function} callback + * @return {function} */ - export const debug: boolean; + export function onNotificationPresented(callback: Function): Function; /** - * Application configuration. - * @type {object} + * Calls callback when a `ApplicationURL` is opened. + * @param {function} callback + * @return {function} */ - export const config: object; - export namespace backend { + export function onApplicationURL(callback: Function): Function; + /** + * Calls callback when a `ApplicationPause` is dispatched. + * @param {function} callback + * @return {function} + */ + export function onApplicationPause(callback: Function): Function; + /** + * Calls callback when a `ApplicationResume` is dispatched. + * @param {function} callback + * @return {function} + */ + export function onApplicationResume(callback: Function): Function; + export const RUNTIME_INIT_EVENT_NAME: "__runtime_init__"; + export const GLOBAL_EVENTS: string[]; + /** + * An event dispatched when the runtime has been initialized. + */ + export class InitEvent { + constructor(); + } + /** + * An event dispatched when the runtime global has been loaded. + */ + export class LoadEvent { + constructor(); + } + /** + * An event dispatched when the runtime is considered ready. + */ + export class ReadyEvent { + constructor(); + } + /** + * An event dispatched when the runtime has been initialized. + */ + export class RuntimeInitEvent { + constructor(); + } + /** + * An interface for registering callbacks for various hooks in + * the runtime. + */ + export class Hooks extends EventTarget { /** - * @param {object} opts - an options object - * @param {boolean} [opts.force = false] - whether to force the existing process to close - * @return {Promise} + * @ignore */ - function open(opts?: { - force?: boolean; - }): Promise; + static GLOBAL_EVENTS: string[]; /** - * @return {Promise} + * @ignore */ - function close(): Promise; - } - export default exports; - import ApplicationWindow from "socket:window"; - import ipc from "socket:ipc"; - import menu from "socket:application/menu"; - import * as exports from "socket:application"; - -} -declare module "socket:bluetooth" { - export default exports; - /** - * Create an instance of a Bluetooth service. - */ - export class Bluetooth extends EventEmitter { - static isInitalized: boolean; + static InitEvent: typeof InitEvent; /** - * constructor is an example property that is set to `true` - * Creates a new service with key-value pairs - * @param {string} serviceId - Given a default value to determine the type + * @ignore */ - constructor(serviceId?: string); - serviceId: string; + static LoadEvent: typeof LoadEvent; /** - * Start the Bluetooth service. - * @return {Promise} - * + * @ignore */ - start(): Promise; + static ReadyEvent: typeof ReadyEvent; /** - * Start scanning for published values that correspond to a well-known UUID. - * Once subscribed to a UUID, events that correspond to that UUID will be - * emitted. To receive these events you can add an event listener, for example... - * - * ```js - * const ble = new Bluetooth(id) - * ble.subscribe(uuid) - * ble.on(uuid, (data, details) => { - * // ...do something interesting - * }) - * ``` - * - * @param {string} [id = ''] - A well-known UUID - * @return {Promise} + * @ignore */ - subscribe(id?: string): Promise; + static RuntimeInitEvent: typeof RuntimeInitEvent; /** - * Start advertising a new value for a well-known UUID - * @param {string} [id=''] - A well-known UUID - * @param {string} [value=''] - * @return {Promise} + * An array of all global events listened to in various hooks */ - publish(id?: string, value?: string): Promise; - } - import * as exports from "socket:bluetooth"; - import { EventEmitter } from "socket:events"; - import ipc from "socket:ipc"; - -} -declare module "socket:bootstrap" { - /** - * @param {string} dest - file path - * @param {string} hash - hash string - * @param {string} hashAlgorithm - hash algorithm - * @returns {Promise} - */ - export function checkHash(dest: string, hash: string, hashAlgorithm: string): Promise; - export function bootstrap(options: any): Bootstrap; - namespace _default { - export { bootstrap }; - export { checkHash }; - } - export default _default; - class Bootstrap extends EventEmitter { - constructor(options: any); - options: any; - run(): Promise; + get globalEvents(): string[]; /** - * @param {object} options - * @param {Uint8Array} options.fileBuffer - * @param {string} options.dest - * @returns {Promise} + * Reference to global object + * @type {object} */ - write({ fileBuffer, dest }: { - fileBuffer: Uint8Array; - dest: string; - }): Promise; + get global(): any; /** - * @param {string} url - url to download - * @returns {Promise} - * @throws {Error} - if status code is not 200 + * Returns `document` in global. + * @type {Document} */ - download(url: string): Promise; - cleanup(): void; - } - import { EventEmitter } from "socket:events"; -} -declare module "socket:ip" { - /** - * Normalizes input as an IPv4 address string - * @param {string|object|string[]|Uint8Array} input - * @return {string} - */ - export function normalizeIPv4(input: string | object | string[] | Uint8Array): string; - /** - * Determines if an input `string` is in IP address version 4 format. - * @param {string|object|string[]|Uint8Array} input - * @return {boolean} - */ - export function isIPv4(input: string | object | string[] | Uint8Array): boolean; - namespace _default { - export { normalizeIPv4 }; - export { isIPv4 }; - } - export default _default; -} -declare module "socket:dns/promises" { - /** - * @async - * @see {@link https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options} - * @param {string} hostname - The host name to resolve. - * @param {Object=} opts - An options object. - * @param {(number|string)=} [opts.family=0] - The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. - * @returns {Promise} - */ - export function lookup(hostname: string, opts?: any | undefined): Promise; - export default exports; - import * as exports from "socket:dns/promises"; - -} -declare module "socket:dns/index" { - /** - * Resolves a host name (e.g. `example.org`) into the first found A (IPv4) or - * AAAA (IPv6) record. All option properties are optional. If options is an - * integer, then it must be 4 or 6 – if options is 0 or not provided, then IPv4 - * and IPv6 addresses are both returned if found. - * - * From the node.js website... - * - * > With the all option set to true, the arguments for callback change to (err, - * addresses), with addresses being an array of objects with the properties - * address and family. - * - * > On error, err is an Error object, where err.code is the error code. Keep in - * mind that err.code will be set to 'ENOTFOUND' not only when the host name does - * not exist but also when the lookup fails in other ways such as no available - * file descriptors. dns.lookup() does not necessarily have anything to do with - * the DNS protocol. The implementation uses an operating system facility that - * can associate names with addresses and vice versa. This implementation can - * have subtle but important consequences on the behavior of any Node.js program. - * Please take some time to consult the Implementation considerations section - * before using dns.lookup(). - * - * @see {@link https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback} - * @param {string} hostname - The host name to resolve. - * @param {(object|intenumberger)=} [options] - An options object or record family. - * @param {(number|string)=} [options.family=0] - The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. - * @param {function} cb - The function to call after the method is complete. - * @returns {void} - */ - export function lookup(hostname: string, options?: (object | intenumberger) | undefined, cb: Function): void; - export { promises }; - export default exports; - import * as promises from "socket:dns/promises"; - import * as exports from "socket:dns/index"; - -} -declare module "socket:dns" { - export * from "socket:dns/index"; - export default exports; - import * as exports from "socket:dns/index"; -} -declare module "socket:dgram" { - export function createSocket(options: string | any, callback?: Function | undefined): Socket; - /** - * New instances of dgram.Socket are created using dgram.createSocket(). - * The new keyword is not to be used to create dgram.Socket instances. - */ - export class Socket extends EventEmitter { - constructor(options: any, callback: any); - id: any; - knownIdWasGivenInSocketConstruction: boolean; - type: any; - signal: any; - state: { - recvBufferSize: any; - sendBufferSize: any; - bindState: number; - connectState: number; - reuseAddr: boolean; - ipv6Only: boolean; - }; + get document(): Document; /** - * Listen for datagram messages on a named port and optional address - * If the address is not specified, the operating system will attempt to - * listen on all addresses. Once the binding is complete, a 'listening' - * event is emitted and the optional callback function is called. - * - * If binding fails, an 'error' event is emitted. - * - * @param {number} port - The port to listen for messages on - * @param {string} address - The address to bind to (0.0.0.0) - * @param {function} callback - With no parameters. Called when binding is complete. - * @see {@link https://nodejs.org/api/dgram.html#socketbindport-address-callback} + * Returns `document` in global. + * @type {Window} */ - bind(arg1: any, arg2: any, arg3: any): this; - dataListener: ({ detail }: { - detail: any; - }) => any; + get window(): Window; /** - * Associates the dgram.Socket to a remote address and port. Every message sent - * by this handle is automatically sent to that destination. Also, the socket - * will only receive messages from that remote peer. Trying to call connect() - * on an already connected socket will result in an ERR_SOCKET_DGRAM_IS_CONNECTED - * exception. If the address is not provided, '0.0.0.0' (for udp4 sockets) or '::1' - * (for udp6 sockets) will be used by default. Once the connection is complete, - * a 'connect' event is emitted and the optional callback function is called. - * In case of failure, the callback is called or, failing this, an 'error' event - * is emitted. - * - * @param {number} port - Port the client should connect to. - * @param {string=} host - Host the client should connect to. - * @param {function=} connectListener - Common parameter of socket.connect() methods. Will be added as a listener for the 'connect' event once. - * @see {@link https://nodejs.org/api/dgram.html#socketconnectport-address-callback} + * Predicate for determining if the global document is ready. + * @type {boolean} */ - connect(arg1: any, arg2: any, arg3: any): void; + get isDocumentReady(): boolean; /** - * A synchronous function that disassociates a connected dgram.Socket from - * its remote address. Trying to call disconnect() on an unbound or already - * disconnected socket will result in an ERR_SOCKET_DGRAM_NOT_CONNECTED exception. - * - * @see {@link https://nodejs.org/api/dgram.html#socketdisconnect} + * Predicate for determining if the global object is ready. + * @type {boolean} */ - disconnect(): void; + get isGlobalReady(): boolean; /** - * Broadcasts a datagram on the socket. For connectionless sockets, the - * destination port and address must be specified. Connected sockets, on the - * other hand, will use their associated remote endpoint, so the port and - * address arguments must not be set. - * - * > The msg argument contains the message to be sent. Depending on its type, - * different behavior can apply. If msg is a Buffer, any TypedArray, or a - * DataView, the offset and length specify the offset within the Buffer where - * the message begins and the number of bytes in the message, respectively. - * If msg is a String, then it is automatically converted to a Buffer with - * 'utf8' encoding. With messages that contain multi-byte characters, offset, - * and length will be calculated with respect to byte length and not the - * character position. If msg is an array, offset and length must not be - * specified. - * - * > The address argument is a string. If the value of the address is a hostname, - * DNS will be used to resolve the address of the host. If the address is not - * provided or otherwise nullish, '0.0.0.0' (for udp4 sockets) or '::1' - * (for udp6 sockets) will be used by default. - * - * > If the socket has not been previously bound with a call to bind, the socket - * is assigned a random port number and is bound to the "all interfaces" - * address ('0.0.0.0' for udp4 sockets, '::1' for udp6 sockets.) - * - * > An optional callback function may be specified as a way of reporting DNS - * errors or for determining when it is safe to reuse the buf object. DNS - * lookups delay the time to send for at least one tick of the Node.js event - * loop. - * - * > The only way to know for sure that the datagram has been sent is by using a - * callback. If an error occurs and a callback is given, the error will be - * passed as the first argument to the callback. If a callback is not given, - * the error is emitted as an 'error' event on the socket object. - * - * > Offset and length are optional but both must be set if either is used. - * They are supported only when the first argument is a Buffer, a TypedArray, - * or a DataView. - * - * @param {Buffer | TypedArray | DataView | string | Array} msg - Message to be sent. - * @param {integer=} offset - Offset in the buffer where the message starts. - * @param {integer=} length - Number of bytes in the message. - * @param {integer=} port - Destination port. - * @param {string=} address - Destination host name or IP address. - * @param {Function=} callback - Called when the message has been sent. - * @see {@link https://nodejs.org/api/dgram.html#socketsendmsg-offset-length-port-address-callback} + * Predicate for determining if the runtime is ready. + * @type {boolean} */ - send(buffer: any, ...args: any[]): Promise; + get isRuntimeReady(): boolean; /** - * Close the underlying socket and stop listening for data on it. If a - * callback is provided, it is added as a listener for the 'close' event. - * - * @param {function=} callback - Called when the connection is completed or on error. - * - * @see {@link https://nodejs.org/api/dgram.html#socketclosecallback} + * Predicate for determining if everything is ready. + * @type {boolean} */ - close(cb: any): this; + get isReady(): boolean; /** - * - * Returns an object containing the address information for a socket. For - * UDP sockets, this object will contain address, family, and port properties. - * - * This method throws EBADF if called on an unbound socket. - * @returns {Object} socketInfo - Information about the local socket - * @returns {string} socketInfo.address - The IP address of the socket - * @returns {string} socketInfo.port - The port of the socket - * @returns {string} socketInfo.family - The IP family of the socket - * - * @see {@link https://nodejs.org/api/dgram.html#socketaddress} + * Predicate for determining if the runtime is working online. + * @type {boolean} */ - address(): any; + get isOnline(): boolean; /** - * Returns an object containing the address, family, and port of the remote - * endpoint. This method throws an ERR_SOCKET_DGRAM_NOT_CONNECTED exception - * if the socket is not connected. - * - * @returns {Object} socketInfo - Information about the remote socket - * @returns {string} socketInfo.address - The IP address of the socket - * @returns {string} socketInfo.port - The port of the socket - * @returns {string} socketInfo.family - The IP family of the socket - * @see {@link https://nodejs.org/api/dgram.html#socketremoteaddress} + * Predicate for determining if the runtime is in a Worker context. + * @type {boolean} */ - remoteAddress(): any; + get isWorkerContext(): boolean; /** - * Sets the SO_RCVBUF socket option. Sets the maximum socket receive buffer in - * bytes. - * - * @param {number} size - The size of the new receive buffer - * @see {@link https://nodejs.org/api/dgram.html#socketsetrecvbuffersizesize} + * Predicate for determining if the runtime is in a Window context. + * @type {boolean} */ - setRecvBufferSize(size: number): Promise; + get isWindowContext(): boolean; /** - * Sets the SO_SNDBUF socket option. Sets the maximum socket send buffer in - * bytes. - * - * @param {number} size - The size of the new send buffer - * @see {@link https://nodejs.org/api/dgram.html#socketsetsendbuffersizesize} + * Wait for a hook event to occur. + * @template {Event | T extends Event} + * @param {string|function} nameOrFunction + * @param {WaitOptions=} [options] + * @return {Promise} */ - setSendBufferSize(size: number): Promise; + wait(nameOrFunction: string | Function, options?: WaitOptions | undefined): Promise; /** - * @see {@link https://nodejs.org/api/dgram.html#socketgetrecvbuffersize} + * Wait for the global Window, Document, and Runtime to be ready. + * The callback function is called exactly once. + * @param {function} callback + * @return {function} */ - getRecvBufferSize(): any; + onReady(callback: Function): Function; /** - * @returns {number} the SO_SNDBUF socket send buffer size in bytes. - * @see {@link https://nodejs.org/api/dgram.html#socketgetsendbuffersize} + * Wait for the global Window and Document to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} */ - getSendBufferSize(): number; - setBroadcast(): void; - setTTL(): void; - setMulticastTTL(): void; - setMulticastLoopback(): void; - setMulticastMembership(): void; - setMulticastInterface(): void; - addMembership(): void; - dropMembership(): void; - addSourceSpecificMembership(): void; - dropSourceSpecificMembership(): void; - ref(): this; - unref(): this; - } - /** - * Generic error class for an error occurring on a `Socket` instance. - * @ignore - */ - export class SocketError extends InternalError { + onLoad(callback: Function): Function; /** - * @type {string} + * Wait for the runtime to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} */ - get code(): string; - } - /** - * Thrown when a socket is already bound. - */ - export class ERR_SOCKET_ALREADY_BOUND extends exports.SocketError { - get message(): string; - } - /** - * @ignore - */ - export class ERR_SOCKET_BAD_BUFFER_SIZE extends exports.SocketError { - } - /** - * @ignore - */ - export class ERR_SOCKET_BUFFER_SIZE extends exports.SocketError { - } - /** - * Thrown when the socket is already connected. - */ - export class ERR_SOCKET_DGRAM_IS_CONNECTED extends exports.SocketError { - get message(): string; - } - /** - * Thrown when the socket is not connected. - */ - export class ERR_SOCKET_DGRAM_NOT_CONNECTED extends exports.SocketError { - syscall: string; - get message(): string; - } - /** - * Thrown when the socket is not running (not bound or connected). - */ - export class ERR_SOCKET_DGRAM_NOT_RUNNING extends exports.SocketError { - get message(): string; - } - /** - * Thrown when a bad socket type is used in an argument. - */ - export class ERR_SOCKET_BAD_TYPE extends TypeError { - code: string; - get message(): string; - } - /** - * Thrown when a bad port is given. - */ - export class ERR_SOCKET_BAD_PORT extends RangeError { - code: string; - } - export default exports; - export type SocketOptions = any; - import { EventEmitter } from "socket:events"; - import { InternalError } from "socket:errors"; - import * as exports from "socket:dgram"; - -} -declare module "socket:enumeration" { - /** - * @module enumeration - * This module provides a data structure for enumerated unique values. - */ - /** - * A container for enumerated values. - */ - export class Enumeration extends Set { + onInit(callback: Function): Function; /** - * Creates an `Enumeration` instance from arguments. - * @param {...any} values - * @return {Enumeration} + * Calls callback when a global exception occurs. + * 'error', 'messageerror', and 'unhandledrejection' events are handled here. + * @param {function} callback + * @return {function} */ - static from(...values: any[]): Enumeration; + onError(callback: Function): Function; /** - * `Enumeration` class constructor. - * @param {any[]} values - * @param {object=} [options = {}] - * @param {number=} [options.start = 0] + * Subscribes to the global data pipe calling callback when + * new data is emitted on the global Window. + * @param {function} callback + * @return {function} */ - constructor(values: any[], options?: object | undefined); + onData(callback: Function): Function; /** - * @type {number} + * Subscribes to global messages likely from an external `postMessage` + * invocation. + * @param {function} callback + * @return {function} */ - get length(): number; + onMessage(callback: Function): Function; /** - * Returns `true` if enumeration contains `value`. An alias - * for `Set.prototype.has`. - * @return {boolean} + * Calls callback when runtime is working online. + * @param {function} callback + * @return {function} */ - contains(value: any): boolean; + onOnline(callback: Function): Function; /** - * @ignore + * Calls callback when runtime is not working online. + * @param {function} callback + * @return {function} */ - add(): void; + onOffline(callback: Function): Function; /** - * @ignore + * Calls callback when runtime user preferred language has changed. + * @param {function} callback + * @return {function} */ - delete(): void; + onLanguageChange(callback: Function): Function; /** - * JSON represenation of a `Enumeration` instance. - * @ignore - * @return {string[]} + * Calls callback when an application permission has changed. + * @param {function} callback + * @return {function} */ - toJSON(): string[]; + onPermissionChange(callback: Function): Function; /** - * Internal inspect function. - * @ignore - * @return {LanguageQueryResult} + * Calls callback in response to a displayed `Notification`. + * @param {function} callback + * @return {function} */ - inspect(): LanguageQueryResult; + onNotificationResponse(callback: Function): Function; + /** + * Calls callback when a `Notification` is presented. + * @param {function} callback + * @return {function} + */ + onNotificationPresented(callback: Function): Function; + /** + * Calls callback when a `ApplicationURL` is opened. + * @param {function} callback + * @return {function} + */ + onApplicationURL(callback: Function): Function; + /** + * Calls callback when an `ApplicationPause` is dispatched. + * @param {function} callback + * @return {function} + */ + onApplicationPause(callback: Function): Function; + /** + * Calls callback when an `ApplicationResume` is dispatched. + * @param {function} callback + * @return {function} + */ + onApplicationResume(callback: Function): Function; + #private; } - export default Enumeration; -} -declare module "socket:mime/index" { + export default hooks; + export type WaitOptions = { + signal?: AbortSignal; + }; /** - * Look up a MIME type in various MIME databases. - * @param {string} query - * @return {Promise} + * `Hooks` single instance. + * @ignore */ - export function lookup(query: string): Promise; + const hooks: Hooks; +} + +declare module "socket:fs/watcher" { /** - * A container for a database lookup query. + * A container for a file system path watcher. */ - export class DatabaseQueryResult { + export class Watcher extends EventEmitter { /** - * `DatabaseQueryResult` class constructor. + * `Watcher` class constructor. * @ignore - * @param {Database} database - * @param {string} name - * @param {string} mime + * @param {string} path + * @param {object=} [options] + * @param {AbortSignal=} [options.signal} + * @param {string|number|bigint=} [options.id] + * @param {string=} [options.encoding = 'utf8'] */ - constructor(database: Database, name: string, mime: string); + constructor(path: string, options?: object | undefined); /** + * The underlying `fs.Watcher` resource id. + * @ignore * @type {string} */ - name: string; + id: string; /** + * The path the `fs.Watcher` is watching * @type {string} */ - mime: string; - database: Database; - } - /** - * A container for MIME types by class (audio, video, text, etc) - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml} - */ - export class Database { + path: string; /** - * `Database` class constructor. - * @param {string} name + * `true` if closed, otherwise `false. + * @type {boolean} */ - constructor(name: string); + closed: boolean; /** - * The name of the MIME database. - * @type {string} + * `true` if aborted, otherwise `false`. + * @type {boolean} */ - name: string; + aborted: boolean; /** - * The URL of the MIME database. - * @type {URL} + * The encoding of the `filename` + * @type {'utf8'|'buffer'} */ - url: URL; + encoding: "utf8" | "buffer"; /** - * The mapping of MIME name to the MIME "content type" - * @type {Map} + * A `AbortController` `AbortSignal` for async aborts. + * @type {AbortSignal?} */ - map: Map; + signal: AbortSignal | null; /** - * An index of MIME "content type" to the MIME name. - * @type {Map} + * Internal event listener cancellation. + * @ignore + * @type {function?} */ - index: Map; + stopListening: Function | null; /** - * An enumeration of all database entries. - * @return {Array>} + * Internal starter for watcher. + * @ignore */ - entries(): Array>; + start(): Promise; /** - * Loads database MIME entries into internal map. + * Closes watcher and stops listening for changes. * @return {Promise} */ - load(): Promise; + close(): Promise; /** - * Lookup MIME type by name or content type - * @param {string} query - * @return {Promise} + * Implements the `AsyncIterator` (`Symbol.asyncIterator`) iterface. + * @ignore + * @return {AsyncIterator<{ eventType: string, filename: string }>} */ - lookup(query: string): Promise; + [Symbol.asyncIterator](): AsyncIterator<{ + eventType: string; + filename: string; + }>; + #private; } + export default Watcher; + import { EventEmitter } from "socket:events"; +} + +declare module "socket:fs/promises" { /** - * A database of MIME types for 'application/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#application} + * Asynchronously check access a file. + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesaccesspath-mode} + * @param {string | Buffer | URL} path + * @param {string?} [mode] + * @param {object?} [options] */ - export const application: Database; + export function access(path: string | Buffer | URL, mode?: string | null, options?: object | null): Promise; /** - * A database of MIME types for 'audio/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#audio} + * @see {@link https://nodejs.org/api/fs.html#fspromiseschmodpath-mode} + * @param {string | Buffer | URL} path + * @param {number} mode + * @returns {Promise} */ - export const audio: Database; + export function chmod(path: string | Buffer | URL, mode: number): Promise; /** - * A database of MIME types for 'font/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#font} + * Changes ownership of file or directory at `path` with `uid` and `gid`. + * @param {string} path + * @param {number} uid + * @param {number} gid + * @return {Promise} */ - export const font: Database; + export function chown(path: string, uid: number, gid: number): Promise; /** - * A database of MIME types for 'image/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#image} + * Asynchronously copies `src` to `dest` calling `callback` upon success or error. + * @param {string} src - The source file path. + * @param {string} dest - The destination file path. + * @param {number} flags - Modifiers for copy operation. + * @return {Promise} */ - export const image: Database; + export function copyFile(src: string, dest: string, flags?: number): Promise; /** - * A database of MIME types for 'model/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#model} + * Chages ownership of link at `path` with `uid` and `gid. + * @param {string} path + * @param {number} uid + * @param {number} gid + * @return {Promise} */ - export const model: Database; + export function lchown(path: string, uid: number, gid: number): Promise; /** - * A database of MIME types for 'multipart/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#multipart} + * Creates a link to `dest` from `dest`. + * @param {string} src + * @param {string} dest + * @return {Promise} */ - export const multipart: Database; + export function link(src: string, dest: string): Promise; /** - * A database of MIME types for 'text/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#text} + * Asynchronously creates a directory. + * + * @param {string} path - The path to create + * @param {object} [options] - The optional options argument can be an integer specifying mode (permission and sticky bits), or an object with a mode property and a recursive property indicating whether parent directories should be created. Calling fs.mkdir() when path is a directory that exists results in an error only when recursive is false. + * @param {boolean} [options.recursive=false] - Recursively create missing path segments. + * @param {number} [options.mode=0o777] - Set the mode of directory, or missing path segments when recursive is true. + * @return {Promise} - Upon success, fulfills with undefined if recursive is false, or the first directory path created if recursive is true. */ - export const text: Database; + export function mkdir(path: string, options?: { + recursive?: boolean; + mode?: number; + }): Promise; /** - * A database of MIME types for 'video/' content types - * @type {Database} - * @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#video} + * Asynchronously open a file. + * @see {@link https://nodejs.org/api/fs.html#fspromisesopenpath-flags-mode } + * + * @param {string | Buffer | URL} path + * @param {string=} flags - default: 'r' + * @param {number=} mode - default: 0o666 + * @return {Promise} */ - export const video: Database; + export function open(path: string | Buffer | URL, flags?: string | undefined, mode?: number | undefined): Promise; /** - * An array of known MIME databases. Custom databases can be added to this - * array in userspace for lookup with `mime.lookup()` - * @type {Database[]} + * @see {@link https://nodejs.org/api/fs.html#fspromisesopendirpath-options} + * @param {string | Buffer | URL} path + * @param {object?} [options] + * @param {string?} [options.encoding = 'utf8'] + * @param {number?} [options.bufferSize = 32] + * @return {Promise} */ - export const databases: Database[]; - namespace _default { - export { Database }; - export { databases }; - export { lookup }; - export { application }; - export { audio }; - export { font }; - export { image }; - export { model }; - export { multipart }; - export { text }; - export { video }; - } - export default _default; -} -declare module "socket:mime" { - export * from "socket:mime/index"; - export default exports; - import * as exports from "socket:mime/index"; -} -declare module "socket:fs/web" { + export function opendir(path: string | Buffer | URL, options?: object | null): Promise; /** - * Creates a new `File` instance from `filename`. - * @param {string} filename - * @param {{ fd: fs.FileHandle, highWaterMark?: number }=} [options] - * @return {File} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreaddirpath-options} + * @param {string | Buffer | URL} path + * @param {object?} options + * @param {string?} [options.encoding = 'utf8'] + * @param {boolean?} [options.withFileTypes = false] */ - export function createFile(filename: string, options?: { - fd: fs.FileHandle; - highWaterMark?: number; - }): File; + export function readdir(path: string | Buffer | URL, options: object | null): Promise<(string | Dirent)[]>; /** - * Creates a `FileSystemWritableFileStream` instance backed - * by `socket:fs:` module from a given `FileSystemFileHandle` instance. - * @param {string|File} file - * @return {Promise} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreadfilepath-options} + * @param {string} path + * @param {object?} [options] + * @param {(string|null)?} [options.encoding = null] + * @param {string?} [options.flag = 'r'] + * @param {AbortSignal?} [options.signal] + * @return {Promise} */ - export function createFileSystemWritableFileStream(handle: any, options: any): Promise; + export function readFile(path: string, options?: object | null): Promise; /** - * Creates a `FileSystemFileHandle` instance backed by `socket:fs:` module from - * a given `File` instance or filename string. - * @param {string|File} file - * @param {object} [options] - * @return {Promise} + * Reads link at `path` + * @param {string} path + * @return {Promise} */ - export function createFileSystemFileHandle(file: string | File, options?: object): Promise; + export function readlink(path: string): Promise; /** - * Creates a `FileSystemDirectoryHandle` instance backed by `socket:fs:` module - * from a given directory name string. - * @param {string} dirname - * @return {Promise} + * Computes real path for `path` + * @param {string} path + * @return {Promise} */ - export function createFileSystemDirectoryHandle(dirname: string, options?: any): Promise; - export const File: { - new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File; - prototype: File; - } | { - new (): { - readonly lastModifiedDate: Date; - readonly lastModified: number; - readonly name: any; - readonly size: number; - readonly type: string; - slice(): void; - arrayBuffer(): Promise; - text(): Promise; - stream(): void; - }; - }; - export const FileSystemHandle: { - new (): { - readonly name: any; - readonly kind: any; - }; - }; - export const FileSystemFileHandle: { - new (): FileSystemFileHandle; - prototype: FileSystemFileHandle; - } | { - new (): { - getFile(): void; - createWritable(options?: any): Promise; - createSyncAccessHandle(): Promise; - readonly name: any; - readonly kind: any; - }; - }; - export const FileSystemDirectoryHandle: { - new (): FileSystemDirectoryHandle; - prototype: FileSystemDirectoryHandle; - } | { - new (): { - entries(): AsyncGenerator; - values(): AsyncGenerator; - keys(): AsyncGenerator; - resolve(possibleDescendant: any): Promise; - removeEntry(name: any, options?: any): Promise; - getDirectoryHandle(name: any, options?: any): Promise; - getFileHandle(name: any, options?: any): Promise; - readonly name: any; - readonly kind: any; - }; - }; - export const FileSystemWritableFileStream: { - new (underlyingSink?: UnderlyingSink, strategy?: QueuingStrategy): { - seek(position: any): Promise; - truncate(size: any): Promise; - write(data: any): Promise; - readonly locked: boolean; - abort(reason?: any): Promise; - close(): Promise; - getWriter(): WritableStreamDefaultWriter; - }; - }; - namespace _default { - export { createFileSystemWritableFileStream }; - export { createFileSystemDirectoryHandle }; - export { createFileSystemFileHandle }; - export { createFile }; - } - export default _default; - import fs from "socket:fs/promises"; -} -declare module "socket:extension" { + export function realpath(path: string): Promise; /** - * Load an extension by name. - * @template {Record T} - * @param {string} name - * @param {ExtensionLoadOptions} [options] - * @return {Promise>} + * Renames file or directory at `src` to `dest`. + * @param {string} src + * @param {string} dest + * @return {Promise} */ - export function load>(name: string, options?: ExtensionLoadOptions): Promise>; + export function rename(src: string, dest: string): Promise; /** - * Provides current stats about the loaded extensions. - * @return {Promise} + * Removes directory at `path`. + * @param {string} path + * @return {Promise} */ - export function stats(): Promise; + export function rmdir(path: string): Promise; /** - * @typedef {{ - * allow: string[] | string, - * imports?: object, - * type?: 'shared' | 'wasm32', - * path?: string, - * stats?: object, - * instance?: WebAssembly.Instance, - * adapter?: WebAssemblyExtensionAdapter - * }} ExtensionLoadOptions + * Get the stats of a file + * @see {@link https://nodejs.org/api/fs.html#fspromisesstatpath-options} + * @param {string | Buffer | URL} path + * @param {object?} [options] + * @param {boolean?} [options.bigint = false] + * @return {Promise} */ + export function stat(path: string | Buffer | URL, options?: object | null): Promise; /** - * @typedef {{ abi: number, version: string, description: string }} ExtensionInfo + * Get the stats of a symbolic link. + * @see {@link https://nodejs.org/api/fs.html#fspromiseslstatpath-options} + * @param {string | Buffer | URL} path + * @param {object?} [options] + * @param {boolean?} [options.bigint = false] + * @return {Promise} */ + export function lstat(path: string | Buffer | URL, options?: object | null): Promise; /** - * @typedef {{ abi: number, loaded: number }} ExtensionStats + * Creates a symlink of `src` at `dest`. + * @param {string} src + * @param {string} dest + * @return {Promise} + */ + export function symlink(src: string, dest: string, type?: any): Promise; + /** + * Unlinks (removes) file at `path`. + * @param {string} path + * @return {Promise} + */ + export function unlink(path: string): Promise; + /** + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromiseswritefilefile-data-options} + * @param {string | Buffer | URL | FileHandle} path - filename or FileHandle + * @param {string|Buffer|Array|DataView|TypedArray} data + * @param {object?} [options] + * @param {string|null} [options.encoding = 'utf8'] + * @param {number} [options.mode = 0o666] + * @param {string} [options.flag = 'w'] + * @param {AbortSignal?} [options.signal] + * @return {Promise} + */ + export function writeFile(path: string | Buffer | URL | FileHandle, data: string | Buffer | any[] | DataView | TypedArray, options?: object | null): Promise; + /** + * Watch for changes at `path` calling `callback` + * @param {string} + * @param {function|object=} [options] + * @param {string=} [options.encoding = 'utf8'] + * @param {AbortSignal=} [options.signal] + * @return {Watcher} + */ + export function watch(path: any, options?: (Function | object) | undefined): Watcher; + export type Stats = any; + export default exports; + export type Buffer = import("socket:buffer").Buffer; + export type TypedArray = Uint8Array | Int8Array; + import { FileHandle } from "socket:fs/handle"; + import { Dir } from "socket:fs/dir"; + import { Dirent } from "socket:fs/dir"; + import { Stats } from "socket:fs/stats"; + import { Watcher } from "socket:fs/watcher"; + import bookmarks from "socket:fs/bookmarks"; + import * as constants from "socket:fs/constants"; + import { DirectoryHandle } from "socket:fs/handle"; + import fds from "socket:fs/fds"; + import { ReadStream } from "socket:fs/stream"; + import { WriteStream } from "socket:fs/stream"; + import * as exports from "socket:fs/promises"; + + export { bookmarks, constants, Dir, DirectoryHandle, Dirent, fds, FileHandle, ReadStream, Watcher, WriteStream }; +} + +declare module "socket:fs/index" { + /** + * Asynchronously check access a file for a given mode calling `callback` + * upon success or error. + * @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback} + * @param {string | Buffer | URL} path + * @param {string?|function(Error?)?} [mode = F_OK(0)] + * @param {function(Error?)?} [callback] + */ + export function access(path: string | Buffer | URL, mode: any, callback?: ((arg0: Error | null) => any) | null): void; + /** + * Synchronously check access a file for a given mode calling `callback` + * upon success or error. + * @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback} + * @param {string | Buffer | URL} path + * @param {string?} [mode = F_OK(0)] + */ + export function accessSync(path: string | Buffer | URL, mode?: string | null): boolean; + /** + * Checks if a path exists + * @param {string | Buffer | URL} path + * @param {function(Boolean)?} [callback] + */ + export function exists(path: string | Buffer | URL, callback?: ((arg0: boolean) => any) | null): void; + /** + * Checks if a path exists + * @param {string | Buffer | URL} path + * @param {function(Boolean)?} [callback] + */ + export function existsSync(path: string | Buffer | URL): boolean; + /** + * Asynchronously changes the permissions of a file. + * No arguments other than a possible exception are given to the completion callback + * + * @see {@link https://nodejs.org/api/fs.html#fschmodpath-mode-callback} + * + * @param {string | Buffer | URL} path + * @param {number} mode + * @param {function(Error?)} callback + */ + export function chmod(path: string | Buffer | URL, mode: number, callback: (arg0: Error | null) => any): TypeError; + /** + * Synchronously changes the permissions of a file. + * + * @see {@link https://nodejs.org/api/fs.html#fschmodpath-mode-callback} + * @param {string | Buffer | URL} path + * @param {number} mode + */ + export function chmodSync(path: string | Buffer | URL, mode: number): void; + /** + * Changes ownership of file or directory at `path` with `uid` and `gid`. + * @param {string} path + * @param {number} uid + * @param {number} gid + * @param {function} callback + */ + export function chown(path: string, uid: number, gid: number, callback: Function): TypeError; + /** + * Changes ownership of file or directory at `path` with `uid` and `gid`. + * @param {string} path + * @param {number} uid + * @param {number} gid + */ + export function chownSync(path: string, uid: number, gid: number): void; + /** + * Asynchronously close a file descriptor calling `callback` upon success or error. + * @see {@link https://nodejs.org/api/fs.html#fsclosefd-callback} + * @param {number} fd + * @param {function(Error?)?} [callback] + */ + export function close(fd: number, callback?: ((arg0: Error | null) => any) | null): void; + /** + * Synchronously close a file descriptor. + * @param {number} fd - fd + */ + export function closeSync(fd: number): void; + /** + * Asynchronously copies `src` to `dest` calling `callback` upon success or error. + * @param {string} src - The source file path. + * @param {string} dest - The destination file path. + * @param {number} flags - Modifiers for copy operation. + * @param {function(Error=)=} [callback] - The function to call after completion. + * @see {@link https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback} + */ + export function copyFile(src: string, dest: string, flags?: number, callback?: ((arg0: Error | undefined) => any) | undefined): void; + /** + * Synchronously copies `src` to `dest` calling `callback` upon success or error. + * @param {string} src - The source file path. + * @param {string} dest - The destination file path. + * @param {number} flags - Modifiers for copy operation. + * @see {@link https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback} + */ + export function copyFileSync(src: string, dest: string, flags?: number): void; + /** + * @see {@link https://nodejs.org/api/fs.html#fscreatewritestreampath-options} + * @param {string | Buffer | URL} path + * @param {object?} [options] + * @returns {ReadStream} + */ + export function createReadStream(path: string | Buffer | URL, options?: object | null): ReadStream; + /** + * @see {@link https://nodejs.org/api/fs.html#fscreatewritestreampath-options} + * @param {string | Buffer | URL} path + * @param {object?} [options] + * @returns {WriteStream} + */ + export function createWriteStream(path: string | Buffer | URL, options?: object | null): WriteStream; + /** + * Invokes the callback with the for the file descriptor. See + * the POSIX fstat(2) documentation for more detail. + * + * @see {@link https://nodejs.org/api/fs.html#fsfstatfd-options-callback} + * + * @param {number} fd - A file descriptor. + * @param {object?|function?} [options] - An options object. + * @param {function?} callback - The function to call after completion. + */ + export function fstat(fd: number, options: any, callback: Function | null): void; + /** + * Request that all data for the open file descriptor is flushed + * to the storage device. + * @param {number} fd - A file descriptor. + * @param {function} callback - The function to call after completion. + */ + export function fsync(fd: number, callback: Function): void; + /** + * Truncates the file up to `offset` bytes. + * @param {number} fd - A file descriptor. + * @param {number=|function} [offset = 0] + * @param {function?} callback - The function to call after completion. + */ + export function ftruncate(fd: number, offset: any, callback: Function | null): void; + /** + * Chages ownership of link at `path` with `uid` and `gid. + * @param {string} path + * @param {number} uid + * @param {number} gid + * @param {function} callback + */ + export function lchown(path: string, uid: number, gid: number, callback: Function): TypeError; + /** + * Creates a link to `dest` from `src`. + * @param {string} src + * @param {string} dest + * @param {function} + */ + export function link(src: string, dest: string, callback: any): void; + /** + * @ignore + */ + export function mkdir(path: any, options: any, callback: any): void; + /** + * @ignore + * @param {string|URL} path + * @param {object=} [options] + */ + export function mkdirSync(path: string | URL, options?: object | undefined): void; + /** + * Asynchronously open a file calling `callback` upon success or error. + * @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback} + * @param {string | Buffer | URL} path + * @param {string?} [flags = 'r'] + * @param {string?} [mode = 0o666] + * @param {object?|function?} [options] + * @param {function(Error?, number?)?} [callback] + */ + export function open(path: string | Buffer | URL, flags?: string | null, mode?: string | null, options?: any, callback?: ((arg0: Error | null, arg1: number | null) => any) | null): void; + /** + * Synchronously open a file. + * @param {string | Buffer | URL} path + * @param {string?} [flags = 'r'] + * @param {string?} [mode = 0o666] + * @param {object?|function?} [options] + */ + export function openSync(path: string | Buffer | URL, flags?: string | null, mode?: string | null, options?: any): any; + /** + * Asynchronously open a directory calling `callback` upon success or error. + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} + * @param {string | Buffer | URL} path + * @param {object?|function(Error?, Dir?)} [options] + * @param {string?} [options.encoding = 'utf8'] + * @param {boolean?} [options.withFileTypes = false] + * @param {function(Error?, Dir?)?} callback + */ + export function opendir(path: string | Buffer | URL, options: {}, callback: ((arg0: Error | null, arg1: Dir | null) => any) | null): void; + /** + * Synchronously open a directory. + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} + * @param {string | Buffer | URL} path + * @param {object?|function(Error?, Dir?)} [options] + * @param {string?} [options.encoding = 'utf8'] + * @param {boolean?} [options.withFileTypes = false] + * @return {Dir} + */ + export function opendirSync(path: string | Buffer | URL, options?: {}): Dir; + /** + * Asynchronously read from an open file descriptor. + * @see {@link https://nodejs.org/api/fs.html#fsreadfd-buffer-offset-length-position-callback} + * @param {number} fd + * @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to. + * @param {number} offset - The position in buffer to write the data to. + * @param {number} length - The number of bytes to read. + * @param {number | BigInt | null} position - Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged. + * @param {function(Error?, number?, Buffer?)} callback + */ + export function read(fd: number, buffer: object | Buffer | TypedArray, offset: number, length: number, position: number | BigInt | null, options: any, callback: (arg0: Error | null, arg1: number | null, arg2: Buffer | null) => any): void; + /** + * Asynchronously write to an open file descriptor. + * @see {@link https://nodejs.org/api/fs.html#fswritefd-buffer-offset-length-position-callback} + * @param {number} fd + * @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to. + * @param {number} offset - The position in buffer to write the data to. + * @param {number} length - The number of bytes to read. + * @param {number | BigInt | null} position - Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged. + * @param {function(Error?, number?, Buffer?)} callback + */ + export function write(fd: number, buffer: object | Buffer | TypedArray, offset: number, length: number, position: number | BigInt | null, options: any, callback: (arg0: Error | null, arg1: number | null, arg2: Buffer | null) => any): void; + /** + * Asynchronously read all entries in a directory. + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} + * @param {string | Buffer | URL } path + * @param {object?|function(Error?, object[])} [options] + * @param {string?} [options.encoding ? 'utf8'] + * @param {boolean?} [options.withFileTypes ? false] + * @param {function(Error?, object[])} callback + */ + export function readdir(path: string | Buffer | URL, options: {}, callback: (arg0: Error | null, arg1: object[]) => any): void; + /** + * Synchronously read all entries in a directory. + * @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback} + * @param {string | Buffer | URL } path + * @param {object?|function(Error?, object[])} [options] + * @param {string?} [options.encoding ? 'utf8'] + * @param {boolean?} [options.withFileTypes ? false] + */ + export function readdirSync(path: string | Buffer | URL, options?: {}): any[]; + /** + * @param {string | Buffer | URL | number } path + * @param {object?|function(Error?, Buffer?)} [options] + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.flag ? 'r'] + * @param {AbortSignal?} [options.signal] + * @param {function(Error?, Buffer?)} callback + */ + export function readFile(path: string | Buffer | URL | number, options: {}, callback: (arg0: Error | null, arg1: Buffer | null) => any): void; + /** + * @param {string|Buffer|URL|number} path + * @param {{ encoding?: string = 'utf8', flags?: string = 'r'}} [options] + * @param {object?|function(Error?, Buffer?)} [options] + * @param {AbortSignal?} [options.signal] + */ + export function readFileSync(path: string | Buffer | URL | number, options?: { + encoding?: string; + flags?: string; + }): any; + /** + * Reads link at `path` + * @param {string} path + * @param {function(err, string)} callback + */ + export function readlink(path: string, callback: (arg0: err, arg1: string) => any): void; + /** + * Computes real path for `path` + * @param {string} path + * @param {function(err, string)} callback + */ + export function realpath(path: string, callback: (arg0: err, arg1: string) => any): void; + /** + * Computes real path for `path` + * @param {string} path + */ + export function realpathSync(path: string): void; + /** + * Renames file or directory at `src` to `dest`. + * @param {string} src + * @param {string} dest + * @param {function} callback + */ + export function rename(src: string, dest: string, callback: Function): void; + /** + * Renames file or directory at `src` to `dest`, synchronously. + * @param {string} src + * @param {string} dest + */ + export function renameSync(src: string, dest: string): void; + /** + * Removes directory at `path`. + * @param {string} path + * @param {function} callback + */ + export function rmdir(path: string, callback: Function): void; + /** + * Removes directory at `path`, synchronously. + * @param {string} path + */ + export function rmdirSync(path: string): void; + /** + * Synchronously get the stats of a file + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.flag ? 'r'] + */ + export function statSync(path: string | Buffer | URL | number, options?: object | null): promises.Stats; + /** + * Get the stats of a file + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.flag ? 'r'] + * @param {AbortSignal?} [options.signal] + * @param {function(Error?, Stats?)} callback + */ + export function stat(path: string | Buffer | URL | number, options: object | null, callback: (arg0: Error | null, arg1: Stats | null) => any): void; + /** + * Get the stats of a symbolic link + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.flag ? 'r'] + * @param {AbortSignal?} [options.signal] + * @param {function(Error?, Stats?)} callback + */ + export function lstat(path: string | Buffer | URL | number, options: object | null, callback: (arg0: Error | null, arg1: Stats | null) => any): void; + /** + * Creates a symlink of `src` at `dest`. + * @param {string} src + * @param {string} dest + */ + export function symlink(src: string, dest: string, type: any, callback: any): void; + /** + * Unlinks (removes) file at `path`. + * @param {string} path + * @param {function} callback + */ + export function unlink(path: string, callback: Function): void; + /** + * Unlinks (removes) file at `path`, synchronously. + * @param {string} path + */ + export function unlinkSync(path: string): void; + /** + * @see {@url https://nodejs.org/api/fs.html#fswritefilefile-data-options-callback} + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {string | Buffer | TypedArray | DataView | object } data + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.mode ? 0o666] + * @param {string?} [options.flag ? 'w'] + * @param {AbortSignal?} [options.signal] + * @param {function(Error?)} callback + */ + export function writeFile(path: string | Buffer | URL | number, data: string | Buffer | TypedArray | DataView | object, options: object | null, callback: (arg0: Error | null) => any): void; + /** + * Writes data to a file synchronously. + * @param {string | Buffer | URL | number } path - filename or file descriptor + * @param {string | Buffer | TypedArray | DataView | object } data + * @param {object?} options + * @param {string?} [options.encoding ? 'utf8'] + * @param {string?} [options.mode ? 0o666] + * @param {string?} [options.flag ? 'w'] + * @param {AbortSignal?} [options.signal] + * @see {@link https://nodejs.org/api/fs.html#fswritefilesyncfile-data-options} + */ + export function writeFileSync(path: string | Buffer | URL | number, data: string | Buffer | TypedArray | DataView | object, options: object | null): void; + /** + * Watch for changes at `path` calling `callback` + * @param {string} + * @param {function|object=} [options] + * @param {string=} [options.encoding = 'utf8'] + * @param {?function} [callback] + * @return {Watcher} + */ + export function watch(path: any, options?: (Function | object) | undefined, callback?: Function | null): Watcher; + export default exports; + export type Buffer = import("socket:buffer").Buffer; + export type TypedArray = Uint8Array | Int8Array; + import { Buffer } from "socket:buffer"; + import { ReadStream } from "socket:fs/stream"; + import { WriteStream } from "socket:fs/stream"; + import { Dir } from "socket:fs/dir"; + import * as promises from "socket:fs/promises"; + import { Stats } from "socket:fs/stats"; + import { Watcher } from "socket:fs/watcher"; + import bookmarks from "socket:fs/bookmarks"; + import * as constants from "socket:fs/constants"; + import { DirectoryHandle } from "socket:fs/handle"; + import { Dirent } from "socket:fs/dir"; + import fds from "socket:fs/fds"; + import { FileHandle } from "socket:fs/handle"; + import * as exports from "socket:fs/index"; + + export { bookmarks, constants, Dir, DirectoryHandle, Dirent, fds, FileHandle, promises, ReadStream, Stats, Watcher, WriteStream }; +} + +declare module "socket:fs" { + export * from "socket:fs/index"; + export default exports; + import * as exports from "socket:fs/index"; +} + +declare module "socket:external/libsodium/index" { + const _default: any; + export default _default; +} + +declare module "socket:crypto/sodium" { + export {}; +} + +declare module "socket:crypto" { + /** + * Generate cryptographically strong random values into the `buffer` + * @param {TypedArray} buffer + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues} + * @return {TypedArray} + */ + export function getRandomValues(buffer: TypedArray, ...args: any[]): TypedArray; + /** + * Generate a random 64-bit number. + * @returns {BigInt} - A random 64-bit number. + */ + export function rand64(): BigInt; + /** + * Generate `size` random bytes. + * @param {number} size - The number of bytes to generate. The size must not be larger than 2**31 - 1. + * @returns {Buffer} - A promise that resolves with an instance of socket.Buffer with random bytes. + */ + export function randomBytes(size: number): Buffer; + /** + * @param {string} algorithm - `SHA-1` | `SHA-256` | `SHA-384` | `SHA-512` + * @param {Buffer | TypedArray | DataView} message - An instance of socket.Buffer, TypedArray or Dataview. + * @returns {Promise} - A promise that resolves with an instance of socket.Buffer with the hash. + */ + export function createDigest(algorithm: string, buf: any): Promise; + /** + * A murmur3 hash implementation based on https://github.com/jwerle/murmurhash.c + * that works on strings and `ArrayBuffer` views (typed arrays) + * @param {string|Uint8Array|ArrayBuffer} value + * @param {number=} [seed = 0] + * @return {number} + */ + export function murmur3(value: string | Uint8Array | ArrayBuffer, seed?: number | undefined): number; + /** + * @typedef {Uint8Array|Int8Array} TypedArray + */ + /** + * WebCrypto API + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto} + */ + export let webcrypto: any; + /** + * A promise that resolves when all internals to be loaded/ready. + * @type {Promise} + */ + export const ready: Promise; + /** + * Maximum total size of random bytes per page + */ + export const RANDOM_BYTES_QUOTA: number; + /** + * Maximum total size for random bytes. + */ + export const MAX_RANDOM_BYTES: 281474976710655; + /** + * Maximum total amount of allocated per page of bytes (max/quota) + */ + export const MAX_RANDOM_BYTES_PAGES: number; + export default exports; + export type TypedArray = Uint8Array | Int8Array; + import { Buffer } from "socket:buffer"; + export namespace sodium { + let ready: Promise; + } + import * as exports from "socket:crypto"; + +} + +declare module "socket:ai" { + /** + * A class to interact with large language models (using llama.cpp) + */ + export class LLM extends EventEmitter { + /** + * Constructs an LLM instance. Each parameter is designed to configure and control + * the behavior of the underlying large language model provided by llama.cpp. + * @param {Object} options - Configuration options for the LLM instance. + * @param {string} options.path - The file path to the model in .gguf format. This model file contains + * the weights and configuration necessary for initializing the language model. + * @param {string} options.prompt - The initial input text to the model, setting the context or query + * for generating responses. The model uses this as a starting point for text generation. + * @param {string} [options.id] - An optional unique identifier for this specific instance of the model, + * useful for tracking or referencing the model in multi-model setups. + * @param {number} [options.n_ctx=1024] - Specifies the maximum number of tokens that the model can consider + * for a single query. This is crucial for managing memory and computational + * efficiency. Exceeding the model's configuration may lead to errors or truncated outputs. + * @param {number} [options.n_threads=8] - The number of threads allocated for the model's computation, + * affecting performance and speed of response generation. + * @param {number} [options.temp=1.1] - Sampling temperature controls the randomness of predictions. + * Higher values increase diversity, potentially at the cost of coherence. + * @param {number} [options.max_tokens=512] - The upper limit on the number of tokens that the model can generate + * in response to a single prompt. This prevents runaway generations. + * @param {number} [options.n_gpu_layers=32] - The number of GPU layers dedicated to the model processing. + * More layers can increase accuracy and complexity of the outputs. + * @param {number} [options.n_keep=0] - Determines how many of the top generated responses are retained after + * the initial generation phase. Useful for models that generate multiple outputs. + * @param {number} [options.n_batch=0] - The size of processing batches. Larger batch sizes can reduce + * the time per token generation by parallelizing computations. + * @param {number} [options.n_predict=0] - Specifies how many forward predictions the model should make + * from the current state. This can pre-generate responses or calculate probabilities. + * @param {number} [options.grp_attn_n=0] - Group attention parameter 'N' modifies how attention mechanisms + * within the model are grouped and interact, affecting the model’s focus and accuracy. + * @param {number} [options.grp_attn_w=0] - Group attention parameter 'W' adjusts the width of each attention group, + * influencing the breadth of context considered by each attention group. + * @param {number} [options.seed=0] - A seed for the random number generator used in the model. Setting this ensures + * consistent results in model outputs, important for reproducibility in experiments. + * @param {number} [options.top_k=0] - Limits the model's output choices to the top 'k' most probable next words, + * reducing the risk of less likely, potentially nonsensical outputs. + * @param {number} [options.tok_p=0.0] - Top-p (nucleus) sampling threshold, filtering the token selection pool + * to only those whose cumulative probability exceeds this value, enhancing output relevance. + * @param {number} [options.min_p=0.0] - Sets a minimum probability filter for token generation, ensuring + * that generated tokens have at least this likelihood of being relevant or coherent. + * @param {number} [options.tfs_z=0.0] - Temperature factor scale for zero-shot learning scenarios, adjusting how + * the model weights novel or unseen prompts during generation. + * @throws {Error} Throws an error if the model path is not provided, as the model cannot initialize without it. + */ + constructor(options?: { + path: string; + prompt: string; + id?: string; + n_ctx?: number; + n_threads?: number; + temp?: number; + max_tokens?: number; + n_gpu_layers?: number; + n_keep?: number; + n_batch?: number; + n_predict?: number; + grp_attn_n?: number; + grp_attn_w?: number; + seed?: number; + top_k?: number; + tok_p?: number; + min_p?: number; + tfs_z?: number; + }); + path: string; + prompt: string; + id: string | BigInt; + /** + * Tell the LLM to stop after the next token. + * @returns {Promise} A promise that resolves when the LLM stops. + */ + stop(): Promise; + /** + * Send a message to the chat. + * @param {string} message - The message to send to the chat. + * @returns {Promise} A promise that resolves with the response from the chat. + */ + chat(message: string): Promise; + } + export default exports; + import { EventEmitter } from "socket:events"; + import * as exports from "socket:ai"; + +} + +declare module "socket:window/constants" { + export const WINDOW_ERROR: -1; + export const WINDOW_NONE: 0; + export const WINDOW_CREATING: 10; + export const WINDOW_CREATED: 11; + export const WINDOW_HIDING: 20; + export const WINDOW_HIDDEN: 21; + export const WINDOW_SHOWING: 30; + export const WINDOW_SHOWN: 31; + export const WINDOW_CLOSING: 40; + export const WINDOW_CLOSED: 41; + export const WINDOW_EXITING: 50; + export const WINDOW_EXITED: 51; + export const WINDOW_KILLING: 60; + export const WINDOW_KILLED: 61; + export default exports; + import * as exports from "socket:window/constants"; + +} + +declare module "socket:application/client" { + /** + * @typedef {{ + * id?: string | null, + * type?: 'window' | 'worker', + * parent?: object | null, + * top?: object | null, + * frameType?: 'top-level' | 'nested' | 'none' + * }} ClientState + */ + export class Client { + /** + * `Client` class constructor + * @private + * @param {ClientState} state + */ + private constructor(); + /** + * The unique ID of the client. + * @type {string|null} + */ + get id(): string; + /** + * The frame type of the client. + * @type {'top-level'|'nested'|'none'} + */ + get frameType(): "none" | "top-level" | "nested"; + /** + * The type of the client. + * @type {'window'|'worker'} + */ + get type(): "window" | "worker"; + /** + * The parent client of the client. + * @type {Client|null} + */ + get parent(): Client; + /** + * The top client of the client. + * @type {Client|null} + */ + get top(): Client; + /** + * A readonly `URL` of the current location of this client. + * @type {URL} + */ + get location(): URL; + /** + * Converts this `Client` instance to JSON. + * @return {object} + */ + toJSON(): object; + #private; + } + const _default: any; + export default _default; + export type ClientState = { + id?: string | null; + type?: "window" | "worker"; + parent?: object | null; + top?: object | null; + frameType?: "top-level" | "nested" | "none"; + }; +} + +declare module "socket:window/hotkey" { + /** + * Normalizes an expression string. + * @param {string} expression + * @return {string} + */ + export function normalizeExpression(expression: string): string; + /** + * Bind a global hotkey expression. + * @param {string} expression + * @param {{ passive?: boolean }} [options] + * @return {Promise} + */ + export function bind(expression: string, options?: { + passive?: boolean; + }): Promise; + /** + * Bind a global hotkey expression. + * @param {string} expression + * @param {object=} [options] + * @return {Promise} + */ + export function unbind(id: any, options?: object | undefined): Promise; + /** + * Get all known globally register hotkey bindings. + * @param {object=} [options] + * @return {Promise} + */ + export function getBindings(options?: object | undefined): Promise; + /** + * Get all known possible keyboard modifier and key mappings for + * expression bindings. + * @param {object=} [options] + * @return {Promise<{ keys: object, modifiers: object }>} + */ + export function getMappings(options?: object | undefined): Promise<{ + keys: object; + modifiers: object; + }>; + /** + * Adds an event listener to the global active bindings. This function is just + * proxy to `bindings.addEventListener`. + * @param {string} type + * @param {function(Event)} listener + * @param {(boolean|object)=} [optionsOrUseCapture] + */ + export function addEventListener(type: string, listener: (arg0: Event) => any, optionsOrUseCapture?: (boolean | object) | undefined): void; + /** + * Removes an event listener to the global active bindings. This function is + * just a proxy to `bindings.removeEventListener` + * @param {string} type + * @param {function(Event)} listener + * @param {(boolean|object)=} [optionsOrUseCapture] + */ + export function removeEventListener(type: string, listener: (arg0: Event) => any, optionsOrUseCapture?: (boolean | object) | undefined): void; + /** + * A high level bindings container map that dispatches events. + */ + export class Bindings extends EventTarget { + /** + * `Bindings` class constructor. + * @ignore + * @param {EventTarget} [sourceEventTarget] + */ + constructor(sourceEventTarget?: EventTarget); + /** + * Global `HotKeyEvent` event listener for `Binding` instance event dispatch. + * @ignore + * @param {import('../internal/events.js').HotKeyEvent} event + */ + onHotKey(event: import("socket:internal/events").HotKeyEvent): boolean; + /** + * The number of `Binding` instances in the mapping. + * @type {number} + */ + get size(): number; + /** + * Setter for the level 1 'error'` event listener. + * @ignore + * @type {function(ErrorEvent)?} + */ + set onerror(onerror: (arg0: ErrorEvent) => any); + /** + * Level 1 'error'` event listener. + * @type {function(ErrorEvent)?} + */ + get onerror(): (arg0: ErrorEvent) => any; + /** + * Setter for the level 1 'hotkey'` event listener. + * @ignore + * @type {function(HotKeyEvent)?} + */ + set onhotkey(onhotkey: (arg0: hotkeyEvent) => any); + /** + * Level 1 'hotkey'` event listener. + * @type {function(hotkeyEvent)?} + */ + get onhotkey(): (arg0: hotkeyEvent) => any; + /** + * Initializes bindings from global context. + * @ignore + * @return {Promise} + */ + init(): Promise; + /** + * Get a binding by `id` + * @param {number} id + * @return {Binding} + */ + get(id: number): Binding; + /** + * Set a `binding` a by `id`. + * @param {number} id + * @param {Binding} binding + */ + set(id: number, binding: Binding): void; + /** + * Delete a binding by `id` + * @param {number} id + * @return {boolean} + */ + delete(id: number): boolean; + /** + * Returns `true` if a binding exists in the mapping, otherwise `false`. + * @return {boolean} + */ + has(id: any): boolean; + /** + * Known `Binding` values in the mapping. + * @return {{ next: function(): { value: Binding|undefined, done: boolean } }} + */ + values(): { + next: () => { + value: Binding | undefined; + done: boolean; + }; + }; + /** + * Known `Binding` keys in the mapping. + * @return {{ next: function(): { value: number|undefined, done: boolean } }} + */ + keys(): { + next: () => { + value: number | undefined; + done: boolean; + }; + }; + /** + * Known `Binding` ids in the mapping. + * @return {{ next: function(): { value: number|undefined, done: boolean } }} + */ + ids(): { + next: () => { + value: number | undefined; + done: boolean; + }; + }; + /** + * Known `Binding` ids and values in the mapping. + * @return {{ next: function(): { value: [number, Binding]|undefined, done: boolean } }} + */ + entries(): { + next: () => { + value: [number, Binding] | undefined; + done: boolean; + }; + }; + /** + * Bind a global hotkey expression. + * @param {string} expression + * @return {Promise} + */ + bind(expression: string): Promise; + /** + * Bind a global hotkey expression. + * @param {string} expression + * @return {Promise} + */ + unbind(expression: string): Promise; + /** + * Returns an array of all active bindings for the application. + * @return {Promise} + */ + active(): Promise; + /** + * Resets all active bindings in the application. + * @param {boolean=} [currentContextOnly] + * @return {Promise} + */ + reset(currentContextOnly?: boolean | undefined): Promise; + /** + * Implements the `Iterator` protocol for each currently registered + * active binding in this window context. The `AsyncIterator` protocol + * will probe for all gloally active bindings. + * @return {Iterator} + */ + [Symbol.iterator](): Iterator; + /** + * Implements the `AsyncIterator` protocol for each globally active + * binding registered to the application. This differs from the `Iterator` + * protocol as this will probe for _all_ active bindings in the entire + * application context. + * @return {AsyncGenerator} + */ + [Symbol.asyncIterator](): AsyncGenerator; + #private; + } + /** + * An `EventTarget` container for a hotkey binding. + */ + export class Binding extends EventTarget { + /** + * `Binding` class constructor. + * @ignore + * @param {object} data + */ + constructor(data: object); + /** + * `true` if the binding is valid, otherwise `false`. + * @type {boolean} + */ + get isValid(): boolean; + /** + * `true` if the binding is considered active, otherwise `false`. + * @type {boolean} + */ + get isActive(): boolean; + /** + * The global unique ID for this binding. + * @type {number?} + */ + get id(): number; + /** + * The computed hash for this binding expression. + * @type {number?} + */ + get hash(): number; + /** + * The normalized expression as a sequence of tokens. + * @type {string[]} + */ + get sequence(): string[]; + /** + * The original expression of the binding. + * @type {string?} + */ + get expression(): string; + /** + * Setter for the level 1 'hotkey'` event listener. + * @ignore + * @type {function(HotKeyEvent)?} + */ + set onhotkey(onhotkey: (arg0: hotkeyEvent) => any); + /** + * Level 1 'hotkey'` event listener. + * @type {function(hotkeyEvent)?} + */ + get onhotkey(): (arg0: hotkeyEvent) => any; + /** + * Binds this hotkey expression. + * @return {Promise} + */ + bind(): Promise; + /** + * Unbinds this hotkey expression. + * @return {Promise} + */ + unbind(): Promise; + /** + * Implements the `AsyncIterator` protocol for async 'hotkey' events + * on this binding instance. + * @return {AsyncGenerator} + */ + [Symbol.asyncIterator](): AsyncGenerator; + #private; + } + /** + * A container for all the bindings currently bound + * by this window context. + * @type {Bindings} + */ + export const bindings: Bindings; + export default bindings; + import { HotKeyEvent } from "socket:internal/events"; +} + +declare module "socket:window" { + /** + * @param {string} url + * @return {string} + * @ignore + */ + export function formatURL(url: string): string; + /** + * @class ApplicationWindow + * Represents a window in the application + */ + export class ApplicationWindow { + static constants: typeof statuses; + static hotkey: import("socket:window/hotkey").Bindings; + constructor({ index, ...options }: { + [x: string]: any; + index: any; + }); + /** + * The unique ID of this window. + * @type {string} + */ + get id(): string; + /** + * Get the index of the window + * @return {number} - the index of the window + */ + get index(): number; + /** + * @type {import('./window/hotkey.js').default} + */ + get hotkey(): import("socket:window/hotkey").Bindings; + /** + * The broadcast channel for this window. + * @type {BroadcastChannel} + */ + get channel(): BroadcastChannel; + /** + * Get the size of the window + * @return {{ width: number, height: number }} - the size of the window + */ + getSize(): { + width: number; + height: number; + }; + /** + * Get the position of the window + * @return {{ x: number, y: number }} - the position of the window + */ + getPosition(): { + x: number; + y: number; + }; + /** + * Get the title of the window + * @return {string} - the title of the window + */ + getTitle(): string; + /** + * Get the status of the window + * @return {string} - the status of the window + */ + getStatus(): string; + /** + * Close the window + * @return {Promise} - the options of the window + */ + close(): Promise; + /** + * Shows the window + * @return {Promise} + */ + show(): Promise; + /** + * Hides the window + * @return {Promise} + */ + hide(): Promise; + /** + * Maximize the window + * @return {Promise} + */ + maximize(): Promise; + /** + * Minimize the window + * @return {Promise} + */ + minimize(): Promise; + /** + * Restore the window + * @return {Promise} + */ + restore(): Promise; + /** + * Sets the title of the window + * @param {string} title - the title of the window + * @return {Promise} + */ + setTitle(title: string): Promise; + /** + * Sets the size of the window + * @param {object} opts - an options object + * @param {(number|string)=} opts.width - the width of the window + * @param {(number|string)=} opts.height - the height of the window + * @return {Promise} + * @throws {Error} - if the width or height is invalid + */ + setSize(opts: { + width?: (number | string) | undefined; + height?: (number | string) | undefined; + }): Promise; + /** + * Sets the position of the window + * @param {object} opts - an options object + * @param {(number|string)=} opts.x - the x position of the window + * @param {(number|string)=} opts.y - the y position of the window + * @return {Promise} + * @throws {Error} - if the x or y is invalid + */ + setPosition(opts: { + x?: (number | string) | undefined; + y?: (number | string) | undefined; + }): Promise; + /** + * Navigate the window to a given path + * @param {object} path - file path + * @return {Promise} + */ + navigate(path: object): Promise; + /** + * Opens the Web Inspector for the window + * @return {Promise} + */ + showInspector(): Promise; + /** + * Sets the background color of the window + * @param {object} opts - an options object + * @param {number} opts.red - the red value + * @param {number} opts.green - the green value + * @param {number} opts.blue - the blue value + * @param {number} opts.alpha - the alpha value + * @return {Promise} + */ + setBackgroundColor(opts: { + red: number; + green: number; + blue: number; + alpha: number; + }): Promise; + /** + * Gets the background color of the window + * @return {Promise} + */ + getBackgroundColor(): Promise; + /** + * Opens a native context menu. + * @param {object} options - an options object + * @return {Promise} + */ + setContextMenu(options: object): Promise; + /** + * Shows a native open file dialog. + * @param {object} options - an options object + * @return {Promise} - an array of file paths + */ + showOpenFilePicker(options: object): Promise; + /** + * Shows a native save file dialog. + * @param {object} options - an options object + * @return {Promise} - an array of file paths + */ + showSaveFilePicker(options: object): Promise; + /** + * Shows a native directory dialog. + * @param {object} options - an options object + * @return {Promise} - an array of file paths + */ + showDirectoryFilePicker(options: object): Promise; + /** + * This is a high-level API that you should use instead of `ipc.request` when + * you want to send a message to another window or to the backend. + * + * @param {object} options - an options object + * @param {number=} options.window - the window to send the message to + * @param {boolean=} [options.backend = false] - whether to send the message to the backend + * @param {string} options.event - the event to send + * @param {(string|object)=} options.value - the value to send + * @returns + */ + send(options: { + window?: number | undefined; + backend?: boolean | undefined; + event: string; + value?: (string | object) | undefined; + }): Promise; + /** + * Post a message to a window + * TODO(@jwerle): research using `BroadcastChannel` instead + * @param {object} message + * @return {Promise} + */ + postMessage(message: object): Promise; + /** + * Opens an URL in the default application associated with the URL protocol, + * such as 'https:' for the default web browser. + * @param {string} value + * @returns {Promise<{ url: string }>} + */ + openExternal(value: string): Promise<{ + url: string; + }>; + /** + * Opens a file in the default file explorer. + * @param {string} value + * @returns {Promise} + */ + revealFile(value: string): Promise; + /** + * Adds a listener to the window. + * @param {string} event - the event to listen to + * @param {function(*): void} cb - the callback to call + * @returns {void} + */ + addListener(event: string, cb: (arg0: any) => void): void; + /** + * Adds a listener to the window. An alias for `addListener`. + * @param {string} event - the event to listen to + * @param {function(*): void} cb - the callback to call + * @returns {void} + * @see addListener + */ + on(event: string, cb: (arg0: any) => void): void; + /** + * Adds a listener to the window. The listener is removed after the first call. + * @param {string} event - the event to listen to + * @param {function(*): void} cb - the callback to call + * @returns {void} + */ + once(event: string, cb: (arg0: any) => void): void; + /** + * Removes a listener from the window. + * @param {string} event - the event to remove the listener from + * @param {function(*): void} cb - the callback to remove + * @returns {void} + */ + removeListener(event: string, cb: (arg0: any) => void): void; + /** + * Removes all listeners from the window. + * @param {string} event - the event to remove the listeners from + * @returns {void} + */ + removeAllListeners(event: string): void; + /** + * Removes a listener from the window. An alias for `removeListener`. + * @param {string} event - the event to remove the listener from + * @param {function(*): void} cb - the callback to remove + * @returns {void} + * @see removeListener + */ + off(event: string, cb: (arg0: any) => void): void; + #private; + } + export default ApplicationWindow; + /** + * @ignore + */ + export const constants: typeof statuses; + import ipc from "socket:ipc"; + import * as statuses from "socket:window/constants"; + import client from "socket:application/client"; + import hotkey from "socket:window/hotkey"; + export { client, hotkey }; +} + +declare module "socket:application" { + /** + * Add an application event `type` callback `listener` with `options`. + * @param {string} type + * @param {function(Event|MessageEvent|CustomEvent|ApplicationURLEvent): boolean} listener + * @param {{ once?: boolean }|boolean=} [options] + */ + export function addEventListener(type: string, listener: (arg0: Event | MessageEvent | CustomEvent | ApplicationURLEvent) => boolean, options?: ({ + once?: boolean; + } | boolean) | undefined): void; + /** + * Remove an application event `type` callback `listener` with `options`. + * @param {string} type + * @param {function(Event|MessageEvent|CustomEvent|ApplicationURLEvent): boolean} listener + */ + export function removeEventListener(type: string, listener: (arg0: Event | MessageEvent | CustomEvent | ApplicationURLEvent) => boolean): void; + /** + * Returns the current window index + * @return {number} + */ + export function getCurrentWindowIndex(): number; + /** + * Creates a new window and returns an instance of ApplicationWindow. + * @param {object} opts - an options object + * @param {string=} opts.aspectRatio - a string (split on ':') provides two float values which set the window's aspect ratio. + * @param {boolean=} opts.closable - deterime if the window can be closed. + * @param {boolean=} opts.minimizable - deterime if the window can be minimized. + * @param {boolean=} opts.maximizable - deterime if the window can be maximized. + * @param {number} [opts.margin] - a margin around the webview. (Private) + * @param {number} [opts.radius] - a radius on the webview. (Private) + * @param {number} opts.index - the index of the window. + * @param {string} opts.path - the path to the HTML file to load into the window. + * @param {string=} opts.title - the title of the window. + * @param {string=} opts.titlebarStyle - determines the style of the titlebar (MacOS only). + * @param {string=} opts.windowControlOffsets - a string (split on 'x') provides the x and y position of the traffic lights (MacOS only). + * @param {string=} opts.backgroundColorDark - determines the background color of the window in dark mode. + * @param {string=} opts.backgroundColorLight - determines the background color of the window in light mode. + * @param {(number|string)=} opts.width - the width of the window. If undefined, the window will have the main window width. + * @param {(number|string)=} opts.height - the height of the window. If undefined, the window will have the main window height. + * @param {(number|string)=} [opts.minWidth = 0] - the minimum width of the window + * @param {(number|string)=} [opts.minHeight = 0] - the minimum height of the window + * @param {(number|string)=} [opts.maxWidth = '100%'] - the maximum width of the window + * @param {(number|string)=} [opts.maxHeight = '100%'] - the maximum height of the window + * @param {boolean=} [opts.resizable=true] - whether the window is resizable + * @param {boolean=} [opts.frameless=false] - whether the window is frameless + * @param {boolean=} [opts.utility=false] - whether the window is utility (macOS only) + * @param {boolean=} [opts.shouldExitApplicationOnClose=false] - whether the window can exit the app + * @param {boolean=} [opts.headless=false] - whether the window will be headless or not (no frame) + * @param {string=} [opts.userScript=null] - A user script that will be injected into the window (desktop only) + * @param {string[]=} [opts.protocolHandlers] - An array of protocol handler schemes to register with the new window (requires service worker) + * @return {Promise} + */ + export function createWindow(opts: { + aspectRatio?: string | undefined; + closable?: boolean | undefined; + minimizable?: boolean | undefined; + maximizable?: boolean | undefined; + margin?: number; + radius?: number; + index: number; + path: string; + title?: string | undefined; + titlebarStyle?: string | undefined; + windowControlOffsets?: string | undefined; + backgroundColorDark?: string | undefined; + backgroundColorLight?: string | undefined; + width?: (number | string) | undefined; + height?: (number | string) | undefined; + minWidth?: (number | string) | undefined; + minHeight?: (number | string) | undefined; + maxWidth?: (number | string) | undefined; + maxHeight?: (number | string) | undefined; + resizable?: boolean | undefined; + frameless?: boolean | undefined; + utility?: boolean | undefined; + shouldExitApplicationOnClose?: boolean | undefined; + headless?: boolean | undefined; + userScript?: string | undefined; + protocolHandlers?: string[] | undefined; + }): Promise; + /** + * Returns the current screen size. + * @returns {Promise<{ width: number, height: number }>} + */ + export function getScreenSize(): Promise<{ + width: number; + height: number; + }>; + /** + * Returns the ApplicationWindow instances for the given indices or all windows if no indices are provided. + * @param {number[]} [indices] - the indices of the windows + * @throws {Error} - if indices is not an array of integer numbers + * @return {Promise} + */ + export function getWindows(indices?: number[], options?: any): Promise; + /** + * Returns the ApplicationWindow instance for the given index + * @param {number} index - the index of the window + * @throws {Error} - if index is not a valid integer number + * @returns {Promise} - the ApplicationWindow instance or null if the window does not exist + */ + export function getWindow(index: number, options: any): Promise; + /** + * Returns the ApplicationWindow instance for the current window. + * @return {Promise} + */ + export function getCurrentWindow(): Promise; + /** + * Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. + * @param {number} [code = 0] - an exit code + * @return {Promise} + */ + export function exit(code?: number): Promise; + /** + * Set the native menu for the app. + * + * @param {object} options - an options object + * @param {string} options.value - the menu layout + * @param {number} options.index - the window to target (if applicable) + * @return {Promise} + * + * Socket Runtime provides a minimalist DSL that makes it easy to create + * cross platform native system and context menus. + * + * Menus are created at run time. They can be created from either the Main or + * Render process. The can be recreated instantly by calling the `setSystemMenu` method. + * + * The method takes a string. Here's an example of a menu. The semi colon is + * significant indicates the end of the menu. Use an underscore when there is no + * accelerator key. Modifiers are optional. And well known OS menu options like + * the edit menu will automatically get accelerators you dont need to specify them. + * + * + * ```js + * socket.application.setSystemMenu({ index: 0, value: ` + * App: + * Foo: f; + * + * Edit: + * Cut: x + * Copy: c + * Paste: v + * Delete: _ + * Select All: a; + * + * Other: + * Apple: _ + * Another Test: T + * !Im Disabled: I + * Some Thing: S + Meta + * --- + * Bazz: s + Meta, Control, Alt; + * `) + * ``` + * + * Separators + * + * To create a separator, use three dashes `---`. + * + * + * Accelerator Modifiers + * + * Accelerator modifiers are used as visual indicators but don't have a + * material impact as the actual key binding is done in the event listener. + * + * A capital letter implies that the accelerator is modified by the `Shift` key. + * + * Additional accelerators are `Meta`, `Control`, `Option`, each separated + * by commas. If one is not applicable for a platform, it will just be ignored. + * + * On MacOS `Meta` is the same as `Command`. + * + * + * Disabled Items + * + * If you want to disable a menu item just prefix the item with the `!` character. + * This will cause the item to appear disabled when the system menu renders. + * + * + * Submenus + * + * We feel like nested menus are an anti-pattern. We don't use them. If you have a + * strong argument for them and a very simple pull request that makes them work we + * may consider them. + * + * + * Event Handling + * + * When a menu item is activated, it raises the `menuItemSelected` event in + * the front end code, you can then communicate with your backend code if you + * want from there. + * + * For example, if the `Apple` item is selected from the `Other` menu... + * + * ```js + * window.addEventListener('menuItemSelected', event => { + * assert(event.detail.parent === 'Other') + * assert(event.detail.title === 'Apple') + * }) + * ``` + * + */ + export function setSystemMenu(o: any): Promise; + /** + * An alias to setSystemMenu for creating a tary menu + */ + export function setTrayMenu(o: any): Promise; + /** + * Set the enabled state of the system menu. + * @param {object} value - an options object + * @return {Promise} + */ + export function setSystemMenuItemEnabled(value: object): Promise; + /** + * Predicate function to determine if application is in a "paused" state. + * @return {boolean} + */ + export function isPaused(): boolean; + export const MAX_WINDOWS: 32; + export class ApplicationWindowList { + static from(...args: any[]): exports.ApplicationWindowList; + constructor(items: any); + get length(): number; + get size(): number; + forEach(callback: any, thisArg: any): void; + item(index: any): any; + entries(): any[][]; + keys(): any[]; + values(): any[]; + add(window: any): this; + remove(windowOrIndex: any): boolean; + contains(windowOrIndex: any): boolean; + clear(): this; + get [Symbol.iterator](): () => IterableIterator; + #private; + } + /** + * Socket Runtime version. + * @type {object} - an object containing the version information + */ + export const runtimeVersion: object; + /** + * Runtime debug flag. + * @type {boolean} + */ + export const debug: boolean; + /** + * Application configuration. + * @type {object} + */ + export const config: object; + export namespace backend { + /** + * @param {object} opts - an options object + * @param {boolean} [opts.force = false] - whether to force the existing process to close + * @return {Promise} + */ + function open(opts?: { + force?: boolean; + }): Promise; + /** + * @return {Promise} + */ + function close(): Promise; + } + export default exports; + import { ApplicationURLEvent } from "socket:internal/events"; + import ApplicationWindow from "socket:window"; + import ipc from "socket:ipc"; + import client from "socket:application/client"; + import menu from "socket:application/menu"; + import * as exports from "socket:application"; + + export { client, menu }; +} + +declare module "socket:test/fast-deep-equal" { + export default function equal(a: any, b: any): boolean; +} + +declare module "socket:assert" { + export function assert(value: any, message?: any): void; + export function ok(value: any, message?: any): void; + export function equal(actual: any, expected: any, message?: any): void; + export function notEqual(actual: any, expected: any, message?: any): void; + export function strictEqual(actual: any, expected: any, message?: any): void; + export function notStrictEqual(actual: any, expected: any, message?: any): void; + export function deepEqual(actual: any, expected: any, message?: any): void; + export function notDeepEqual(actual: any, expected: any, message?: any): void; + export class AssertionError extends Error { + constructor(options: any); + actual: any; + expected: any; + operator: any; + } + const _default: typeof assert & { + AssertionError: typeof AssertionError; + ok: typeof ok; + equal: typeof equal; + notEqual: typeof notEqual; + strictEqual: typeof strictEqual; + notStrictEqual: typeof notStrictEqual; + deepEqual: typeof deepEqual; + notDeepEqual: typeof notDeepEqual; + }; + export default _default; +} + +declare module "socket:async_hooks" { + export default exports; + import { AsyncLocalStorage } from "socket:async/storage"; + import { AsyncResource } from "socket:async/resource"; + import { executionAsyncResource } from "socket:async/hooks"; + import { executionAsyncId } from "socket:async/hooks"; + import { triggerAsyncId } from "socket:async/hooks"; + import { createHook } from "socket:async/hooks"; + import * as exports from "socket:async_hooks"; + + export { AsyncLocalStorage, AsyncResource, executionAsyncResource, executionAsyncId, triggerAsyncId, createHook }; +} + +declare module "socket:bluetooth" { + export default exports; + /** + * Create an instance of a Bluetooth service. + */ + export class Bluetooth extends EventEmitter { + static isInitalized: boolean; + /** + * constructor is an example property that is set to `true` + * Creates a new service with key-value pairs + * @param {string} serviceId - Given a default value to determine the type + */ + constructor(serviceId?: string); + serviceId: string; + /** + * Start the Bluetooth service. + * @return {Promise} + * + */ + start(): Promise; + /** + * Start scanning for published values that correspond to a well-known UUID. + * Once subscribed to a UUID, events that correspond to that UUID will be + * emitted. To receive these events you can add an event listener, for example... + * + * ```js + * const ble = new Bluetooth(id) + * ble.subscribe(uuid) + * ble.on(uuid, (data, details) => { + * // ...do something interesting + * }) + * ``` + * + * @param {string} [id = ''] - A well-known UUID + * @return {Promise} + */ + subscribe(id?: string): Promise; + /** + * Start advertising a new value for a well-known UUID + * @param {string} [id=''] - A well-known UUID + * @param {string} [value=''] + * @return {Promise} + */ + publish(id?: string, value?: string): Promise; + } + import * as exports from "socket:bluetooth"; + import { EventEmitter } from "socket:events"; + import ipc from "socket:ipc"; + +} + +declare module "socket:bootstrap" { + /** + * @param {string} dest - file path + * @param {string} hash - hash string + * @param {string} hashAlgorithm - hash algorithm + * @returns {Promise} + */ + export function checkHash(dest: string, hash: string, hashAlgorithm: string): Promise; + export function bootstrap(options: any): Bootstrap; + namespace _default { + export { bootstrap }; + export { checkHash }; + } + export default _default; + class Bootstrap extends EventEmitter { + constructor(options: any); + options: any; + run(): Promise; + /** + * @param {object} options + * @param {Uint8Array} options.fileBuffer + * @param {string} options.dest + * @returns {Promise} + */ + write({ fileBuffer, dest }: { + fileBuffer: Uint8Array; + dest: string; + }): Promise; + /** + * @param {string} url - url to download + * @returns {Promise} + * @throws {Error} - if status code is not 200 + */ + download(url: string): Promise; + cleanup(): void; + } + import { EventEmitter } from "socket:events"; +} + +declare module "socket:shared-worker/index" { + export function init(sharedWorker: any, options: any): Promise; + /** + * Gets the SharedWorker context window. + * This function will create it if it does not already exist. + * @return {Promise; + export const SHARED_WORKER_WINDOW_INDEX: 46; + export const SHARED_WORKER_WINDOW_TITLE: "socket:shared-worker"; + export const SHARED_WORKER_WINDOW_PATH: "/socket/shared-worker/index.html"; + export const channel: BroadcastChannel; + export const workers: Map; + export class SharedWorkerMessagePort extends ipc.IPCMessagePort { + } + export class SharedWorker extends EventTarget { + /** + * `SharedWorker` class constructor. + * @param {string|URL|Blob} aURL + * @param {string|object=} [nameOrOptions] + */ + constructor(aURL: string | URL | Blob, nameOrOptions?: (string | object) | undefined); + set onerror(onerror: any); + get onerror(): any; + get ready(): any; + get port(): any; + get id(): any; + #private; + } + export default SharedWorker; + import ipc from "socket:ipc"; +} + +declare module "socket:internal/promise" { + export const NativePromise: PromiseConstructor; + export namespace NativePromisePrototype { + export let then: (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => globalThis.Promise; + let _catch: (onrejected?: (reason: any) => TResult | PromiseLike) => globalThis.Promise; + export { _catch as catch }; + let _finally: (onfinally?: () => void) => globalThis.Promise; + export { _finally as finally }; + } + export const NativePromiseAll: any; + export const NativePromiseAny: any; + /** + * @typedef {function(any): void} ResolveFunction + */ + /** + * @typedef {function(Error|string|null): void} RejectFunction + */ + /** + * @typedef {function(ResolveFunction, RejectFunction): void} ResolverFunction + */ + /** + * @typedef {{ + * promise: Promise, + * resolve: ResolveFunction, + * reject: RejectFunction + * }} PromiseResolvers + */ + export class Promise extends globalThis.Promise { + /** + * Creates a new `Promise` with resolver functions. + * @see {https://github.com/tc39/proposal-promise-with-resolvers} + * @return {PromiseResolvers} + */ + static withResolvers(): PromiseResolvers; + /** + * `Promise` class constructor. + * @ignore + * @param {ResolverFunction} resolver + */ + constructor(resolver: ResolverFunction); + [resourceSymbol]: { + "__#15@#type": any; + "__#15@#destroyed": boolean; + "__#15@#asyncId": number; + "__#15@#triggerAsyncId": any; + "__#15@#requireManualDestroy": boolean; + readonly type: string; + readonly destroyed: boolean; + asyncId(): number; + triggerAsyncId(): number; + emitDestroy(): CoreAsyncResource; + bind(fn: Function, thisArg?: object | undefined): Function; + runInAsyncScope(fn: Function, thisArg?: object | undefined, ...args?: any[]): any; + }; + } + export namespace Promise { + function all(iterable: any): any; + function any(iterable: any): any; + } + export default Promise; + export type ResolveFunction = (arg0: any) => void; + export type RejectFunction = (arg0: Error | string | null) => void; + export type ResolverFunction = (arg0: ResolveFunction, arg1: RejectFunction) => void; + export type PromiseResolvers = { + promise: Promise; + resolve: ResolveFunction; + reject: RejectFunction; + }; + const resourceSymbol: unique symbol; + import * as asyncHooks from "socket:internal/async/hooks"; +} + +declare module "socket:internal/globals" { + /** + * Gets a runtime global value by name. + * @ignore + * @param {string} name + * @return {any|null} + */ + export function get(name: string): any | null; + /** + * Symbolic global registry + * @ignore + */ + export class GlobalsRegistry { + get global(): any; + symbol(name: any): symbol; + register(name: any, value: any): any; + get(name: any): any; + } + export default registry; + const registry: any; +} + +declare module "socket:console" { + export function patchGlobalConsole(globalConsole: any, options?: {}): any; + export const globalConsole: globalThis.Console; + export class Console { + /** + * @ignore + */ + constructor(options: any); + /** + * @type {import('dom').Console} + */ + console: any; + /** + * @type {Map} + */ + timers: Map; + /** + * @type {Map} + */ + counters: Map; + /** + * @type {function?} + */ + postMessage: Function | null; + write(destination: any, ...args: any[]): any; + assert(assertion: any, ...args: any[]): void; + clear(): void; + count(label?: string): void; + countReset(label?: string): void; + debug(...args: any[]): void; + dir(...args: any[]): void; + dirxml(...args: any[]): void; + error(...args: any[]): void; + info(...args: any[]): void; + log(...args: any[]): void; + table(...args: any[]): any; + time(label?: string): void; + timeEnd(label?: string): void; + timeLog(label?: string): void; + trace(...objects: any[]): void; + warn(...args: any[]): void; + } + const _default: Console & { + Console: typeof Console; + globalConsole: globalThis.Console; + }; + export default _default; +} + +declare module "socket:vm" { + /** + * @ignore + * @param {object[]} transfer + * @param {object} object + * @param {object=} [options] + * @return {object[]} + */ + export function findMessageTransfers(transfers: any, object: object, options?: object | undefined): object[]; + /** + * @ignore + * @param {object} context + */ + export function applyInputContextReferences(context: object): void; + /** + * @ignore + * @param {object} context + */ + export function applyOutputContextReferences(context: object): void; + /** + * @ignore + * @param {object} context + */ + export function filterNonTransferableValues(context: object): void; + /** + * @ignore + * @param {object=} [currentContext] + * @param {object=} [updatedContext] + * @param {object=} [contextReference] + * @return {{ deletions: string[], merges: string[] }} + */ + export function applyContextDifferences(currentContext?: object | undefined, updatedContext?: object | undefined, contextReference?: object | undefined, preserveScriptArgs?: boolean): { + deletions: string[]; + merges: string[]; + }; + /** + * Wrap a JavaScript function source. + * @ignore + * @param {string} source + * @param {object=} [options] + */ + export function wrapFunctionSource(source: string, options?: object | undefined): string; + /** + * Gets the VM context window. + * This function will create it if it does not already exist. + * @return {Promise; + /** + * Gets the `SharedWorker` that for the VM context. + * @return {Promise} + */ + export function getContextWorker(): Promise; + /** + * Terminates the VM script context window. + * @ignore + */ + export function terminateContextWindow(): Promise; + /** + * Terminates the VM script context worker. + * @ignore + */ + export function terminateContextWorker(): Promise; + /** + * Creates a prototype object of known global reserved intrinsics. + * @ignore + */ + export function createIntrinsics(options: any): any; + /** + * Returns `true` if value is an intrinsic, otherwise `false`. + * @param {any} value + * @return {boolean} + */ + export function isIntrinsic(value: any): boolean; + /** + * Get the intrinsic type of a given `value`. + * @param {any} + * @return {function|object|null|undefined} + */ + export function getIntrinsicType(value: any): Function | object | null | undefined; + /** + * Get the intrinsic type string of a given `value`. + * @param {any} + * @return {string|null} + */ + export function getIntrinsicTypeString(value: any): string | null; + /** + * Creates a global proxy object for context execution. + * @ignore + * @param {object} context + * @param {object=} [options] + * @return {Proxy} + */ + export function createGlobalObject(context: object, options?: object | undefined): ProxyConstructor; + /** + * @ignore + * @param {string} source + * @return {boolean} + */ + export function detectFunctionSourceType(source: string): boolean; + /** + * Compiles `source` with `options` into a function. + * @ignore + * @param {string} source + * @param {object=} [options] + * @return {function} + */ + export function compileFunction(source: string, options?: object | undefined): Function; + /** + * Run `source` JavaScript in given context. The script context execution + * context is preserved until the `context` object that points to it is + * garbage collected or there are no longer any references to it and its + * associated `Script` instance. + * @param {string|object|function} source + * @param {object=} [context] + * @param {ScriptOptions=} [options] + * @return {Promise} + */ + export function runInContext(source: string | object | Function, context?: object | undefined, options?: ScriptOptions | undefined): Promise; + /** + * Run `source` JavaScript in new context. The script context is destroyed after + * execution. This is typically a "one off" isolated run. + * @param {string} source + * @param {object=} [context] + * @param {ScriptOptions=} [options] + * @return {Promise} + */ + export function runInNewContext(source: string, context?: object | undefined, options?: ScriptOptions | undefined): Promise; + /** + * Run `source` JavaScript in this current context (`globalThis`). + * @param {string} source + * @param {ScriptOptions=} [options] + * @return {Promise} + */ + export function runInThisContext(source: string, options?: ScriptOptions | undefined): Promise; + /** + * @ignore + * @param {Reference} reference + */ + export function putReference(reference: Reference): void; + /** + * Create a `Reference` for a `value` in a script `context`. + * @param {any} value + * @param {object} context + * @param {object=} [options] + * @return {Reference} + */ + export function createReference(value: any, context: object, options?: object | undefined): Reference; + /** + * Get a script context by ID or values + * @param {string|object|function} id + * @return {Reference?} + */ + export function getReference(id: string | object | Function): Reference | null; + /** + * Remove a script context reference by ID. + * @param {string} id + */ + export function removeReference(id: string): void; + /** + * Get all transferable values in the `object` hierarchy. + * @param {object} object + * @return {object[]} + */ + export function getTransferables(object: object): object[]; + /** + * @ignore + * @param {object} object + * @return {object} + */ + export function createContext(object: object): object; + /** + * Returns `true` if `object` is a "context" object. + * @param {object} + * @return {boolean} + */ + export function isContext(object: any): boolean; + /** + * Shared broadcast for virtual machaines + * @type {BroadcastChannel} + */ + export const channel: BroadcastChannel; + /** + * A container for a context worker message channel that looks like a "worker". + * @ignore + */ + export class ContextWorkerInterface extends EventTarget { + get channel(): any; + get port(): any; + destroy(): void; + #private; + } + /** + * A container proxy for a context worker message channel that + * looks like a "worker". + * @ignore + */ + export class ContextWorkerInterfaceProxy extends EventTarget { + constructor(globals: any); + get port(): any; + #private; + } + /** + * Global reserved values that a script context may not modify. + * @type {string[]} + */ + export const RESERVED_GLOBAL_INTRINSICS: string[]; + /** + * A unique reference to a value owner by a "context object" and a + * `Script` instance. + */ + export class Reference { + /** + * Predicate function to determine if a `value` is an internal or external + * script reference value. + * @param {amy} value + * @return {boolean} + */ + static isReference(value: amy): boolean; + /** + * `Reference` class constructor. + * @param {string} id + * @param {any} value + * @param {object=} [context] + * @param {object=} [options] + */ + constructor(id: string, value: any, context?: object | undefined, options?: object | undefined); + /** + * The unique id of the reference + * @type {string} + */ + get id(): string; + /** + * The underling primitive type of the reference value. + * @ignore + * @type {'undefined'|'object'|'number'|'boolean'|'function'|'symbol'} + */ + get type(): "number" | "boolean" | "symbol" | "undefined" | "object" | "function"; + /** + * The underlying value of the reference. + * @type {any?} + */ + get value(): any; + /** + * The name of the type. + * @type {string?} + */ + get name(): string; + /** + * The `Script` this value belongs to, if available. + * @type {Script?} + */ + get script(): Script; + /** + * The "context object" this reference value belongs to. + * @type {object?} + */ + get context(): any; + /** + * A boolean value to indicate if the underlying reference value is an + * intrinsic value. + * @type {boolean} + */ + get isIntrinsic(): boolean; + /** + * A boolean value to indicate if the underlying reference value is an + * external reference value. + * @type {boolean} + */ + get isExternal(): boolean; + /** + * The intrinsic type this reference may be an instance of or directly refer to. + * @type {function|object} + */ + get intrinsicType(): any; + /** + * Releases strongly held value and weak references + * to the "context object". + */ + release(): void; + /** + * Converts this `Reference` to a JSON object. + * @param {boolean=} [includeValue = false] + */ + toJSON(includeValue?: boolean | undefined): { + __vmScriptReference__: boolean; + id: string; + type: "number" | "boolean" | "symbol" | "undefined" | "object" | "function"; + name: string; + isIntrinsic: boolean; + intrinsicType: string; + }; + #private; + } + /** + * @typedef {{ + * filename?: string, + * context?: object + * }} ScriptOptions + */ + /** + * A `Script` is a container for raw JavaScript to be executed in + * a completely isolated virtual machine context, optionally with + * user supplied context. Context objects references are not actually + * shared, but instead provided to the script execution context using the + * structured cloning algorithm used by the Message Channel API. Context + * differences are computed and applied after execution so the user supplied + * context object realizes context changes after script execution. All script + * sources run in an "async" context so a "top level await" should work. + */ + export class Script extends EventTarget { + /** + * `Script` class constructor + * @param {string} source + * @param {ScriptOptions} [options] + */ + constructor(source: string, options?: ScriptOptions); + /** + * The script identifier. + */ + get id(): any; + /** + * The source for this script. + * @type {string} + */ + get source(): string; + /** + * The filename for this script. + * @type {string} + */ + get filename(): string; + /** + * A promise that resolves when the script is ready. + * @type {Promise} + */ + get ready(): Promise; + /** + * The default script context object + * @type {object} + */ + get context(): any; + /** + * Destroy the script execution context. + * @return {Promise} + */ + destroy(): Promise; + /** + * Run `source` JavaScript in given context. The script context execution + * context is preserved until the `context` object that points to it is + * garbage collected or there are no longer any references to it and its + * associated `Script` instance. + * @param {ScriptOptions=} [options] + * @param {object=} [context] + * @return {Promise} + */ + runInContext(context?: object | undefined, options?: ScriptOptions | undefined): Promise; + /** + * Run `source` JavaScript in new context. The script context is destroyed after + * execution. This is typically a "one off" isolated run. + * @param {ScriptOptions=} [options] + * @param {object=} [context] + * @return {Promise} + */ + runInNewContext(context?: object | undefined, options?: ScriptOptions | undefined): Promise; + /** + * Run `source` JavaScript in this current context (`globalThis`). + * @param {ScriptOptions=} [options] + * @return {Promise} + */ + runInThisContext(options?: ScriptOptions | undefined): Promise; + #private; + } + namespace _default { + export { createGlobalObject }; + export { compileFunction }; + export { createReference }; + export { getContextWindow }; + export { getContextWorker }; + export { getReference }; + export { getTransferables }; + export { putReference }; + export { Reference }; + export { removeReference }; + export { runInContext }; + export { runInNewContext }; + export { runInThisContext }; + export { Script }; + export { createContext }; + export { isContext }; + export { channel }; + } + export default _default; + export type ScriptOptions = { + filename?: string; + context?: object; + }; + import { SharedWorker } from "socket:shared-worker/index"; +} + +declare module "socket:worker_threads/init" { + export const SHARE_ENV: unique symbol; + export const isMainThread: boolean; + export namespace state { + export { isMainThread }; + export let parentPort: any; + export let mainPort: any; + export let workerData: any; + export let url: any; + export let env: {}; + export let id: number; + } + namespace _default { + export { state }; + } + export default _default; +} + +declare module "socket:worker_threads" { + /** + * Set shared worker environment data. + * @param {string} key + * @param {any} value + */ + export function setEnvironmentData(key: string, value: any): void; + /** + * Get shared worker environment data. + * @param {string} key + * @return {any} + */ + export function getEnvironmentData(key: string): any; + /** + + * A pool of known worker threads. + * @type {} + */ + export const workers: () => () => any; + /** + * `true` if this is the "main" thread, otherwise `false` + * The "main" thread is the top level webview window. + * @type {boolean} + */ + export const isMainThread: boolean; + /** + * The main thread `MessagePort` which is `null` when the + * current context is not the "main thread". + * @type {MessagePort?} + */ + export const mainPort: MessagePort | null; + /** + * A worker thread `BroadcastChannel` class. + */ + export class BroadcastChannel extends globalThis.BroadcastChannel { + } + /** + * A worker thread `MessageChannel` class. + */ + export class MessageChannel extends globalThis.MessageChannel { + } + /** + * A worker thread `MessagePort` class. + */ + export class MessagePort extends globalThis.MessagePort { + } + /** + * The current unique thread ID. + * @type {number} + */ + export const threadId: number; + /** + * The parent `MessagePort` instance + * @type {MessagePort?} + */ + export const parentPort: MessagePort | null; + /** + * Transferred "worker data" when creating a new `Worker` instance. + * @type {any?} + */ + export const workerData: any | null; + export class Pipe extends AsyncResource { + /** + * `Pipe` class constructor. + * @param {Childworker} worker + * @ignore + */ + constructor(worker: Childworker); + /** + * `true` if the pipe is still reading, otherwise `false`. + * @type {boolean} + */ + get reading(): boolean; + /** + * Destroys the pipe + */ + destroy(): void; + #private; + } + /** + * @typedef {{ + * env?: object, + * stdin?: boolean = false, + * stdout?: boolean = false, + * stderr?: boolean = false, + * workerData?: any, + * transferList?: any[], + * eval?: boolean = false + * }} WorkerOptions + + /** + * A worker thread that can communicate directly with a parent thread, + * share environment data, and process streamed data. + */ + export class Worker extends EventEmitter { + /** + * `Worker` class constructor. + * @param {string} filename + * @param {WorkerOptions=} [options] + */ + constructor(filename: string, options?: WorkerOptions | undefined); + /** + * Handles incoming worker messages. + * @ignore + * @param {MessageEvent} event + */ + onWorkerMessage(event: MessageEvent): boolean; + /** + * Handles process environment change events + * @ignore + * @param {import('./process.js').ProcessEnvironmentEvent} event + */ + onProcessEnvironmentEvent(event: import("socket:process").ProcessEnvironmentEvent): void; + /** + * The unique ID for this `Worker` thread instace. + * @type {number} + */ + get id(): number; + get threadId(): number; + /** + * A `Writable` standard input stream if `{ stdin: true }` was set when + * creating this `Worker` instance. + * @type {import('./stream.js').Writable?} + */ + get stdin(): Writable; + /** + * A `Readable` standard output stream if `{ stdout: true }` was set when + * creating this `Worker` instance. + * @type {import('./stream.js').Readable?} + */ + get stdout(): Readable; + /** + * A `Readable` standard error stream if `{ stderr: true }` was set when + * creating this `Worker` instance. + * @type {import('./stream.js').Readable?} + */ + get stderr(): Readable; + /** + * Terminates the `Worker` instance + */ + terminate(): void; + postMessage(...args: any[]): void; + #private; + } + namespace _default { + export { Worker }; + export { isMainThread }; + export { parentPort }; + export { setEnvironmentData }; + export { getEnvironmentData }; + export { workerData }; + export { threadId }; + export { SHARE_ENV }; + } + export default _default; + /** + * /** + * A worker thread that can communicate directly with a parent thread, + * share environment data, and process streamed data. + */ + export type WorkerOptions = { + env?: object; + stdin?: boolean; + stdout?: boolean; + stderr?: boolean; + workerData?: any; + transferList?: any[]; + eval?: boolean; + }; + import { AsyncResource } from "socket:async/resource"; + import { EventEmitter } from "socket:events"; + import { Writable } from "socket:stream"; + import { Readable } from "socket:stream"; + import { SHARE_ENV } from "socket:worker_threads/init"; + import init from "socket:worker_threads/init"; + export { SHARE_ENV, init }; +} + +declare module "socket:child_process" { + /** + * Spawns a child process exeucting `command` with `args` + * @param {string} command + * @param {string[]|object=} [args] + * @param {object=} [options + * @return {ChildProcess} + */ + export function spawn(command: string, args?: (string[] | object) | undefined, options?: object | undefined): ChildProcess; + export function exec(command: any, options: any, callback: any): ChildProcess & { + then(resolve: any, reject: any): Promise; + catch(reject: any): Promise; + finally(next: any): Promise; + }; + export function execSync(command: any, options: any): any; + export class Pipe extends AsyncResource { + /** + * `Pipe` class constructor. + * @param {ChildProcess} process + * @ignore + */ + constructor(process: ChildProcess); + /** + * `true` if the pipe is still reading, otherwise `false`. + * @type {boolean} + */ + get reading(): boolean; + /** + * @type {import('./process')} + */ + get process(): typeof import("socket:process"); + /** + * Destroys the pipe + */ + destroy(): void; + #private; + } + export class ChildProcess extends EventEmitter { + /** + * `ChildProcess` class constructor. + * @param {{ + * env?: object, + * stdin?: boolean, + * stdout?: boolean, + * stderr?: boolean, + * signal?: AbortSigal, + * }=} [options] + */ + constructor(options?: { + env?: object; + stdin?: boolean; + stdout?: boolean; + stderr?: boolean; + signal?: AbortSigal; + } | undefined); + /** + * @ignore + * @type {Pipe} + */ + get pipe(): Pipe; + /** + * `true` if the child process was killed with kill()`, + * otherwise `false`. + * @type {boolean} + */ + get killed(): boolean; + /** + * The process identifier for the child process. This value is + * `> 0` if the process was spawned successfully, otherwise `0`. + * @type {number} + */ + get pid(): number; + /** + * The executable file name of the child process that is launched. This + * value is `null` until the child process has successfully been spawned. + * @type {string?} + */ + get spawnfile(): string; + /** + * The full list of command-line arguments the child process was spawned with. + * This value is an empty array until the child process has successfully been + * spawned. + * @type {string[]} + */ + get spawnargs(): string[]; + /** + * Always `false` as the IPC messaging is not supported. + * @type {boolean} + */ + get connected(): boolean; + /** + * The child process exit code. This value is `null` if the child process + * is still running, otherwise it is a positive integer. + * @type {number?} + */ + get exitCode(): number; + /** + * If available, the underlying `stdin` writable stream for + * the child process. + * @type {import('./stream').Writable?} + */ + get stdin(): import("socket:stream").Writable; + /** + * If available, the underlying `stdout` readable stream for + * the child process. + * @type {import('./stream').Readable?} + */ + get stdout(): import("socket:stream").Readable; + /** + * If available, the underlying `stderr` readable stream for + * the child process. + * @type {import('./stream').Readable?} + */ + get stderr(): import("socket:stream").Readable; + /** + * The underlying worker thread. + * @ignore + * @type {import('./worker_threads').Worker} + */ + get worker(): Worker; + /** + * This function does nothing, but is present for nodejs compat. + */ + disconnect(): boolean; + /** + * This function does nothing, but is present for nodejs compat. + * @return {boolean} + */ + send(): boolean; + /** + * This function does nothing, but is present for nodejs compat. + */ + ref(): boolean; + /** + * This function does nothing, but is present for nodejs compat. + */ + unref(): boolean; + /** + * Kills the child process. This function throws an error if the child + * process has not been spawned or is already killed. + * @param {number|string} signal + */ + kill(...args: any[]): this; + /** + * Spawns the child process. This function will thrown an error if the process + * is already spawned. + * @param {string} command + * @param {string[]=} [args] + * @return {ChildProcess} + */ + spawn(...args?: string[] | undefined): ChildProcess; + /** + * `EventTarget` based `addEventListener` method. + * @param {string} event + * @param {function(Event)} callback + * @param {{ once?: false }} [options] + */ + addEventListener(event: string, callback: (arg0: Event) => any, options?: { + once?: false; + }): void; + /** + * `EventTarget` based `removeEventListener` method. + * @param {string} event + * @param {function(Event)} callback + * @param {{ once?: false }} [options] + */ + removeEventListener(event: string, callback: (arg0: Event) => any): void; + #private; + } + export function execFile(command: any, options: any, callback: any): ChildProcess & { + then(resolve: any, reject: any): Promise; + catch(reject: any): Promise; + finally(next: any): Promise; + }; + namespace _default { + export { ChildProcess }; + export { spawn }; + export { execFile }; + export { exec }; + } + export default _default; + import { AsyncResource } from "socket:async/resource"; + import { EventEmitter } from "socket:events"; + import { Worker } from "socket:worker_threads"; +} + +declare module "socket:constants" { + export * from "socket:fs/constants"; + export * from "socket:window/constants"; + export const E2BIG: any; + export const EACCES: any; + export const EADDRINUSE: any; + export const EADDRNOTAVAIL: any; + export const EAFNOSUPPORT: any; + export const EAGAIN: any; + export const EALREADY: any; + export const EBADF: any; + export const EBADMSG: any; + export const EBUSY: any; + export const ECANCELED: any; + export const ECHILD: any; + export const ECONNABORTED: any; + export const ECONNREFUSED: any; + export const ECONNRESET: any; + export const EDEADLK: any; + export const EDESTADDRREQ: any; + export const EDOM: any; + export const EDQUOT: any; + export const EEXIST: any; + export const EFAULT: any; + export const EFBIG: any; + export const EHOSTUNREACH: any; + export const EIDRM: any; + export const EILSEQ: any; + export const EINPROGRESS: any; + export const EINTR: any; + export const EINVAL: any; + export const EIO: any; + export const EISCONN: any; + export const EISDIR: any; + export const ELOOP: any; + export const EMFILE: any; + export const EMLINK: any; + export const EMSGSIZE: any; + export const EMULTIHOP: any; + export const ENAMETOOLONG: any; + export const ENETDOWN: any; + export const ENETRESET: any; + export const ENETUNREACH: any; + export const ENFILE: any; + export const ENOBUFS: any; + export const ENODATA: any; + export const ENODEV: any; + export const ENOENT: any; + export const ENOEXEC: any; + export const ENOLCK: any; + export const ENOLINK: any; + export const ENOMEM: any; + export const ENOMSG: any; + export const ENOPROTOOPT: any; + export const ENOSPC: any; + export const ENOSR: any; + export const ENOSTR: any; + export const ENOSYS: any; + export const ENOTCONN: any; + export const ENOTDIR: any; + export const ENOTEMPTY: any; + export const ENOTSOCK: any; + export const ENOTSUP: any; + export const ENOTTY: any; + export const ENXIO: any; + export const EOPNOTSUPP: any; + export const EOVERFLOW: any; + export const EPERM: any; + export const EPIPE: any; + export const EPROTO: any; + export const EPROTONOSUPPORT: any; + export const EPROTOTYPE: any; + export const ERANGE: any; + export const EROFS: any; + export const ESPIPE: any; + export const ESRCH: any; + export const ESTALE: any; + export const ETIME: any; + export const ETIMEDOUT: any; + export const ETXTBSY: any; + export const EWOULDBLOCK: any; + export const EXDEV: any; + export const SIGHUP: any; + export const SIGINT: any; + export const SIGQUIT: any; + export const SIGILL: any; + export const SIGTRAP: any; + export const SIGABRT: any; + export const SIGIOT: any; + export const SIGBUS: any; + export const SIGFPE: any; + export const SIGKILL: any; + export const SIGUSR1: any; + export const SIGSEGV: any; + export const SIGUSR2: any; + export const SIGPIPE: any; + export const SIGALRM: any; + export const SIGTERM: any; + export const SIGCHLD: any; + export const SIGCONT: any; + export const SIGSTOP: any; + export const SIGTSTP: any; + export const SIGTTIN: any; + export const SIGTTOU: any; + export const SIGURG: any; + export const SIGXCPU: any; + export const SIGXFSZ: any; + export const SIGVTALRM: any; + export const SIGPROF: any; + export const SIGWINCH: any; + export const SIGIO: any; + export const SIGINFO: any; + export const SIGSYS: any; + const _default: any; + export default _default; +} + +declare module "socket:timers/platform" { + export namespace platform { + let setTimeout: any; + let setInterval: any; + let setImmediate: any; + let clearTimeout: any; + let clearInterval: any; + let clearImmediate: any; + let postTask: any; + } + export default platform; +} + +declare module "socket:timers/timer" { + export class Timer extends AsyncResource { + static from(...args: any[]): Timer; + constructor(type: any, create: any, destroy: any); + get id(): number; + init(...args: any[]): this; + close(): boolean; + [Symbol.toPrimitive](): number; + #private; + } + export class Timeout extends Timer { + constructor(); + } + export class Interval extends Timer { + constructor(); + } + export class Immediate extends Timer { + constructor(); + } + namespace _default { + export { Timer }; + export { Immediate }; + export { Timeout }; + export { Interval }; + } + export default _default; + import { AsyncResource } from "socket:async/resource"; +} + +declare module "socket:timers/promises" { + export function setTimeout(delay?: number, value?: any, options?: any): Promise; + export function setInterval(delay?: number, value?: any, options?: any): AsyncGenerator; + export function setImmediate(value?: any, options?: any): Promise; + namespace _default { + export { setImmediate }; + export { setInterval }; + export { setTimeout }; + } + export default _default; +} + +declare module "socket:timers/scheduler" { + export function wait(delay: any, options?: any): Promise; + export function postTask(callback: any, options?: any): Promise; + namespace _default { + export { postTask }; + export { setImmediate as yield }; + export { wait }; + } + export default _default; + import { setImmediate } from "socket:timers/promises"; +} + +declare module "socket:timers/index" { + export function setTimeout(callback: any, delay: any, ...args: any[]): import("socket:timers/timer").Timer; + export function clearTimeout(timeout: any): void; + export function setInterval(callback: any, delay: any, ...args: any[]): import("socket:timers/timer").Timer; + export function clearInterval(interval: any): void; + export function setImmediate(callback: any, ...args: any[]): import("socket:timers/timer").Timer; + export function clearImmediate(immediate: any): void; + /** + * Pause async execution for `timeout` milliseconds. + * @param {number} timeout + * @return {Promise} + */ + export function sleep(timeout: number): Promise; + export namespace sleep { + /** + * Pause sync execution for `timeout` milliseconds. + * @param {number} timeout + */ + function sync(timeout: number): void; + } + export { platform }; + namespace _default { + export { platform }; + export { promises }; + export { scheduler }; + export { setTimeout }; + export { clearTimeout }; + export { setInterval }; + export { clearInterval }; + export { setImmediate }; + export { clearImmediate }; + } + export default _default; + import platform from "socket:timers/platform"; + import promises from "socket:timers/promises"; + import scheduler from "socket:timers/scheduler"; +} + +declare module "socket:timers" { + export * from "socket:timers/index"; + export default exports; + import * as exports from "socket:timers/index"; +} + +declare module "socket:internal/conduit" { + /** + * @typedef {{ options: object, payload: Uint8Array }} ReceiveMessage + * @typedef {function(Error?, ReceiveCallback | undefined)} ReceiveCallback + * @typedef {{ id?: string|BigInt|number, reconnect?: {} }} ConduitOptions + * @typedef {{ isActive: boolean, handles: { ids: string[], count: number }}} ConduitDiagnostics + * @typedef {{ isActive: boolean, port: number }} ConduitStatus + */ + export const DEFALUT_MAX_RECONNECT_RETRIES: 32; + export const DEFAULT_MAX_RECONNECT_TIMEOUT: 256; + /** + * A pool of known `Conduit` instances. + * @type {Set} + */ + export const pool: Set; + /** + * A container for managing a WebSocket connection to the internal runtime + * Conduit WebSocket server. + */ + export class Conduit extends EventTarget { + static set port(port: number); + /** + * The global `Conduit` port + * @type {number} + */ + static get port(): number; + /** + * Returns diagnostics information about the conduit server + * @return {Promise} + */ + static diagnostics(): Promise; + /** + * Returns the current Conduit server status + * @return {Promise} + */ + static status(): Promise; + /** + * Waits for conduit to be active + * @param {{ maxQueriesForStatus?: number }=} [options] + * @return {Promise} + */ + static waitForActiveState(options?: { + maxQueriesForStatus?: number; + } | undefined): Promise; + /** + * Creates an instance of Conduit. + * + * @param {object} params - The parameters for the Conduit. + * @param {string} params.id - The ID for the connection. + * @param {string} params.method - The method to use for the connection. + */ + constructor({ id }: { + id: string; + method: string; + }); + /** + * @type {boolean} + */ + shouldReconnect: boolean; + /** + * @type {boolean} + */ + isConnecting: boolean; + /** + * @type {boolean} + */ + isActive: boolean; + /** + * @type {WebSocket?} + */ + socket: WebSocket | null; + /** + * @type {number} + */ + port: number; + /** + * @type {number?} + */ + id: number | null; + /** + * The URL string for the WebSocket server. + * @type {string} + */ + get url(): string; + set onmessage(onmessage: (arg0: MessageEvent) => any); + /** + * @type {function(MessageEvent)} + */ + get onmessage(): (arg0: MessageEvent) => any; + set onerror(onerror: (arg0: ErrorEvent) => any); + /** + * @type {function(ErrorEvent)} + */ + get onerror(): (arg0: ErrorEvent) => any; + set onclose(onclose: (arg0: CloseEvent) => any); + /** + * @type {function(CloseEvent)} + */ + get onclose(): (arg0: CloseEvent) => any; + set onopen(onopen: (arg0: Event) => any); + /** + * @type {function(Event)} + */ + get onopen(): (arg0: Event) => any; + /** + * Connects the underlying conduit `WebSocket`. + * @param {function(Error?)=} [callback] + * @return {Promise} + */ + connect(callback?: ((arg0: Error | null) => any) | undefined): Promise; + /** + * Reconnects a `Conduit` socket. + * @param {{retries?: number, timeout?: number}} [options] + * @return {Promise} + */ + reconnect(options?: { + retries?: number; + timeout?: number; + }): Promise; + /** + * Encodes a single header into a Uint8Array. + * + * @private + * @param {string} key - The header key. + * @param {string} value - The header value. + * @returns {Uint8Array} The encoded header. + */ + private encodeOption; + /** + * Encodes options and payload into a single Uint8Array message. + * + * @private + * @param {object} options - The options to encode. + * @param {Uint8Array} payload - The payload to encode. + * @returns {Uint8Array} The encoded message. + */ + private encodeMessage; + /** + * Decodes a Uint8Array message into options and payload. + * @param {Uint8Array} data - The data to decode. + * @return {ReceiveMessage} The decoded message containing options and payload. + * @throws Will throw an error if the data is invalid. + */ + decodeMessage(data: Uint8Array): ReceiveMessage; + /** + * Registers a callback to handle incoming messages. + * The callback will receive an error object and an object containing + * decoded options and payload. + * @param {ReceiveCallback} callback - The callback function to handle incoming messages. + */ + receive(callback: ReceiveCallback): void; + /** + * Sends a message with the specified options and payload over the + * WebSocket connection. + * @param {object} options - The options to send. + * @param {Uint8Array=} [payload] - The payload to send. + * @return {boolean} + */ + send(options: object, payload?: Uint8Array | undefined): boolean; + /** + * Closes the WebSocket connection, preventing reconnects. + */ + close(): void; + #private; + } + export type ReceiveMessage = { + options: object; + payload: Uint8Array; + }; + export type ReceiveCallback = (arg0: Error | null, arg1: ReceiveCallback | undefined) => any; + export type ConduitOptions = { + id?: string | BigInt | number; + reconnect?: {}; + }; + export type ConduitDiagnostics = { + isActive: boolean; + handles: { + ids: string[]; + count: number; + }; + }; + export type ConduitStatus = { + isActive: boolean; + port: number; + }; +} + +declare module "socket:ip" { + /** + * Normalizes input as an IPv4 address string + * @param {string|object|string[]|Uint8Array} input + * @return {string} + */ + export function normalizeIPv4(input: string | object | string[] | Uint8Array): string; + /** + * Determines if an input `string` is in IP address version 4 format. + * @param {string|object|string[]|Uint8Array} input + * @return {boolean} + */ + export function isIPv4(input: string | object | string[] | Uint8Array): boolean; + namespace _default { + export { normalizeIPv4 }; + export { isIPv4 }; + } + export default _default; +} + +declare module "socket:dns/promises" { + /** + * @async + * @see {@link https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options} + * @param {string} hostname - The host name to resolve. + * @param {Object=} opts - An options object. + * @param {(number|string)=} [opts.family=0] - The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. + * @returns {Promise} + */ + export function lookup(hostname: string, opts?: any | undefined): Promise; + export default exports; + import * as exports from "socket:dns/promises"; + +} + +declare module "socket:dns/index" { + /** + * Resolves a host name (e.g. `example.org`) into the first found A (IPv4) or + * AAAA (IPv6) record. All option properties are optional. If options is an + * integer, then it must be 4 or 6 – if options is 0 or not provided, then IPv4 + * and IPv6 addresses are both returned if found. + * + * From the node.js website... + * + * > With the all option set to true, the arguments for callback change to (err, + * addresses), with addresses being an array of objects with the properties + * address and family. + * + * > On error, err is an Error object, where err.code is the error code. Keep in + * mind that err.code will be set to 'ENOTFOUND' not only when the host name does + * not exist but also when the lookup fails in other ways such as no available + * file descriptors. dns.lookup() does not necessarily have anything to do with + * the DNS protocol. The implementation uses an operating system facility that + * can associate names with addresses and vice versa. This implementation can + * have subtle but important consequences on the behavior of any Node.js program. + * Please take some time to consult the Implementation considerations section + * before using dns.lookup(). + * + * @see {@link https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback} + * @param {string} hostname - The host name to resolve. + * @param {(object|intenumberger)=} [options] - An options object or record family. + * @param {(number|string)=} [options.family=0] - The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. + * @param {function} cb - The function to call after the method is complete. + * @returns {void} + */ + export function lookup(hostname: string, options?: (object | intenumberger) | undefined, cb: Function): void; + export { promises }; + export default exports; + import * as promises from "socket:dns/promises"; + import * as exports from "socket:dns/index"; + +} + +declare module "socket:dns" { + export * from "socket:dns/index"; + export default exports; + import * as exports from "socket:dns/index"; +} + +declare module "socket:dgram" { + export function createSocket(options: string | any, callback?: Function | undefined): Socket; + /** + * New instances of dgram.Socket are created using dgram.createSocket(). + * The new keyword is not to be used to create dgram.Socket instances. + */ + export class Socket extends EventEmitter { + constructor(options: any, callback: any); + id: any; + knownIdWasGivenInSocketConstruction: boolean; + type: any; + signal: any; + state: { + recvBufferSize: any; + sendBufferSize: any; + bindState: number; + connectState: number; + reuseAddr: boolean; + ipv6Only: boolean; + }; + /** + * Listen for datagram messages on a named port and optional address + * If the address is not specified, the operating system will attempt to + * listen on all addresses. Once the binding is complete, a 'listening' + * event is emitted and the optional callback function is called. + * + * If binding fails, an 'error' event is emitted. + * + * @param {number} port - The port to listen for messages on + * @param {string} address - The address to bind to (0.0.0.0) + * @param {function} callback - With no parameters. Called when binding is complete. + * @see {@link https://nodejs.org/api/dgram.html#socketbindport-address-callback} + */ + bind(arg1: any, arg2: any, arg3: any): this; + dataListener: ({ detail }: { + detail: any; + }) => any; + conduit: Conduit; + /** + * Associates the dgram.Socket to a remote address and port. Every message sent + * by this handle is automatically sent to that destination. Also, the socket + * will only receive messages from that remote peer. Trying to call connect() + * on an already connected socket will result in an ERR_SOCKET_DGRAM_IS_CONNECTED + * exception. If the address is not provided, '0.0.0.0' (for udp4 sockets) or '::1' + * (for udp6 sockets) will be used by default. Once the connection is complete, + * a 'connect' event is emitted and the optional callback function is called. + * In case of failure, the callback is called or, failing this, an 'error' event + * is emitted. + * + * @param {number} port - Port the client should connect to. + * @param {string=} host - Host the client should connect to. + * @param {function=} connectListener - Common parameter of socket.connect() methods. Will be added as a listener for the 'connect' event once. + * @see {@link https://nodejs.org/api/dgram.html#socketconnectport-address-callback} + */ + connect(arg1: any, arg2: any, arg3: any): void; + /** + * A synchronous function that disassociates a connected dgram.Socket from + * its remote address. Trying to call disconnect() on an unbound or already + * disconnected socket will result in an ERR_SOCKET_DGRAM_NOT_CONNECTED exception. + * + * @see {@link https://nodejs.org/api/dgram.html#socketdisconnect} + */ + disconnect(): void; + /** + * Broadcasts a datagram on the socket. For connectionless sockets, the + * destination port and address must be specified. Connected sockets, on the + * other hand, will use their associated remote endpoint, so the port and + * address arguments must not be set. + * + * > The msg argument contains the message to be sent. Depending on its type, + * different behavior can apply. If msg is a Buffer, any TypedArray, or a + * DataView, the offset and length specify the offset within the Buffer where + * the message begins and the number of bytes in the message, respectively. + * If msg is a String, then it is automatically converted to a Buffer with + * 'utf8' encoding. With messages that contain multi-byte characters, offset, + * and length will be calculated with respect to byte length and not the + * character position. If msg is an array, offset and length must not be + * specified. + * + * > The address argument is a string. If the value of the address is a hostname, + * DNS will be used to resolve the address of the host. If the address is not + * provided or otherwise nullish, '0.0.0.0' (for udp4 sockets) or '::1' + * (for udp6 sockets) will be used by default. + * + * > If the socket has not been previously bound with a call to bind, the socket + * is assigned a random port number and is bound to the "all interfaces" + * address ('0.0.0.0' for udp4 sockets, '::1' for udp6 sockets.) + * + * > An optional callback function may be specified as a way of reporting DNS + * errors or for determining when it is safe to reuse the buf object. DNS + * lookups delay the time to send for at least one tick of the Node.js event + * loop. + * + * > The only way to know for sure that the datagram has been sent is by using a + * callback. If an error occurs and a callback is given, the error will be + * passed as the first argument to the callback. If a callback is not given, + * the error is emitted as an 'error' event on the socket object. + * + * > Offset and length are optional but both must be set if either is used. + * They are supported only when the first argument is a Buffer, a TypedArray, + * or a DataView. + * + * @param {Buffer | TypedArray | DataView | string | Array} msg - Message to be sent. + * @param {integer=} offset - Offset in the buffer where the message starts. + * @param {integer=} length - Number of bytes in the message. + * @param {integer=} port - Destination port. + * @param {string=} address - Destination host name or IP address. + * @param {Function=} callback - Called when the message has been sent. + * @see {@link https://nodejs.org/api/dgram.html#socketsendmsg-offset-length-port-address-callback} + */ + send(buffer: any, ...args: any[]): Promise; + /** + * Close the underlying socket and stop listening for data on it. If a + * callback is provided, it is added as a listener for the 'close' event. + * + * @param {function=} callback - Called when the connection is completed or on error. + * + * @see {@link https://nodejs.org/api/dgram.html#socketclosecallback} + */ + close(cb: any): this; + /** + * + * Returns an object containing the address information for a socket. For + * UDP sockets, this object will contain address, family, and port properties. + * + * This method throws EBADF if called on an unbound socket. + * @returns {Object} socketInfo - Information about the local socket + * @returns {string} socketInfo.address - The IP address of the socket + * @returns {string} socketInfo.port - The port of the socket + * @returns {string} socketInfo.family - The IP family of the socket + * + * @see {@link https://nodejs.org/api/dgram.html#socketaddress} + */ + address(): any; + /** + * Returns an object containing the address, family, and port of the remote + * endpoint. This method throws an ERR_SOCKET_DGRAM_NOT_CONNECTED exception + * if the socket is not connected. + * + * @returns {Object} socketInfo - Information about the remote socket + * @returns {string} socketInfo.address - The IP address of the socket + * @returns {string} socketInfo.port - The port of the socket + * @returns {string} socketInfo.family - The IP family of the socket + * @see {@link https://nodejs.org/api/dgram.html#socketremoteaddress} + */ + remoteAddress(): any; + /** + * Sets the SO_RCVBUF socket option. Sets the maximum socket receive buffer in + * bytes. + * + * @param {number} size - The size of the new receive buffer + * @see {@link https://nodejs.org/api/dgram.html#socketsetrecvbuffersizesize} + */ + setRecvBufferSize(size: number): Promise; + /** + * Sets the SO_SNDBUF socket option. Sets the maximum socket send buffer in + * bytes. + * + * @param {number} size - The size of the new send buffer + * @see {@link https://nodejs.org/api/dgram.html#socketsetsendbuffersizesize} + */ + setSendBufferSize(size: number): Promise; + /** + * @see {@link https://nodejs.org/api/dgram.html#socketgetrecvbuffersize} + */ + getRecvBufferSize(): any; + /** + * @returns {number} the SO_SNDBUF socket send buffer size in bytes. + * @see {@link https://nodejs.org/api/dgram.html#socketgetsendbuffersize} + */ + getSendBufferSize(): number; + setBroadcast(): void; + setTTL(): void; + setMulticastTTL(): void; + setMulticastLoopback(): void; + setMulticastMembership(): void; + setMulticastInterface(): void; + addMembership(): void; + dropMembership(): void; + addSourceSpecificMembership(): void; + dropSourceSpecificMembership(): void; + ref(): this; + unref(): this; + #private; + } + /** + * Generic error class for an error occurring on a `Socket` instance. + * @ignore + */ + export class SocketError extends InternalError { + /** + * @type {string} + */ + get code(): string; + } + /** + * Thrown when a socket is already bound. + */ + export class ERR_SOCKET_ALREADY_BOUND extends exports.SocketError { + get message(): string; + } + /** + * @ignore + */ + export class ERR_SOCKET_BAD_BUFFER_SIZE extends exports.SocketError { + } + /** + * @ignore + */ + export class ERR_SOCKET_BUFFER_SIZE extends exports.SocketError { + } + /** + * Thrown when the socket is already connected. + */ + export class ERR_SOCKET_DGRAM_IS_CONNECTED extends exports.SocketError { + get message(): string; + } + /** + * Thrown when the socket is not connected. + */ + export class ERR_SOCKET_DGRAM_NOT_CONNECTED extends exports.SocketError { + syscall: string; + get message(): string; + } + /** + * Thrown when the socket is not running (not bound or connected). + */ + export class ERR_SOCKET_DGRAM_NOT_RUNNING extends exports.SocketError { + get message(): string; + } + /** + * Thrown when a bad socket type is used in an argument. + */ + export class ERR_SOCKET_BAD_TYPE extends TypeError { + code: string; + get message(): string; + } + /** + * Thrown when a bad port is given. + */ + export class ERR_SOCKET_BAD_PORT extends RangeError { + code: string; + } + export default exports; + export type SocketOptions = any; + import { EventEmitter } from "socket:events"; + import { Conduit } from "socket:internal/conduit"; + import { InternalError } from "socket:errors"; + import * as exports from "socket:dgram"; + +} + +declare module "socket:fs/web" { + /** + * Creates a new `File` instance from `filename`. + * @param {string} filename + * @param {{ fd: fs.FileHandle, highWaterMark?: number }=} [options] + * @return {File} + */ + export function createFile(filename: string, options?: { + fd: fs.FileHandle; + highWaterMark?: number; + } | undefined): File; + /** + * Creates a `FileSystemWritableFileStream` instance backed + * by `socket:fs:` module from a given `FileSystemFileHandle` instance. + * @param {string|File} file + * @return {Promise} + */ + export function createFileSystemWritableFileStream(handle: any, options: any): Promise; + /** + * Creates a `FileSystemFileHandle` instance backed by `socket:fs:` module from + * a given `File` instance or filename string. + * @param {string|File} file + * @param {object} [options] + * @return {Promise} + */ + export function createFileSystemFileHandle(file: string | File, options?: object): Promise; + /** + * Creates a `FileSystemDirectoryHandle` instance backed by `socket:fs:` module + * from a given directory name string. + * @param {string} dirname + * @return {Promise} + */ + export function createFileSystemDirectoryHandle(dirname: string, options?: any): Promise; + export const kFileSystemHandleFullName: unique symbol; + export const kFileDescriptor: unique symbol; + export const kFileFullName: unique symbol; + export const File: { + new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File; + prototype: File; + } | { + new (): { + readonly lastModifiedDate: Date; + readonly lastModified: number; + readonly name: any; + readonly size: number; + readonly type: string; + slice(): void; + arrayBuffer(): Promise; + bytes(): Promise; + text(): Promise; + stream(): void; + }; + }; + export const FileSystemHandle: { + new (): { + readonly name: any; + readonly kind: any; + }; + }; + export const FileSystemFileHandle: { + new (): FileSystemFileHandle; + prototype: FileSystemFileHandle; + } | { + new (): { + getFile(): Promise; + createWritable(options?: any): Promise; + createSyncAccessHandle(): Promise; + readonly name: any; + readonly kind: any; + }; + }; + export const FileSystemDirectoryHandle: { + new (): FileSystemDirectoryHandle; + prototype: FileSystemDirectoryHandle; + } | { + new (): { + entries(): AsyncGenerator; + values(): AsyncGenerator; + keys(): AsyncGenerator; + resolve(possibleDescendant: any): Promise; + removeEntry(name: any, options?: any): Promise; + getDirectoryHandle(name: any, options?: any): Promise; + getFileHandle(name: any, options?: any): Promise; + readonly name: any; + readonly kind: any; + }; + }; + export const FileSystemWritableFileStream: { + new (underlyingSink?: UnderlyingSink, strategy?: QueuingStrategy): { + seek(position: any): Promise; + truncate(size: any): Promise; + write(data: any): Promise; + readonly locked: boolean; + abort(reason?: any): Promise; + close(): Promise; + getWriter(): WritableStreamDefaultWriter; + }; + }; + namespace _default { + export { createFileSystemWritableFileStream }; + export { createFileSystemDirectoryHandle }; + export { createFileSystemFileHandle }; + export { createFile }; + } + export default _default; + import fs from "socket:fs/promises"; +} + +declare module "socket:extension" { + /** + * Load an extension by name. + * @template {Record T} + * @param {string} name + * @param {ExtensionLoadOptions} [options] + * @return {Promise>} + */ + export function load>(name: string, options?: ExtensionLoadOptions): Promise>; + /** + * Provides current stats about the loaded extensions. + * @return {Promise} + */ + export function stats(): Promise; + /** + * @typedef {{ + * allow: string[] | string, + * imports?: object, + * type?: 'shared' | 'wasm32', + * path?: string, + * stats?: object, + * instance?: WebAssembly.Instance, + * adapter?: WebAssemblyExtensionAdapter + * }} ExtensionLoadOptions + */ + /** + * @typedef {{ abi: number, version: string, description: string }} ExtensionInfo + */ + /** + * @typedef {{ abi: number, loaded: number }} ExtensionStats */ /** * A interface for a native extension. * @template {Record T} */ - export class Extension> extends EventTarget { + export class Extension> extends EventTarget { + /** + * Load an extension by name. + * @template {Record T} + * @param {string} name + * @param {ExtensionLoadOptions} [options] + * @return {Promise>} + */ + static load>(name: string, options?: ExtensionLoadOptions): Promise>; + /** + * Query type of extension by name. + * @param {string} name + * @return {Promise<'shared'|'wasm32'|'unknown'|null>} + */ + static type(name: string): Promise<"shared" | "wasm32" | "unknown" | null>; + /** + * Provides current stats about the loaded extensions or one by name. + * @param {?string} name + * @return {Promise} + */ + static stats(name: string | null): Promise; + /** + * `Extension` class constructor. + * @param {string} name + * @param {ExtensionInfo} info + * @param {ExtensionLoadOptions} [options] + */ + constructor(name: string, info: ExtensionInfo, options?: ExtensionLoadOptions); + /** + * The name of the extension + * @type {string?} + */ + name: string | null; + /** + * The version of the extension + * @type {string?} + */ + version: string | null; + /** + * The description of the extension + * @type {string?} + */ + description: string | null; + /** + * The abi of the extension + * @type {number} + */ + abi: number; + /** + * @type {object} + */ + options: object; + /** + * @type {T} + */ + binding: T; + /** + * Not `null` if extension is of type 'wasm32' + * @type {?WebAssemblyExtensionAdapter} + */ + adapter: WebAssemblyExtensionAdapter | null; + /** + * `true` if the extension was loaded, otherwise `false` + * @type {boolean} + */ + get loaded(): boolean; + /** + * The extension type: 'shared' or 'wasm32' + * @type {'shared'|'wasm32'} + */ + get type(): "shared" | "wasm32"; + /** + * Unloads the loaded extension. + * @throws Error + */ + unload(): Promise; + instance: any; + [$type]: "shared" | "wasm32"; + [$loaded]: boolean; + } + namespace _default { + export { load }; + export { stats }; + } + export default _default; + export type Pointer = number; + export type ExtensionLoadOptions = { + allow: string[] | string; + imports?: object; + type?: "shared" | "wasm32"; + path?: string; + stats?: object; + instance?: WebAssembly.Instance; + adapter?: WebAssemblyExtensionAdapter; + }; + export type ExtensionInfo = { + abi: number; + version: string; + description: string; + }; + export type ExtensionStats = { + abi: number; + loaded: number; + }; + /** + * An adapter for reading and writing various values from a WebAssembly instance's + * memory buffer. + * @ignore + */ + class WebAssemblyExtensionAdapter { + constructor({ instance, module, table, memory, policies }: { + instance: any; + module: any; + table: any; + memory: any; + policies: any; + }); + view: any; + heap: any; + table: any; + stack: any; + buffer: any; + module: any; + memory: any; + context: any; + policies: any[]; + externalReferences: Map; + instance: any; + exitStatus: any; + textDecoder: TextDecoder; + textEncoder: TextEncoder; + errorMessagePointers: {}; + indirectFunctionTable: any; + get globalBaseOffset(): any; + destroy(): void; + init(): boolean; + get(pointer: any, size?: number): any; + set(pointer: any, value: any): void; + createExternalReferenceValue(value: any): any; + getExternalReferenceValue(pointer: any): any; + setExternalReferenceValue(pointer: any, value: any): Map; + removeExternalReferenceValue(pointer: any): void; + getExternalReferencePointer(value: any): any; + getFloat32(pointer: any): any; + setFloat32(pointer: any, value: any): boolean; + getFloat64(pointer: any): any; + setFloat64(pointer: any, value: any): boolean; + getInt8(pointer: any): any; + setInt8(pointer: any, value: any): boolean; + getInt16(pointer: any): any; + setInt16(pointer: any, value: any): boolean; + getInt32(pointer: any): any; + setInt32(pointer: any, value: any): boolean; + getUint8(pointer: any): any; + setUint8(pointer: any, value: any): boolean; + getUint16(pointer: any): any; + setUint16(pointer: any, value: any): boolean; + getUint32(pointer: any): any; + setUint32(pointer: any, value: any): boolean; + getString(pointer: any, buffer: any, size: any): string; + setString(pointer: any, string: any, buffer?: any): boolean; + } + const $type: unique symbol; + /** + * @typedef {number} Pointer + */ + const $loaded: unique symbol; +} + +declare module "socket:internal/database" { + /** + * A typed container for optional options given to the `Database` + * class constructor. + * + * @typedef {{ + * version?: string | undefined + * }} DatabaseOptions + */ + /** + * A typed container for various optional options made to a `get()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined, + * count?: number | undefined + * }} DatabaseGetOptions + */ + /** + * A typed container for various optional options made to a `put()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined, + * durability?: 'strict' | 'relaxed' | undefined + * }} DatabasePutOptions + */ + /** + * A typed container for various optional options made to a `delete()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined + * }} DatabaseDeleteOptions + */ + /** + * A typed container for optional options given to the `Database` + * class constructor. + * + * @typedef {{ + * offset?: number | undefined, + * backlog?: number | undefined + * }} DatabaseRequestQueueWaitOptions + */ + /** + * A typed container for various optional options made to a `entries()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined + * }} DatabaseEntriesOptions + */ + /** + * A `DatabaseRequestQueueRequestConflict` callback function type. + * @typedef {function(Event, DatabaseRequestQueueRequestConflict): any} DatabaseRequestQueueConflictResolutionCallback + */ + /** + * Waits for an event of `eventType` to be dispatched on a given `EventTarget`. + * @param {EventTarget} target + * @param {string} eventType + * @return {Promise} + */ + export function waitFor(target: EventTarget, eventType: string): Promise; + /** + * Creates an opens a named `Database` instance. + * @param {string} name + * @param {?DatabaseOptions | undefined} [options] + * @return {Promise} + */ + export function open(name: string, options?: (DatabaseOptions | undefined) | null): Promise; + /** + * Complete deletes a named `Database` instance. + * @param {string} name + * @param {?DatabaseOptions|undefined} [options] + */ + export function drop(name: string, options?: (DatabaseOptions | undefined) | null): Promise; + /** + * A mapping of named `Database` instances that are currently opened + * @type {Map>} + */ + export const opened: Map>; + /** + * A container for conflict resolution for a `DatabaseRequestQueue` instance + * `IDBRequest` instance. + */ + export class DatabaseRequestQueueRequestConflict { + /** + * `DatabaseRequestQueueRequestConflict` class constructor + * @param {function(any): void)} resolve + * @param {function(Error): void)} reject + * @param {function(): void)} cleanup + */ + constructor(resolve: any, reject: any, cleanup: any); + /** + * Called when a conflict is resolved. + * @param {any} argument + */ + resolve(argument?: any): void; + /** + * Called when a conflict is rejected + * @param {Error} error + */ + reject(error: Error): void; + #private; + } + /** + * An event dispatched on a `DatabaseRequestQueue` + */ + export class DatabaseRequestQueueEvent extends Event { + /** + * `DatabaseRequestQueueEvent` class constructor. + * @param {string} type + * @param {IDBRequest|IDBTransaction} request + */ + constructor(type: string, request: IDBRequest | IDBTransaction); + /** + * A reference to the underlying request for this event. + * @type {IDBRequest|IDBTransaction} + */ + get request(): IDBRequest | IDBTransaction; + #private; + } + /** + * An event dispatched on a `Database` + */ + export class DatabaseEvent extends Event { + /** + * `DatabaseEvent` class constructor. + * @param {string} type + * @param {Database} database + */ + constructor(type: string, database: Database); + /** + * A reference to the underlying database for this event. + * @type {Database} + */ + get database(): Database; + #private; + } + /** + * An error event dispatched on a `DatabaseRequestQueue` + */ + export class DatabaseRequestQueueErrorEvent extends ErrorEvent { + /** + * `DatabaseRequestQueueErrorEvent` class constructor. + * @param {string} type + * @param {IDBRequest|IDBTransaction} request + * @param {{ error: Error, cause?: Error }} options + */ + constructor(type: string, request: IDBRequest | IDBTransaction, options: { + error: Error; + cause?: Error; + }); + /** + * A reference to the underlying request for this error event. + * @type {IDBRequest|IDBTransaction} + */ + get request(): IDBRequest | IDBTransaction; + #private; + } + /** + * A container for various `IDBRequest` and `IDBTransaction` instances + * occurring during the life cycles of a `Database` instance. + */ + export class DatabaseRequestQueue extends EventTarget { + /** + * Computed queue length + * @type {number} + */ + get length(): number; + /** + * Pushes an `IDBRequest` or `IDBTransaction onto the queue and returns a + * `Promise` that resolves upon a 'success' or 'complete' event and rejects + * upon an error' event. + * @param {IDBRequest|IDBTransaction} + * @param {?DatabaseRequestQueueConflictResolutionCallback} [conflictResolutionCallback] + * @return {Promise} + */ + push(request: any, conflictResolutionCallback?: DatabaseRequestQueueConflictResolutionCallback | null): Promise; + /** + * Waits for all pending requests to complete. This function will throw when + * an `IDBRequest` or `IDBTransaction` instance emits an 'error' event. + * Callers of this function can optionally specify a maximum backlog to wait + * for instead of waiting for all requests to finish. + * @param {?DatabaseRequestQueueWaitOptions | undefined} [options] + */ + wait(options?: (DatabaseRequestQueueWaitOptions | undefined) | null): Promise; + #private; + } + /** + * An interface for reading from named databases backed by IndexedDB. + */ + export class Database extends EventTarget { + /** + * `Database` class constructor. + * @param {string} name + * @param {?DatabaseOptions | undefined} [options] + */ + constructor(name: string, options?: (DatabaseOptions | undefined) | null); + /** + * `true` if the `Database` is currently opening, otherwise `false`. + * A `Database` instance should not attempt to be opened if this property value + * is `true`. + * @type {boolean} + */ + get opening(): boolean; + /** + * `true` if the `Database` instance was successfully opened such that the + * internal `IDBDatabase` storage instance was created and can be referenced + * on the `Database` instance, otherwise `false`. + * @type {boolean} + */ + get opened(): boolean; + /** + * `true` if the `Database` instance was closed or has not been opened such + * that the internal `IDBDatabase` storage instance was not created or cannot + * be referenced on the `Database` instance, otherwise `false`. + * @type {boolean} + */ + get closed(): boolean; + /** + * `true` if the `Database` is currently closing, otherwise `false`. + * A `Database` instance should not attempt to be closed if this property value + * is `true`. + * @type {boolean} + */ + get closing(): boolean; + /** + * The name of the `IDBDatabase` database. This value cannot be `null`. + * @type {string} + */ + get name(): string; + /** + * The version of the `IDBDatabase` database. This value may be `null`. + * @type {?string} + */ + get version(): string; + /** + * A reference to the `IDBDatabase`, if the `Database` instance was opened. + * This value may ba `null`. + * @type {?IDBDatabase} + */ + get storage(): IDBDatabase; + /** + * Opens the `IDBDatabase` database optionally at a specific "version" if + * one was given upon construction of the `Database` instance. This function + * is not idempotent and will throw if the underlying `IDBDatabase` instance + * was created successfully or is in the process of opening. + * @return {Promise} + */ + open(): Promise; + /** + * Closes the `IDBDatabase` database storage, if opened. This function is not + * idempotent and will throw if the underlying `IDBDatabase` instance is + * already closed (not opened) or currently closing. + * @return {Promise} + */ + close(): Promise; + /** + * Deletes entire `Database` instance and closes after successfully + * delete storage. + */ + drop(): Promise; + /** + * Gets a "readonly" value by `key` in the `Database` object storage. + * @param {string} key + * @param {?DatabaseGetOptions|undefined} [options] + * @return {Promise} + */ + get(key: string, options?: (DatabaseGetOptions | undefined) | null): Promise; + /** + * Put a `value` at `key`, updating if it already exists, otherwise + * "inserting" it into the `Database` instance. + * @param {string} key + * @param {any} value + * @param {?DatabasePutOptions|undefined} [options] + * @return {Promise} + */ + put(key: string, value: any, options?: (DatabasePutOptions | undefined) | null): Promise; + /** + * Inserts a new `value` at `key`. This function throws if a value at `key` + * already exists. + * @param {string} key + * @param {any} value + * @param {?DatabasePutOptions|undefined} [options] + * @return {Promise} + */ + insert(key: string, value: any, options?: (DatabasePutOptions | undefined) | null): Promise; + /** + * Update a `value` at `key`, updating if it already exists, otherwise + * "inserting" it into the `Database` instance. + * @param {string} key + * @param {any} value + * @param {?DatabasePutOptions|undefined} [options] + * @return {Promise} + */ + update(key: string, value: any, options?: (DatabasePutOptions | undefined) | null): Promise; + /** + * Delete a value at `key`. + * @param {string} key + * @param {?DatabaseDeleteOptions|undefined} [options] + * @return {Promise} + */ + delete(key: string, options?: (DatabaseDeleteOptions | undefined) | null): Promise; + /** + * Gets a "readonly" value by `key` in the `Database` object storage. + * @param {string} key + * @param {?DatabaseEntriesOptions|undefined} [options] + * @return {Promise} + */ + entries(options?: (DatabaseEntriesOptions | undefined) | null): Promise; + #private; + } + namespace _default { + export { Database }; + export { open }; + export { drop }; + } + export default _default; + /** + * A typed container for optional options given to the `Database` + * class constructor. + */ + export type DatabaseOptions = { + version?: string | undefined; + }; + /** + * A typed container for various optional options made to a `get()` function + * on a `Database` instance. + */ + export type DatabaseGetOptions = { + store?: string | undefined; + stores?: string[] | undefined; + count?: number | undefined; + }; + /** + * A typed container for various optional options made to a `put()` function + * on a `Database` instance. + */ + export type DatabasePutOptions = { + store?: string | undefined; + stores?: string[] | undefined; + durability?: "strict" | "relaxed" | undefined; + }; + /** + * A typed container for various optional options made to a `delete()` function + * on a `Database` instance. + */ + export type DatabaseDeleteOptions = { + store?: string | undefined; + stores?: string[] | undefined; + }; + /** + * A typed container for optional options given to the `Database` + * class constructor. + */ + export type DatabaseRequestQueueWaitOptions = { + offset?: number | undefined; + backlog?: number | undefined; + }; + /** + * A typed container for various optional options made to a `entries()` function + * on a `Database` instance. + */ + export type DatabaseEntriesOptions = { + store?: string | undefined; + stores?: string[] | undefined; + }; + /** + * A `DatabaseRequestQueueRequestConflict` callback function type. + */ + export type DatabaseRequestQueueConflictResolutionCallback = (arg0: Event, arg1: DatabaseRequestQueueRequestConflict) => any; +} + +declare module "socket:service-worker/env" { + /** + * Opens an environment for a particular scope. + * @param {EnvironmentOptions} options + * @return {Promise} + */ + export function open(options: EnvironmentOptions): Promise; + /** + * Closes an active `Environment` instance, dropping the global + * instance reference. + * @return {Promise} + */ + export function close(): Promise; + /** + * Resets an active `Environment` instance + * @return {Promise} + */ + export function reset(): Promise; + /** + * @typedef {{ + * scope: string + * }} EnvironmentOptions + */ + /** + * An event dispatched when a environment value is updated (set, delete) + */ + export class EnvironmentEvent extends Event { + /** + * `EnvironmentEvent` class constructor. + * @param {'set'|'delete'} type + * @param {object=} [entry] + */ + constructor(type: "set" | "delete", entry?: object | undefined); + entry: any; + } + /** + * An environment context object with persistence and durability + * for service worker environments. + */ + export class Environment extends EventTarget { + /** + * Maximum entries that will be restored from storage into the environment + * context object. + * @type {number} + */ + static MAX_CONTEXT_ENTRIES: number; + /** + * Opens an environment for a particular scope. + * @param {EnvironmentOptions} options + * @return {Environment} + */ + static open(options: EnvironmentOptions): Environment; + /** + * The current `Environment` instance + * @type {Environment?} + */ + static instance: Environment | null; + /** + * `Environment` class constructor + * @ignore + * @param {EnvironmentOptions} options + */ + constructor(options: EnvironmentOptions); + /** + * A reference to the currently opened environment database. + * @type {import('../internal/database.js').Database} + */ + get database(): import("socket:internal/database").Database; + /** + * A proxied object for reading and writing environment state. + * Values written to this object must be cloneable with respect to the + * structured clone algorithm. + * @see {https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm} + * @type {Proxy} + */ + get context(): ProxyConstructor; + /** + * The environment type + * @type {string} + */ + get type(): string; + /** + * The current environment name. This value is also used as the + * internal database name. + * @type {string} + */ + get name(): string; + /** + * Resets the current environment to an empty state. + */ + reset(): Promise; + /** + * Opens the environment. + * @ignore + */ + open(): Promise; + /** + * Closes the environment database, purging existing state. + * @ignore + */ + close(): Promise; + #private; + } + namespace _default { + export { Environment }; + export { close }; + export { reset }; + export { open }; + } + export default _default; + export type EnvironmentOptions = { + scope: string; + }; +} + +declare module "socket:service-worker/debug" { + export function debug(...args: any[]): void; + export default debug; +} + +declare module "socket:service-worker/state" { + export const channel: BroadcastChannel; + export const state: any; + export default state; +} + +declare module "socket:service-worker/clients" { + export class Client { + constructor(options: any); + get id(): any; + get url(): any; + get type(): any; + get frameType(): any; + postMessage(message: any, optionsOrTransferables?: any): void; + #private; + } + export class WindowClient extends Client { + get focused(): boolean; + get ancestorOrigins(): any[]; + get visibilityState(): string; + focus(): Promise; + navigate(url: any): Promise; + #private; + } + export class Clients { + get(id: any): Promise; + matchAll(options?: any): Promise; + openWindow(url: any, options?: any): Promise; + claim(): Promise; + } + const _default: Clients; + export default _default; +} + +declare module "socket:service-worker/context" { + /** + * A context given to `ExtendableEvent` interfaces and provided to + * simplified service worker modules + */ + export class Context { + /** + * `Context` class constructor. + * @param {import('./events.js').ExtendableEvent} event + */ + constructor(event: import("socket:service-worker/events").ExtendableEvent); + /** + * Context data. This may be a custom protocol handler scheme data + * by default, if available. + * @type {any?} + */ + data: any | null; + /** + * The `ExtendableEvent` for this `Context` instance. + * @type {ExtendableEvent} + */ + get event(): ExtendableEvent; + /** + * An environment context object. + * @type {object?} + */ + get env(): any; + /** + * Resets the current environment context. + * @return {Promise} + */ + resetEnvironment(): Promise; + /** + * Unused, but exists for cloudflare compat. + * @ignore + */ + passThroughOnException(): void; + /** + * Tells the event dispatcher that work is ongoing. + * It can also be used to detect whether that work was successful. + * @param {Promise} promise + */ + waitUntil(promise: Promise): Promise; + /** + * TODO + */ + handled(): Promise; + /** + * Gets the client for this event context. + * @return {Promise} + */ + client(): Promise; + #private; + } + namespace _default { + export { Context }; + } + export default _default; +} + +declare module "socket:service-worker/events" { + export const textEncoder: TextEncoderStream; + export const FETCH_EVENT_TIMEOUT: number; + export const FETCH_EVENT_MAX_RESPONSE_REDIRECTS: number; + /** + * The `ExtendableEvent` interface extends the lifetime of the "install" and + * "activate" events dispatched on the global scope as part of the service + * worker lifecycle. + */ + export class ExtendableEvent extends Event { + /** + * `ExtendableEvent` class constructor. + * @ignore + */ + constructor(...args: any[]); + /** + * A context for this `ExtendableEvent` instance. + * @type {import('./context.js').Context} + */ + get context(): Context; + /** + * A promise that can be awaited which waits for this `ExtendableEvent` + * instance no longer has pending promises. + * @type {Promise} + */ + get awaiting(): Promise; + /** + * The number of pending promises + * @type {number} + */ + get pendingPromises(): number; + /** + * `true` if the `ExtendableEvent` instance is considered "active", + * otherwise `false`. + * @type {boolean} + */ + get isActive(): boolean; + /** + * Tells the event dispatcher that work is ongoing. + * It can also be used to detect whether that work was successful. + * @param {Promise} promise + */ + waitUntil(promise: Promise): void; + /** + * Returns a promise that this `ExtendableEvent` instance is waiting for. + * @return {Promise} + */ + waitsFor(): Promise; + #private; + } + /** + * This is the event type for "fetch" events dispatched on the service worker + * global scope. It contains information about the fetch, including the + * request and how the receiver will treat the response. + */ + export class FetchEvent extends ExtendableEvent { + static defaultHeaders: Headers; + /** + * `FetchEvent` class constructor. + * @ignore + * @param {string=} [type = 'fetch'] + * @param {object=} [options] + */ + constructor(type?: string | undefined, options?: object | undefined); + /** + * The handled property of the `FetchEvent` interface returns a promise + * indicating if the event has been handled by the fetch algorithm or not. + * This property allows executing code after the browser has consumed a + * response, and is usually used together with the `waitUntil()` method. + * @type {Promise} + */ + get handled(): Promise; + /** + * The request read-only property of the `FetchEvent` interface returns the + * `Request` that triggered the event handler. + * @type {Request} + */ + get request(): Request; + /** + * The `clientId` read-only property of the `FetchEvent` interface returns + * the id of the Client that the current service worker is controlling. + * @type {string} + */ + get clientId(): string; + /** + * @ignore + * @type {string} + */ + get resultingClientId(): string; + /** + * @ignore + * @type {string} + */ + get replacesClientId(): string; + /** + * @ignore + * @type {boolean} + */ + get isReload(): boolean; + /** + * @ignore + * @type {Promise} + */ + get preloadResponse(): Promise; + /** + * The `respondWith()` method of `FetchEvent` prevents the webview's + * default fetch handling, and allows you to provide a promise for a + * `Response` yourself. + * @param {Response|Promise} response + */ + respondWith(response: Response | Promise): void; + #private; + } + export class ExtendableMessageEvent extends ExtendableEvent { + /** + * `ExtendableMessageEvent` class constructor. + * @param {string=} [type = 'message'] + * @param {object=} [options] + */ + constructor(type?: string | undefined, options?: object | undefined); + /** + * @type {any} + */ + get data(): any; + /** + * @type {MessagePort[]} + */ + get ports(): MessagePort[]; + /** + * @type {import('./clients.js').Client?} + */ + get source(): import("socket:service-worker/clients").Client; + /** + * @type {string?} + */ + get origin(): string; + /** + * @type {string} + */ + get lastEventId(): string; + #private; + } + export class NotificationEvent extends ExtendableEvent { + constructor(type: any, options: any); + get action(): string; + get notification(): any; + #private; + } + namespace _default { + export { ExtendableMessageEvent }; + export { ExtendableEvent }; + export { FetchEvent }; + } + export default _default; + import { Context } from "socket:service-worker/context"; +} + +declare module "socket:http/adapters" { + /** + * @typedef {{ + * Connection: typeof import('../http.js').Connection, + * globalAgent: import('../http.js').Agent, + * IncomingMessage: typeof import('../http.js').IncomingMessage, + * ServerResponse: typeof import('../http.js').ServerResponse, + * STATUS_CODES: object, + * METHODS: string[] + * }} HTTPModuleInterface + */ + /** + * An abstract base clase for a HTTP server adapter. + */ + export class ServerAdapter extends EventTarget { + /** + * `ServerAdapter` class constructor. + * @ignore + * @param {import('../http.js').Server} server + * @param {HTTPModuleInterface} httpInterface + */ + constructor(server: import("socket:http").Server, httpInterface: HTTPModuleInterface); + /** + * A readonly reference to the underlying HTTP(S) server + * for this adapter. + * @type {import('../http.js').Server} + */ + get server(): import("socket:http").Server; + /** + * A readonly reference to the underlying HTTP(S) module interface + * for creating various HTTP module class objects. + * @type {HTTPModuleInterface} + */ + get httpInterface(): HTTPModuleInterface; + /** + * A readonly reference to the `AsyncContext.Variable` associated with this + * `ServerAdapter` instance. + */ + get context(): import("socket:async/context").Variable; + /** + * Called when the adapter should destroy itself. + * @abstract + */ + destroy(): Promise; + #private; + } + /** + * A HTTP adapter for running a HTTP server in a service worker that uses the + * "fetch" event for the request and response lifecycle. + */ + export class ServiceWorkerServerAdapter extends ServerAdapter { + /** + * Handles the 'install' service worker event. + * @ignore + * @param {import('../service-worker/events.js').ExtendableEvent} event + */ + onInstall(event: import("socket:service-worker/events").ExtendableEvent): Promise; + /** + * Handles the 'activate' service worker event. + * @ignore + * @param {import('../service-worker/events.js').ExtendableEvent} event + */ + onActivate(event: import("socket:service-worker/events").ExtendableEvent): Promise; + /** + * Handles the 'fetch' service worker event. + * @ignore + * @param {import('../service-worker/events.js').FetchEvent} + */ + onFetch(event: any): Promise; + } + namespace _default { + export { ServerAdapter }; + export { ServiceWorkerServerAdapter }; + } + export default _default; + export type HTTPModuleInterface = { + Connection: typeof import("socket:http").Connection; + globalAgent: import("socket:http").Agent; + IncomingMessage: typeof import("socket:http").IncomingMessage; + ServerResponse: typeof import("socket:http").ServerResponse; + STATUS_CODES: object; + METHODS: string[]; + }; +} + +declare module "socket:http" { + /** + * Makes a HTTP or `socket:` GET request. A simplified alias to `request()`. + * @param {string|object} optionsOrURL + * @param {(object|function)=} [options] + * @param {function=} [callback] + * @return {ClientRequest} + */ + export function get(optionsOrURL: string | object, options?: (object | Function) | undefined, callback?: Function | undefined): ClientRequest; + /** + * Creates a HTTP server that can listen for incoming requests. + * Requests that are dispatched to this server depend on the context + * in which it is created, such as a service worker which will use a + * "fetch event" adapter. + * @param {object|function=} [options] + * @param {function=} [callback] + * @return {Server} + */ + export function createServer(options?: (object | Function) | undefined, callback?: Function | undefined): Server; + /** + * All known possible HTTP methods. + * @type {string[]} + */ + export const METHODS: string[]; + /** + * A mapping of status codes to status texts + * @type {object} + */ + export const STATUS_CODES: object; + export const CONTINUE: 100; + export const SWITCHING_PROTOCOLS: 101; + export const PROCESSING: 102; + export const EARLY_HINTS: 103; + export const OK: 200; + export const CREATED: 201; + export const ACCEPTED: 202; + export const NONAUTHORITATIVE_INFORMATION: 203; + export const NO_CONTENT: 204; + export const RESET_CONTENT: 205; + export const PARTIAL_CONTENT: 206; + export const MULTISTATUS: 207; + export const ALREADY_REPORTED: 208; + export const IM_USED: 226; + export const MULTIPLE_CHOICES: 300; + export const MOVED_PERMANENTLY: 301; + export const FOUND: 302; + export const SEE_OTHER: 303; + export const NOT_MODIFIED: 304; + export const USE_PROXY: 305; + export const TEMPORARY_REDIRECT: 307; + export const PERMANENT_REDIRECT: 308; + export const BAD_REQUEST: 400; + export const UNAUTHORIZED: 401; + export const PAYMENT_REQUIRED: 402; + export const FORBIDDEN: 403; + export const NOT_FOUND: 404; + export const METHOD_NOT_ALLOWED: 405; + export const NOT_ACCEPTABLE: 406; + export const PROXY_AUTHENTICATION_REQUIRED: 407; + export const REQUEST_TIMEOUT: 408; + export const CONFLICT: 409; + export const GONE: 410; + export const LENGTH_REQUIRED: 411; + export const PRECONDITION_FAILED: 412; + export const PAYLOAD_TOO_LARGE: 413; + export const URI_TOO_LONG: 414; + export const UNSUPPORTED_MEDIA_TYPE: 415; + export const RANGE_NOT_SATISFIABLE: 416; + export const EXPECTATION_FAILED: 417; + export const IM_A_TEAPOT: 418; + export const MISDIRECTED_REQUEST: 421; + export const UNPROCESSABLE_ENTITY: 422; + export const LOCKED: 423; + export const FAILED_DEPENDENCY: 424; + export const TOO_EARLY: 425; + export const UPGRADE_REQUIRED: 426; + export const PRECONDITION_REQUIRED: 428; + export const TOO_MANY_REQUESTS: 429; + export const REQUEST_HEADER_FIELDS_TOO_LARGE: 431; + export const UNAVAILABLE_FOR_LEGAL_REASONS: 451; + export const INTERNAL_SERVER_ERROR: 500; + export const NOT_IMPLEMENTED: 501; + export const BAD_GATEWAY: 502; + export const SERVICE_UNAVAILABLE: 503; + export const GATEWAY_TIMEOUT: 504; + export const HTTP_VERSION_NOT_SUPPORTED: 505; + export const VARIANT_ALSO_NEGOTIATES: 506; + export const INSUFFICIENT_STORAGE: 507; + export const LOOP_DETECTED: 508; + export const BANDWIDTH_LIMIT_EXCEEDED: 509; + export const NOT_EXTENDED: 510; + export const NETWORK_AUTHENTICATION_REQUIRED: 511; + /** + * The parent class of `ClientRequest` and `ServerResponse`. + * It is an abstract outgoing message from the perspective of the + * participants of an HTTP transaction. + * @see {@link https://nodejs.org/api/http.html#class-httpoutgoingmessage} + */ + export class OutgoingMessage extends Writable { + /** + * `OutgoingMessage` class constructor. + * @ignore + */ + constructor(); + /** + * `true` if the headers were sent + * @type {boolean} + */ + headersSent: boolean; + /** + * Internal buffers + * @ignore + * @type {Buffer[]} + */ + get buffers(): Buffer[]; + /** + * An object of the outgoing message headers. + * This is equivalent to `getHeaders()` + * @type {object} + */ + get headers(): any; + /** + * @ignore + */ + get socket(): this; + /** + * `true` if the write state is "ended" + * @type {boolean} + */ + get writableEnded(): boolean; + /** + * `true` if the write state is "finished" + * @type {boolean} + */ + get writableFinished(): boolean; + /** + * The number of buffered bytes. + * @type {number} + */ + get writableLength(): number; + /** + * @ignore + * @type {boolean} + */ + get writableObjectMode(): boolean; + /** + * @ignore + */ + get writableCorked(): number; + /** + * The `highWaterMark` of the writable stream. + * @type {number} + */ + get writableHighWaterMark(): number; + /** + * @ignore + * @return {OutgoingMessage} + */ + addTrailers(headers: any): OutgoingMessage; + /** + * @ignore + * @return {OutgoingMessage} + */ + cork(): OutgoingMessage; + /** + * @ignore + * @return {OutgoingMessage} + */ + uncork(): OutgoingMessage; + /** + * Destroys the message. + * Once a socket is associated with the message and is connected, + * that socket will be destroyed as well. + * @param {Error?} [err] + * @return {OutgoingMessage} + */ + destroy(err?: Error | null): OutgoingMessage; + /** + * Finishes the outgoing message. + * @param {(Buffer|Uint8Array|string|function)=} [chunk] + * @param {(string|function)=} [encoding] + * @param {function=} [callback] + * @return {OutgoingMessage} + */ + end(chunk?: (Buffer | Uint8Array | string | Function) | undefined, encoding?: (string | Function) | undefined, callback?: Function | undefined): OutgoingMessage; + /** + * Append a single header value for the header object. + * @param {string} name + * @param {string|string[]} value + * @return {OutgoingMessage} + */ + appendHeader(name: string, value: string | string[]): OutgoingMessage; + /** + * Append a single header value for the header object. + * @param {string} name + * @param {string} value + * @return {OutgoingMessage} + */ + setHeader(name: string, value: string): OutgoingMessage; + /** + * Flushes the message headers. + */ + flushHeaders(): void; + /** + * Gets the value of the HTTP header with the given name. + * If that header is not set, the returned value will be `undefined`. + * @param {string} + * @return {string|undefined} + */ + getHeader(name: any): string | undefined; + /** + * Returns an array containing the unique names of the current outgoing + * headers. All names are lowercase. + * @return {string[]} + */ + getHeaderNames(): string[]; + /** + * @ignore + */ + getRawHeaderNames(): string[]; + /** + * Returns a copy of the HTTP headers as an object. + * @return {object} + */ + getHeaders(): object; + /** + * Returns true if the header identified by name is currently set in the + * outgoing headers. The header name is case-insensitive. + * @param {string} name + * @return {boolean} + */ + hasHeader(name: string): boolean; + /** + * Removes a header that is queued for implicit sending. + * @param {string} name + */ + removeHeader(name: string): void; + /** + * Sets the outgoing message timeout with an optional callback. + * @param {number} timeout + * @param {function=} [callback] + * @return {OutgoingMessage} + */ + setTimeout(timeout: number, callback?: Function | undefined): OutgoingMessage; + /** + * @ignore + */ + _implicitHeader(): void; + #private; + } + /** + * An `IncomingMessage` object is created by `Server` or `ClientRequest` and + * passed as the first argument to the 'request' and 'response' event + * respectively. + * It may be used to access response status, headers, and data. + * @see {@link https://nodejs.org/api/http.html#class-httpincomingmessage} + */ + export class IncomingMessage extends Readable { + /** + * `IncomingMessage` class constructor. + * @ignore + * @param {object} options + */ + constructor(options: object); + set url(url: string); + /** + * The URL for this incoming message. This value is not absolute with + * respect to the protocol and hostname. It includes the path and search + * query component parameters. + * @type {string} + */ + get url(): string; + /** + * @type {Server} + */ + get server(): exports.Server; + /** + * @type {AsyncContext.Variable} + */ + get context(): typeof import("socket:async/context").Variable; + /** + * This property will be `true` if a complete HTTP message has been received + * and successfully parsed. + * @type {boolean} + */ + get complete(): boolean; + /** + * An object of the incoming message headers. + * @type {object} + */ + get headers(): any; + /** + * Similar to `message.headers`, but there is no join logic and the values + * are always arrays of strings, even for headers received just once. + * @type {object} + */ + get headersDistinct(): any; + /** + * The HTTP major version of this request. + * @type {number} + */ + get httpVersionMajor(): number; + /** + * The HTTP minor version of this request. + * @type {number} + */ + get httpVersionMinor(): number; + /** + * The HTTP version string. + * A concatenation of `httpVersionMajor` and `httpVersionMinor`. + * @type {string} + */ + get httpVersion(): string; + /** + * The HTTP request method. + * @type {string} + */ + get method(): string; + /** + * The raw request/response headers list potentially as they were received. + * @type {string[]} + */ + get rawHeaders(): string[]; + /** + * @ignore + */ + get rawTrailers(): any[]; + /** + * @ignore + */ + get socket(): this; + /** + * The HTTP request status code. + * Only valid for response obtained from `ClientRequest`. + * @type {number} + */ + get statusCode(): number; + /** + * The HTTP response status message (reason phrase). + * Such as "OK" or "Internal Server Error." + * Only valid for response obtained from `ClientRequest`. + * @type {string?} + */ + get statusMessage(): string; + /** + * An alias for `statusCode` + * @type {number} + */ + get status(): number; + /** + * An alias for `statusMessage` + * @type {string?} + */ + get statusText(): string; + /** + * @ignore + */ + get trailers(): {}; + /** + * @ignore + */ + get trailersDistinct(): {}; + /** + * Gets the value of the HTTP header with the given name. + * If that header is not set, the returned value will be `undefined`. + * @param {string} + * @return {string|undefined} + */ + getHeader(name: any): string | undefined; + /** + * Returns an array containing the unique names of the current outgoing + * headers. All names are lowercase. + * @return {string[]} + */ + getHeaderNames(): string[]; + /** + * @ignore + */ + getRawHeaderNames(): string[]; + /** + * Returns a copy of the HTTP headers as an object. + * @return {object} + */ + getHeaders(): object; + /** + * Returns true if the header identified by name is currently set in the + * outgoing headers. The header name is case-insensitive. + * @param {string} name + * @return {boolean} + */ + hasHeader(name: string): boolean; + /** + * Sets the incoming message timeout with an optional callback. + * @param {number} timeout + * @param {function=} [callback] + * @return {IncomingMessage} + */ + setTimeout(timeout: number, callback?: Function | undefined): IncomingMessage; + #private; + } + /** + * An object that is created internally and returned from `request()`. + * @see {@link https://nodejs.org/api/http.html#class-httpclientrequest} + */ + export class ClientRequest extends exports.OutgoingMessage { + /** + * `ClientRequest` class constructor. + * @ignore + * @param {object} options + */ + constructor(options: object); + /** + * The HTTP request method. + * @type {string} + */ + get method(): string; + /** + * The request protocol + * @type {string?} + */ + get protocol(): string; + /** + * The request path. + * @type {string} + */ + get path(): string; + /** + * The request host name (including port). + * @type {string?} + */ + get host(): string; + /** + * The URL for this outgoing message. This value is not absolute with + * respect to the protocol and hostname. It includes the path and search + * query component parameters. + * @type {string} + */ + get url(): string; + /** + * @ignore + * @type {boolean} + */ + get finished(): boolean; + /** + * @ignore + * @type {boolean} + */ + get reusedSocket(): boolean; + /** + * @ignore + * @param {boolean=} [value] + * @return {ClientRequest} + */ + setNoDelay(value?: boolean | undefined): ClientRequest; + /** + * @ignore + * @param {boolean=} [enable] + * @param {number=} [initialDelay] + * @return {ClientRequest} + */ + setSocketKeepAlive(enable?: boolean | undefined, initialDelay?: number | undefined): ClientRequest; + #private; + } + /** + * An object that is created internally by a `Server` instance, not by the user. + * It is passed as the second parameter to the 'request' event. + * @see {@link https://nodejs.org/api/http.html#class-httpserverresponse} + */ + export class ServerResponse extends exports.OutgoingMessage { + /** + * `ServerResponse` class constructor. + * @param {object} options + */ + constructor(options: object); + /** + * @type {Server} + */ + get server(): exports.Server; + /** + * A reference to the original HTTP request object. + * @type {IncomingMessage} + */ + get request(): exports.IncomingMessage; + /** + * A reference to the original HTTP request object. + * @type {IncomingMessage} + */ + get req(): exports.IncomingMessage; + set statusCode(statusCode: number); + /** + * The HTTP request status code. + * Only valid for response obtained from `ClientRequest`. + * @type {number} + */ + get statusCode(): number; + set statusMessage(statusMessage: string); + /** + * The HTTP response status message (reason phrase). + * Such as "OK" or "Internal Server Error." + * Only valid for response obtained from `ClientRequest`. + * @type {string?} + */ + get statusMessage(): string; + set status(status: number); + /** + * An alias for `statusCode` + * @type {number} + */ + get status(): number; + set statusText(statusText: string); + /** + * An alias for `statusMessage` + * @type {string?} + */ + get statusText(): string; + set sendDate(value: boolean); + /** + * If `true`, the "Date" header will be automatically generated and sent in + * the response if it is not already present in the headers. + * Defaults to `true`. + * @type {boolean} + */ + get sendDate(): boolean; + /** + * @ignore + */ + writeContinue(): this; + /** + * @ignore + */ + writeEarlyHints(): this; + /** + * @ignore + */ + writeProcessing(): this; + /** + * Writes the response header to the request. + * The `statusCode` is a 3-digit HTTP status code, like 200 or 404. + * The last argument, `headers`, are the response headers. + * Optionally one can give a human-readable `statusMessage` + * as the second argument. + * @param {number|string} statusCode + * @param {string|object|string[]} [statusMessage] + * @param {object|string[]} [headers] + * @return {ClientRequest} + */ + writeHead(statusCode: number | string, statusMessage?: string | object | string[], headers?: object | string[]): ClientRequest; + #private; + } + /** + * An options object container for an `Agent` instance. + */ + export class AgentOptions { + /** + * `AgentOptions` class constructor. + * @ignore + * @param {{ + * keepAlive?: boolean, + * timeout?: number + * }} [options] + */ + constructor(options?: { + keepAlive?: boolean; + timeout?: number; + }); + keepAlive: boolean; + timeout: number; + } + /** + * An Agent is responsible for managing connection persistence + * and reuse for HTTP clients. + * @see {@link https://nodejs.org/api/http.html#class-httpagent} + */ + export class Agent extends EventEmitter { + /** + * `Agent` class constructor. + * @param {AgentOptions=} [options] + */ + constructor(options?: AgentOptions | undefined); + defaultProtocol: string; + options: any; + requests: Set; + sockets: {}; + maxFreeSockets: number; + maxTotalSockets: number; + maxSockets: number; + /** + * @ignore + */ + get freeSockets(): {}; + /** + * @ignore + * @param {object} options + */ + getName(options: object): string; + /** + * Produces a socket/stream to be used for HTTP requests. + * @param {object} options + * @param {function(Duplex)=} [callback] + * @return {Duplex} + */ + createConnection(options: object, callback?: ((arg0: Duplex) => any) | undefined): Duplex; + /** + * @ignore + */ + keepSocketAlive(): void; + /** + * @ignore + */ + reuseSocket(): void; + /** + * @ignore + */ + destroy(): void; + } + /** + * The global and default HTTP agent. + * @type {Agent} + */ + export const globalAgent: Agent; + /** + * A duplex stream between a HTTP request `IncomingMessage` and the + * response `ServerResponse` + */ + export class Connection extends Duplex { + /** + * `Connection` class constructor. + * @ignore + * @param {Server} server + * @param {IncomingMessage} incomingMessage + * @param {ServerResponse} serverResponse + */ + constructor(server: Server, incomingMessage: IncomingMessage, serverResponse: ServerResponse); + server: any; + active: boolean; + request: any; + response: any; + /** + * Closes the connection, destroying the underlying duplex, request, and + * response streams. + * @return {Connection} + */ + close(): Connection; + } + /** + * A nodejs compat HTTP server typically intended for running in a "worker" + * environment. + * @see {@link https://nodejs.org/api/http.html#class-httpserver} + */ + export class Server extends EventEmitter { + requestTimeout: number; + timeout: number; + maxRequestsPerSocket: number; + keepAliveTimeout: number; + headersTimeout: number; + /** + * @ignore + * @type {AsyncResource} + */ + get resource(): AsyncResource; + /** + * The adapter interface for this `Server` instance. + * @ignore + */ + get adapterInterace(): { + Connection: typeof exports.Connection; + globalAgent: exports.Agent; + IncomingMessage: typeof exports.IncomingMessage; + METHODS: string[]; + ServerResponse: typeof exports.ServerResponse; + STATUS_CODES: any; + }; + /** + * `true` if the server is closed, otherwise `false`. + * @type {boolean} + */ + get closed(): boolean; + /** + * The host to listen to. This value can be `null`. + * Defaults to `location.hostname`. This value + * is used to filter requests by hostname. + * @type {string?} + */ + get host(): string; + /** + * The `port` to listen on. This value can be `0`, which is the default. + * This value is used to filter requests by port, if given. A port value + * of `0` does not filter on any port. + * @type {number} + */ + get port(): number; + /** + * A readonly array of all active or inactive (idle) connections. + * @type {Connection[]} + */ + get connections(): exports.Connection[]; + /** + * `true` if the server is listening for requests. + * @type {boolean} + */ + get listening(): boolean; + set maxConnections(value: number); + /** + * The number of concurrent max connections this server should handle. + * Default: Infinity + * @type {number} + */ + get maxConnections(): number; + /** + * Gets the HTTP server address and port that it this server is + * listening (emulated) on in the runtime with respect to the + * adapter internal being used by the server. + * @return {{ family: string, address: string, port: number}} + */ + address(): { + family: string; + address: string; + port: number; + }; + /** + * Closes the server. + * @param {function=} [close] + */ + close(callback?: any): void; + /** + * Closes all connections. + */ + closeAllConnections(): void; + /** + * Closes all idle connections. + */ + closeIdleConnections(): void; + /** + * @ignore + */ + setTimeout(timeout?: number, callback?: any): this; + /** + * @param {number|object=} [port] + * @param {string=} [host] + * @param {function|null} [unused] + * @param {function=} [callback + * @return Server + */ + listen(port?: (number | object) | undefined, host?: string | undefined, unused?: Function | null, callback?: Function | undefined): this; + #private; + } + export default exports; + import { Writable } from "socket:stream"; + import { Buffer } from "socket:buffer"; + import { Readable } from "socket:stream"; + import { EventEmitter } from "socket:events"; + import { Duplex } from "socket:stream"; + import { AsyncResource } from "socket:async/resource"; + import * as exports from "socket:http"; + +} + +declare module "socket:https" { + /** + * Makes a HTTPS request, optionally a `socket://` for relative paths when + * `socket:` is the origin protocol. + * @param {string|object} optionsOrURL + * @param {(object|function)=} [options] + * @param {function=} [callback] + * @return {ClientRequest} + */ + export function request(optionsOrURL: string | object, options?: (object | Function) | undefined, callback?: Function | undefined): ClientRequest; + /** + * Makes a HTTPS or `socket:` GET request. A simplified alias to `request()`. + * @param {string|object} optionsOrURL + * @param {(object|function)=} [options] + * @param {function=} [callback] + * @return {ClientRequest} + */ + export function get(optionsOrURL: string | object, options?: (object | Function) | undefined, callback?: Function | undefined): ClientRequest; + /** + * Creates a HTTPS server that can listen for incoming requests. + * Requests that are dispatched to this server depend on the context + * in which it is created, such as a service worker which will use a + * "fetch event" adapter. + * @param {object|function=} [options] + * @param {function=} [callback] + * @return {Server} + */ + export function createServer(...args: any[]): Server; + export const CONTINUE: 100; + export const SWITCHING_PROTOCOLS: 101; + export const PROCESSING: 102; + export const EARLY_HINTS: 103; + export const OK: 200; + export const CREATED: 201; + export const ACCEPTED: 202; + export const NONAUTHORITATIVE_INFORMATION: 203; + export const NO_CONTENT: 204; + export const RESET_CONTENT: 205; + export const PARTIAL_CONTENT: 206; + export const MULTISTATUS: 207; + export const ALREADY_REPORTED: 208; + export const IM_USED: 226; + export const MULTIPLE_CHOICES: 300; + export const MOVED_PERMANENTLY: 301; + export const FOUND: 302; + export const SEE_OTHER: 303; + export const NOT_MODIFIED: 304; + export const USE_PROXY: 305; + export const TEMPORARY_REDIRECT: 307; + export const PERMANENT_REDIRECT: 308; + export const BAD_REQUEST: 400; + export const UNAUTHORIZED: 401; + export const PAYMENT_REQUIRED: 402; + export const FORBIDDEN: 403; + export const NOT_FOUND: 404; + export const METHOD_NOT_ALLOWED: 405; + export const NOT_ACCEPTABLE: 406; + export const PROXY_AUTHENTICATION_REQUIRED: 407; + export const REQUEST_TIMEOUT: 408; + export const CONFLICT: 409; + export const GONE: 410; + export const LENGTH_REQUIRED: 411; + export const PRECONDITION_FAILED: 412; + export const PAYLOAD_TOO_LARGE: 413; + export const URI_TOO_LONG: 414; + export const UNSUPPORTED_MEDIA_TYPE: 415; + export const RANGE_NOT_SATISFIABLE: 416; + export const EXPECTATION_FAILED: 417; + export const IM_A_TEAPOT: 418; + export const MISDIRECTED_REQUEST: 421; + export const UNPROCESSABLE_ENTITY: 422; + export const LOCKED: 423; + export const FAILED_DEPENDENCY: 424; + export const TOO_EARLY: 425; + export const UPGRADE_REQUIRED: 426; + export const PRECONDITION_REQUIRED: 428; + export const TOO_MANY_REQUESTS: 429; + export const REQUEST_HEADER_FIELDS_TOO_LARGE: 431; + export const UNAVAILABLE_FOR_LEGAL_REASONS: 451; + export const INTERNAL_SERVER_ERROR: 500; + export const NOT_IMPLEMENTED: 501; + export const BAD_GATEWAY: 502; + export const SERVICE_UNAVAILABLE: 503; + export const GATEWAY_TIMEOUT: 504; + export const HTTP_VERSION_NOT_SUPPORTED: 505; + export const VARIANT_ALSO_NEGOTIATES: 506; + export const INSUFFICIENT_STORAGE: 507; + export const LOOP_DETECTED: 508; + export const BANDWIDTH_LIMIT_EXCEEDED: 509; + export const NOT_EXTENDED: 510; + export const NETWORK_AUTHENTICATION_REQUIRED: 511; + /** + * All known possible HTTP methods. + * @type {string[]} + */ + export const METHODS: string[]; + /** + * A mapping of status codes to status texts + * @type {object} + */ + export const STATUS_CODES: object; + /** + * An options object container for an `Agent` instance. + */ + export class AgentOptions extends http.AgentOptions { + } + /** + * An Agent is responsible for managing connection persistence + * and reuse for HTTPS clients. + * @see {@link https://nodejs.org/api/https.html#class-httpsagent} + */ + export class Agent extends http.Agent { + } + /** + * An object that is created internally and returned from `request()`. + * @see {@link https://nodejs.org/api/http.html#class-httpclientrequest} + */ + export class ClientRequest extends http.ClientRequest { + } + /** + * The parent class of `ClientRequest` and `ServerResponse`. + * It is an abstract outgoing message from the perspective of the + * participants of an HTTP transaction. + * @see {@link https://nodejs.org/api/http.html#class-httpoutgoingmessage} + */ + export class OutgoingMessage extends http.OutgoingMessage { + } + /** + * An `IncomingMessage` object is created by `Server` or `ClientRequest` and + * passed as the first argument to the 'request' and 'response' event + * respectively. + * It may be used to access response status, headers, and data. + * @see {@link https://nodejs.org/api/http.html#class-httpincomingmessage} + */ + export class IncomingMessage extends http.IncomingMessage { + } + /** + * An object that is created internally by a `Server` instance, not by the user. + * It is passed as the second parameter to the 'request' event. + * @see {@link https://nodejs.org/api/http.html#class-httpserverresponse} + */ + export class ServerResponse extends http.ServerResponse { + } + /** + * A duplex stream between a HTTP request `IncomingMessage` and the + * response `ServerResponse` + */ + export class Connection extends http.Connection { + } + /** + * A nodejs compat HTTP server typically intended for running in a "worker" + * environment. + * @see {@link https://nodejs.org/api/http.html#class-httpserver} + */ + export class Server extends http.Server { + } + /** + * The global and default HTTPS agent. + * @type {Agent} + */ + export const globalAgent: Agent; + export default exports; + import http from "socket:http"; + import * as exports from "socket:http"; +} + +declare module "socket:enumeration" { + /** + * @module enumeration + * This module provides a data structure for enumerated unique values. + */ + /** + * A container for enumerated values. + */ + export class Enumeration extends Set { + /** + * Creates an `Enumeration` instance from arguments. + * @param {...any} values + * @return {Enumeration} + */ + static from(...values: any[]): Enumeration; + /** + * `Enumeration` class constructor. + * @param {any[]} values + * @param {object=} [options = {}] + * @param {number=} [options.start = 0] + */ + constructor(values: any[], options?: object | undefined); + /** + * @type {number} + */ + get length(): number; + /** + * Returns `true` if enumeration contains `value`. An alias + * for `Set.prototype.has`. + * @return {boolean} + */ + contains(value: any): boolean; + /** + * @ignore + */ + add(): void; + /** + * @ignore + */ + delete(): void; + /** + * JSON represenation of a `Enumeration` instance. + * @ignore + * @return {string[]} + */ + toJSON(): string[]; + /** + * Internal inspect function. + * @ignore + * @return {LanguageQueryResult} + */ + inspect(): LanguageQueryResult; + } + export default Enumeration; +} + +declare module "socket:language" { + /** + * Look up a language name or code by query. + * @param {string} query + * @param {object=} [options] + * @param {boolean=} [options.strict = false] + * @return {?LanguageQueryResult[]} + */ + export function lookup(query: string, options?: object | undefined, ...args: any[]): LanguageQueryResult[] | null; + /** + * Describe a language by tag + * @param {string} query + * @param {object=} [options] + * @param {boolean=} [options.strict = true] + * @return {?LanguageDescription[]} + */ + export function describe(query: string, options?: object | undefined): LanguageDescription[] | null; + /** + * A list of ISO 639-1 language names. + * @type {string[]} + */ + export const names: string[]; + /** + * A list of ISO 639-1 language codes. + * @type {string[]} + */ + export const codes: string[]; + /** + * A list of RFC 5646 language tag identifiers. + * @see {@link http://tools.ietf.org/html/rfc5646} + */ + export const tags: Enumeration; + /** + * A list of RFC 5646 language tag titles corresponding + * to language tags. + * @see {@link http://tools.ietf.org/html/rfc5646} + */ + export const descriptions: Enumeration; + /** + * A container for a language query response containing an ISO language + * name and code. + * @see {@link https://www.sitepoint.com/iso-2-letter-language-codes} + */ + export class LanguageQueryResult { + /** + * `LanguageQueryResult` class constructor. + * @param {string} code + * @param {string} name + * @param {string[]} [tags] + */ + constructor(code: string, name: string, tags?: string[]); + /** + * The language code corresponding to the query. + * @type {string} + */ + get code(): string; + /** + * The language name corresponding to the query. + * @type {string} + */ + get name(): string; + /** + * The language tags corresponding to the query. + * @type {string[]} + */ + get tags(): string[]; + /** + * JSON represenation of a `LanguageQueryResult` instance. + * @return {{ + * code: string, + * name: string, + * tags: string[] + * }} + */ + toJSON(): { + code: string; + name: string; + tags: string[]; + }; + /** + * Internal inspect function. + * @ignore + * @return {LanguageQueryResult} + */ + inspect(): LanguageQueryResult; + #private; + } + /** + * A container for a language code, tag, and description. + */ + export class LanguageDescription { + /** + * `LanguageDescription` class constructor. + * @param {string} code + * @param {string} tag + * @param {string} description + */ + constructor(code: string, tag: string, description: string); + /** + * The language code corresponding to the language + * @type {string} + */ + get code(): string; + /** + * The language tag corresponding to the language. + * @type {string} + */ + get tag(): string; + /** + * The language description corresponding to the language. + * @type {string} + */ + get description(): string; + /** + * JSON represenation of a `LanguageDescription` instance. + * @return {{ + * code: string, + * tag: string, + * description: string + * }} + */ + toJSON(): { + code: string; + tag: string; + description: string; + }; + /** + * Internal inspect function. + * @ignore + * @return {LanguageDescription} + */ + inspect(): LanguageDescription; + #private; + } + namespace _default { + export { codes }; + export { describe }; + export { lookup }; + export { names }; + export { tags }; + } + export default _default; + import Enumeration from "socket:enumeration"; +} + +declare module "socket:latica/packets" { + /** + * The magic bytes prefixing every packet. They are the + * 2nd, 3rd, 5th, and 7th, prime numbers. + * @type {number[]} + */ + export const MAGIC_BYTES_PREFIX: number[]; + /** + * The version of the protocol. + */ + export const VERSION: 6; + /** + * The size in bytes of the prefix magic bytes. + */ + export const MAGIC_BYTES: 4; + /** + * The maximum size of the user message. + */ + export const MESSAGE_BYTES: 1024; + /** + * The cache TTL in milliseconds. + */ + export const CACHE_TTL: number; + export namespace PACKET_SPEC { + namespace type { + let bytes: number; + let encoding: string; + } + namespace version { + let bytes_1: number; + export { bytes_1 as bytes }; + let encoding_1: string; + export { encoding_1 as encoding }; + export { VERSION as default }; + } + namespace clock { + let bytes_2: number; + export { bytes_2 as bytes }; + let encoding_2: string; + export { encoding_2 as encoding }; + let _default: number; + export { _default as default }; + } + namespace hops { + let bytes_3: number; + export { bytes_3 as bytes }; + let encoding_3: string; + export { encoding_3 as encoding }; + let _default_1: number; + export { _default_1 as default }; + } + namespace index { + let bytes_4: number; + export { bytes_4 as bytes }; + let encoding_4: string; + export { encoding_4 as encoding }; + let _default_2: number; + export { _default_2 as default }; + export let signed: boolean; + } + namespace ttl { + let bytes_5: number; + export { bytes_5 as bytes }; + let encoding_5: string; + export { encoding_5 as encoding }; + export { CACHE_TTL as default }; + } + namespace clusterId { + let bytes_6: number; + export { bytes_6 as bytes }; + let encoding_6: string; + export { encoding_6 as encoding }; + let _default_3: number[]; + export { _default_3 as default }; + } + namespace subclusterId { + let bytes_7: number; + export { bytes_7 as bytes }; + let encoding_7: string; + export { encoding_7 as encoding }; + let _default_4: number[]; + export { _default_4 as default }; + } + namespace previousId { + let bytes_8: number; + export { bytes_8 as bytes }; + let encoding_8: string; + export { encoding_8 as encoding }; + let _default_5: number[]; + export { _default_5 as default }; + } + namespace packetId { + let bytes_9: number; + export { bytes_9 as bytes }; + let encoding_9: string; + export { encoding_9 as encoding }; + let _default_6: number[]; + export { _default_6 as default }; + } + namespace nextId { + let bytes_10: number; + export { bytes_10 as bytes }; + let encoding_10: string; + export { encoding_10 as encoding }; + let _default_7: number[]; + export { _default_7 as default }; + } + namespace usr1 { + let bytes_11: number; + export { bytes_11 as bytes }; + let _default_8: number[]; + export { _default_8 as default }; + } + namespace usr2 { + let bytes_12: number; + export { bytes_12 as bytes }; + let _default_9: number[]; + export { _default_9 as default }; + } + namespace usr3 { + let bytes_13: number; + export { bytes_13 as bytes }; + let _default_10: number[]; + export { _default_10 as default }; + } + namespace usr4 { + let bytes_14: number; + export { bytes_14 as bytes }; + let _default_11: number[]; + export { _default_11 as default }; + } + namespace message { + let bytes_15: number; + export { bytes_15 as bytes }; + let _default_12: number[]; + export { _default_12 as default }; + } + namespace sig { + let bytes_16: number; + export { bytes_16 as bytes }; + let _default_13: number[]; + export { _default_13 as default }; + } + } + /** + * The size in bytes of the total packet frame and message. + */ + export const PACKET_BYTES: number; + /** + * The maximum distance that a packet can be replicated. + */ + export const MAX_HOPS: 16; + export function validateMessage(o: object, constraints: { + [key: string]: constraint; + }): void; + /** + * Computes a SHA-256 hash of input returning a hex encoded string. + * @type {function(string|Buffer|Uint8Array): Promise} + */ + export const sha256: (arg0: string | Buffer | Uint8Array) => Promise; + export function decode(buf: Buffer): Packet; + export function getTypeFromBytes(buf: any): any; + export class Packet { + static ttl: number; + static maxLength: number; + /** + * Returns an empty `Packet` instance. + * @return {Packet} + */ + static empty(): Packet; + /** + * @param {Packet|object} packet + * @return {Packet} + */ + static from(packet: Packet | object): Packet; + /** + * Determines if input is a packet. + * @param {Buffer|Uint8Array|number[]|object|Packet} packet + * @return {boolean} + */ + static isPacket(packet: Buffer | Uint8Array | number[] | object | Packet): boolean; + /** + */ + static encode(p: any): Promise; + static decode(buf: any): Packet; + /** + * `Packet` class constructor. + * @param {Packet|object?} options + */ + constructor(options?: Packet | (object | null)); + /** + * @param {Packet} packet + * @return {Packet} + */ + copy(): Packet; + timestamp: any; + isComposed: any; + isReconciled: any; + meta: any; + } + export class PacketPing extends Packet { + static type: number; + } + export class PacketPong extends Packet { + static type: number; + } + export class PacketIntro extends Packet { + static type: number; + } + export class PacketJoin extends Packet { + static type: number; + } + export class PacketPublish extends Packet { + static type: number; + } + export class PacketStream extends Packet { + static type: number; + } + export class PacketSync extends Packet { + static type: number; + } + export class PacketQuery extends Packet { + static type: number; + } + export default Packet; + export type constraint = { + type: string; + required?: boolean; + /** + * optional validator fn returning boolean + */ + assert?: Function; + }; + import { Buffer } from "socket:buffer"; +} + +declare module "socket:latica/encryption" { + /** + * Class for handling encryption and key management. + */ + export class Encryption { + /** + * Creates a shared key based on the provided seed or generates a random one. + * @param {Uint8Array|string} seed - Seed for key generation. + * @returns {Promise} - Shared key. + */ + static createSharedKey(seed: Uint8Array | string): Promise; + /** + * Creates a key pair for signing and verification. + * @param {Uint8Array|string} seed - Seed for key generation. + * @returns {Promise<{ publicKey: Uint8Array, privateKey: Uint8Array }>} - Key pair. + */ + static createKeyPair(seed: Uint8Array | string): Promise<{ + publicKey: Uint8Array; + privateKey: Uint8Array; + }>; + /** + * Creates an ID using SHA-256 hash. + * @param {string} str - String to hash. + * @returns {Promise} - SHA-256 hash. + */ + static createId(str: string): Promise; + /** + * Creates a cluster ID using SHA-256 hash with specified output size. + * @param {string} str - String to hash. + * @returns {Promise} - SHA-256 hash with specified output size. + */ + static createClusterId(str: string): Promise; /** - * Load an extension by name. - * @template {Record T} - * @param {string} name - * @param {ExtensionLoadOptions} [options] - * @return {Promise>} + * Signs a message using the given secret key. + * @param {Buffer} b - The message to sign. + * @param {Uint8Array} sk - The secret key to use. + * @returns {Uint8Array} - Signature. */ - static load>(name: string, options?: ExtensionLoadOptions): Promise>; + static sign(b: Buffer, sk: Uint8Array): Uint8Array; /** - * Query type of extension by name. - * @param {string} name - * @return {Promise<'shared'|'wasm32'|'unknown'|null>} + * Verifies the signature of a message using the given public key. + * @param {Buffer} b - The message to verify. + * @param {Uint8Array} sig - The signature to check. + * @param {Uint8Array} pk - The public key to use. + * @returns {number} - Returns non-zero if the buffer could not be verified. */ - static type(name: string): Promise<'shared' | 'wasm32' | 'unknown' | null>; + static verify(b: Buffer, sig: Uint8Array, pk: Uint8Array): number; /** - * Provides current stats about the loaded extensions or one by name. - * @param {?string} name - * @return {Promise} + * Mapping of public keys to key objects. + * @type {Object.} */ - static stats(name: string | null): Promise; + keys: { + [x: string]: { + publicKey: Uint8Array; + privateKey: Uint8Array; + ts: number; + }; + }; /** - * `Extension` class constructor. - * @param {string} name - * @param {ExtensionInfo} info - * @param {ExtensionLoadOptions} [options] + * Adds a key pair to the keys mapping. + * @param {Uint8Array|string} publicKey - Public key. + * @param {Uint8Array} privateKey - Private key. */ - constructor(name: string, info: ExtensionInfo, options?: ExtensionLoadOptions); + add(publicKey: Uint8Array | string, privateKey: Uint8Array): void; /** - * The name of the extension - * @type {string?} + * Removes a key from the keys mapping. + * @param {Uint8Array|string} publicKey - Public key. */ - name: string | null; + remove(publicKey: Uint8Array | string): void; /** - * The version of the extension - * @type {string?} + * Checks if a key is in the keys mapping. + * @param {Uint8Array|string} to - Public key or Uint8Array. + * @returns {boolean} - True if the key is present, false otherwise. */ - version: string | null; + has(to: Uint8Array | string): boolean; /** - * The description of the extension - * @type {string?} + * Opens a sealed message using the specified key. + * @param {Buffer} message - The sealed message. + * @param {Object|string} v - Key object or public key. + * @returns {Buffer} - Decrypted message. + * @throws {Error} - Throws ENOKEY if the key is not found. */ - description: string | null; + openUnsigned(message: Buffer, v: any | string): Buffer; + sealUnsigned(message: any, v: any): any; /** - * The abi of the extension + * Decrypts a sealed and signed message for a specific receiver. + * @param {Buffer} message - The sealed message. + * @param {Object|string} v - Key object or public key. + * @returns {Buffer} - Decrypted message. + * @throws {Error} - Throws ENOKEY if the key is not found, EMALFORMED if the message is malformed, ENOTVERIFIED if the message cannot be verified. + */ + open(message: Buffer, v: any | string): Buffer; + /** + * Seals and signs a message for a specific receiver using their public key. + * + * `Seal(message, receiver)` performs an _encrypt-sign-encrypt_ (ESE) on + * a plaintext `message` for a `receiver` identity. This prevents repudiation + * attacks and doesn't rely on packet chain guarantees. + * + * let ct = Seal(sender | pt, receiver) + * let sig = Sign(ct, sk) + * let out = Seal(sig | ct) + * + * In an setup between Alice & Bob, this means: + * - Only Bob sees the plaintext + * - Alice wrote the plaintext and the ciphertext + * - Only Bob can see that Alice wrote the plaintext and ciphertext + * - Bob cannot forward the message without invalidating Alice's signature. + * - The outer encryption serves to prevent an attacker from replacing Alice's + * signature. As with _sign-encrypt-sign (SES), ESE is a variant of + * including the recipient's name inside the plaintext, which is then signed + * and encrypted Alice signs her plaintext along with her ciphertext, so as + * to protect herself from a laintext-substitution attack. At the same time, + * Alice's signed plaintext gives Bob non-repudiation. + * + * @see https://theworld.com/~dtd/sign_encrypt/sign_encrypt7.html + * + * @param {Buffer} message - The message to seal. + * @param {Object|string} v - Key object or public key. + * @returns {Buffer} - Sealed message. + * @throws {Error} - Throws ENOKEY if the key is not found. + */ + seal(message: Buffer, v: any | string): Buffer; + } + import Buffer from "socket:buffer"; +} + +declare module "socket:latica/cache" { + /** + * @typedef {Packet} CacheEntry + * @typedef {function(CacheEntry, CacheEntry): number} CacheEntrySiblingResolver + */ + /** + * Default cache sibling resolver that computes a delta between + * two entries clocks. + * @param {CacheEntry} a + * @param {CacheEntry} b + * @return {number} + */ + export function defaultSiblingResolver(a: CacheEntry, b: CacheEntry): number; + /** + * Default max size of a `Cache` instance. + */ + export const DEFAULT_MAX_SIZE: number; + /** + * Internal mapping of packet IDs to packet data used by `Cache`. + */ + export class CacheData extends Map { + constructor(); + constructor(entries?: readonly (readonly [any, any])[]); + constructor(); + constructor(iterable?: Iterable); + } + /** + * A class for storing a cache of packets by ID. This class includes a scheme + * for reconciling disjointed packet caches in a large distributed system. The + * following are key design characteristics. + * + * Space Efficiency: This scheme can be space-efficient because it summarizes + * the cache's contents in a compact binary format. By sharing these summaries, + * two computers can quickly determine whether their caches have common data or + * differences. + * + * Bandwidth Efficiency: Sharing summaries instead of the full data can save + * bandwidth. If the differences between the caches are small, sharing summaries + * allows for more efficient data synchronization. + * + * Time Efficiency: The time efficiency of this scheme depends on the size of + * the cache and the differences between the two caches. Generating summaries + * and comparing them can be faster than transferring and comparing the entire + * dataset, especially for large caches. + * + * Complexity: The scheme introduces some complexity due to the need to encode + * and decode summaries. In some cases, the overhead introduced by this + * complexity might outweigh the benefits, especially if the caches are + * relatively small. In this case, you should be using a query. + * + * Data Synchronization Needs: The efficiency also depends on the data + * synchronization needs. If the data needs to be synchronized in real-time, + * this scheme might not be suitable. It's more appropriate for cases where + * periodic or batch synchronization is acceptable. + * + * Scalability: The scheme's efficiency can vary depending on the scalability + * of the system. As the number of cache entries or computers involved + * increases, the complexity of generating and comparing summaries will stay + * bound to a maximum of 16Mb. + * + */ + export class Cache { + static HASH_SIZE_BYTES: number; + static HASH_EMPTY: string; + /** + * The encodeSummary method provides a compact binary encoding of the output + * of summary() + * + * @param {Object} summary - the output of calling summary() + * @return {Buffer} + **/ + static encodeSummary(summary: any): Buffer; + /** + * The decodeSummary method decodes the output of encodeSummary() + * + * @param {Buffer} bin - the output of calling encodeSummary() + * @return {Object} summary + **/ + static decodeSummary(bin: Buffer): any; + /** + * Test a summary hash format is valid + * + * @param {string} hash + * @returns boolean + */ + static isValidSummaryHashFormat(hash: string): boolean; + /** + * `Cache` class constructor. + * @param {CacheData?} [data] + */ + constructor(data?: CacheData | null, siblingResolver?: typeof defaultSiblingResolver); + data: CacheData; + maxSize: number; + siblingResolver: typeof defaultSiblingResolver; + /** + * Readonly count of the number of cache entries. * @type {number} */ - abi: number; + get size(): number; /** - * @type {object} + * Readonly size of the cache in bytes. + * @type {number} */ - options: object; + get bytes(): number; /** - * @type {T} + * Inserts a `CacheEntry` value `v` into the cache at key `k`. + * @param {string} k + * @param {CacheEntry} v + * @return {boolean} */ - binding: T; + insert(k: string, v: CacheEntry): boolean; /** - * Not `null` if extension is of type 'wasm32' - * @type {?WebAssemblyExtensionAdapter} + * Gets a `CacheEntry` value at key `k`. + * @param {string} k + * @return {CacheEntry?} */ - adapter: WebAssemblyExtensionAdapter | null; + get(k: string): CacheEntry | null; /** - * `true` if the extension was loaded, otherwise `false` - * @type {boolean} + * @param {string} k + * @return {boolean} */ - get loaded(): boolean; + delete(k: string): boolean; /** - * The extension type: 'shared' or 'wasm32' - * @type {'shared'|'wasm32'} + * Predicate to determine if cache contains an entry at key `k`. + * @param {string} k + * @return {boolean} */ - get type(): "shared" | "wasm32"; + has(k: string): boolean; /** - * Unloads the loaded extension. - * @throws Error + * Composes an indexed packet into a new `Packet` + * @param {Packet} packet + */ + compose(packet: Packet, source?: CacheData): Promise; + sha1(value: any, toHex: any): Promise; + /** + * + * The summarize method returns a terse yet comparable summary of the cache + * contents. + * + * Think of the cache as a trie of hex characters, the summary returns a + * checksum for the current level of the trie and for its 16 children. + * + * This is similar to a merkel tree as equal subtrees can easily be detected + * without the need for further recursion. When the subtree checksums are + * inequivalent then further negotiation at lower levels may be required, this + * process continues until the two trees become synchonized. + * + * When the prefix is empty, the summary will return an array of 16 checksums + * these checksums provide a way of comparing that subtree with other peers. + * + * When a variable-length hexidecimal prefix is provided, then only cache + * member hashes sharing this prefix will be considered. + * + * For each hex character provided in the prefix, the trie will decend by one + * level, each level divides the 2^128 address space by 16. For exmaple... + * + * ``` + * Level 0 1 2 + * ---------------- + * 2b00 + * aa0e ━┓ ━┓ + * aa1b ┃ ┃ + * aae3 ┃ ┃ ━┓ + * aaea ┃ ┃ ┃ + * aaeb ┃ ━┛ ━┛ + * ab00 ┃ ━┓ + * ab1e ┃ ┃ + * ab2a ┃ ┃ + * abef ┃ ┃ + * abf0 ━┛ ━┛ + * bff9 + * ``` + * + * @param {string} prefix - a string of lowercased hexidecimal characters + * @return {Object} + * */ - unload(): Promise; - instance: any; - [$type]: "shared" | "wasm32"; - [$loaded]: boolean; - } - namespace _default { - export { load }; - export { stats }; + summarize(prefix?: string, predicate?: (o: any) => boolean): any; } - export default _default; - export type ExtensionLoadOptions = { - allow: string[] | string; - imports?: object; - type?: 'shared' | 'wasm32'; - path?: string; - stats?: object; - instance?: WebAssembly.Instance; - adapter?: WebAssemblyExtensionAdapter; - }; - export type ExtensionInfo = { - abi: number; - version: string; - description: string; - }; - export type ExtensionStats = { - abi: number; - loaded: number; - }; + export default Cache; + export type CacheEntry = Packet; + export type CacheEntrySiblingResolver = (arg0: CacheEntry, arg1: CacheEntry) => number; + import { Packet } from "socket:latica/packets"; + import { Buffer } from "socket:buffer"; +} + +declare module "socket:latica/nat" { /** - * An adapter for reading and writing various values from a WebAssembly instance's - * memory buffer. - * @ignore + * The NAT type is encoded using 5 bits: + * + * 0b00001 : the lsb indicates if endpoint dependence information is included + * 0b00010 : the second bit indicates the endpoint dependence value + * + * 0b00100 : the third bit indicates if firewall information is included + * 0b01000 : the fourth bit describes which requests can pass the firewall, only known IPs (0) or any IP (1) + * 0b10000 : the fifth bit describes which requests can pass the firewall, only known ports (0) or any port (1) */ - class WebAssemblyExtensionAdapter { - constructor({ instance, module, table, memory, policies }: { - instance: any; - module: any; - table: any; - memory: any; - policies: any; - }); - view: any; - heap: any; - table: any; - stack: any; - buffer: any; - module: any; - memory: any; - context: any; - policies: any[]; - externalReferences: Map; - instance: any; - exitStatus: any; - textDecoder: TextDecoder; - textEncoder: TextEncoder; - errorMessagePointers: {}; - indirectFunctionTable: any; - get globalBaseOffset(): any; - destroy(): void; - init(): boolean; - get(pointer: any, size?: number): any; - set(pointer: any, value: any): void; - createExternalReferenceValue(value: any): any; - getExternalReferenceValue(pointer: any): any; - setExternalReferenceValue(pointer: any, value: any): Map; - removeExternalReferenceValue(pointer: any): void; - getExternalReferencePointer(value: any): any; - getFloat32(pointer: any): any; - setFloat32(pointer: any, value: any): boolean; - getFloat64(pointer: any): any; - setFloat64(pointer: any, value: any): boolean; - getInt8(pointer: any): any; - setInt8(pointer: any, value: any): boolean; - getInt16(pointer: any): any; - setInt16(pointer: any, value: any): boolean; - getInt32(pointer: any): any; - setInt32(pointer: any, value: any): boolean; - getUint8(pointer: any): any; - setUint8(pointer: any, value: any): boolean; - getUint16(pointer: any): any; - setUint16(pointer: any, value: any): boolean; - getUint32(pointer: any): any; - setUint32(pointer: any, value: any): boolean; - getString(pointer: any, buffer: any, size: any): string; - setString(pointer: any, string: any, buffer?: any): boolean; - } - const $type: unique symbol; /** - * {Pointer} + * Every remote will see the same IP:PORT mapping for this peer. + * + * :3333 ┌──────┐ + * :1111 ┌───▶ │ R1 │ + * ┌──────┐ ┌───────┐ │ └──────┘ + * │ P1 ├───▶│ NAT ├──┤ + * └──────┘ └───────┘ │ ┌──────┐ + * └───▶ │ R2 │ + * :3333 └──────┘ + */ + export const MAPPING_ENDPOINT_INDEPENDENT: 3; + /** + * Every remote will see a different IP:PORT mapping for this peer. + * + * :4444 ┌──────┐ + * :1111 ┌───▶ │ R1 │ + * ┌──────┐ ┌───────┐ │ └──────┘ + * │ P1 ├───▶│ NAT ├──┤ + * └──────┘ └───────┘ │ ┌──────┐ + * └───▶ │ R2 │ + * :5555 └──────┘ */ - type $loaded = number; + export const MAPPING_ENDPOINT_DEPENDENT: 1; /** - * @typedef {number} {Pointer} + * The firewall allows the port mapping to be accessed by: + * - Any IP:PORT combination (FIREWALL_ALLOW_ANY) + * - Any PORT on a previously connected IP (FIREWALL_ALLOW_KNOWN_IP) + * - Only from previously connected IP:PORT combinations (FIREWALL_ALLOW_KNOWN_IP_AND_PORT) */ - const $loaded: unique symbol; - import path from "socket:path"; -} -declare module "socket:fetch/fetch" { - export class DOMException { - private constructor(); - } - export function Headers(headers: any): void; - export class Headers { - constructor(headers: any); - map: {}; - append(name: any, value: any): void; - delete(name: any): void; - get(name: any): any; - has(name: any): boolean; - set(name: any, value: any): void; - forEach(callback: any, thisArg: any): void; - keys(): { - next: () => { - done: boolean; - value: any; - }; - }; - values(): { - next: () => { - done: boolean; - value: any; - }; - }; - entries(): { - next: () => { - done: boolean; - value: any; - }; - }; - } - export function Request(input: any, options: any): void; - export class Request { - constructor(input: any, options: any); - url: string; - credentials: any; - headers: Headers; - method: any; - mode: any; - signal: any; - referrer: any; - clone(): Request; - } - export function Response(bodyInit: any, options: any): void; - export class Response { - constructor(bodyInit: any, options: any); - type: string; - status: any; - ok: boolean; - statusText: string; - headers: Headers; - url: any; - clone(): Response; - } - export namespace Response { - function error(): Response; - function redirect(url: any, status: any): Response; - } - export function fetch(input: any, init: any): Promise; - export namespace fetch { - let polyfill: boolean; - } -} -declare module "socket:fetch/index" { - export * from "socket:fetch/fetch"; - export default fetch; - import { fetch } from "socket:fetch/fetch"; -} -declare module "socket:fetch" { - export * from "socket:fetch/index"; - export default fetch; - import fetch from "socket:fetch/index"; + export const FIREWALL_ALLOW_ANY: 28; + export const FIREWALL_ALLOW_KNOWN_IP: 12; + export const FIREWALL_ALLOW_KNOWN_IP_AND_PORT: 4; + /** + * The initial state of the nat is unknown and its value is 0 + */ + export const UNKNOWN: 0; + /** + * Full-cone NAT, also known as one-to-one NAT + * + * Any external host can send packets to iAddr:iPort by sending packets to eAddr:ePort. + * + * @summary its a packet party at this mapping and everyone's invited + */ + export const UNRESTRICTED: number; + /** + * (Address)-restricted-cone NAT + * + * An external host (hAddr:any) can send packets to iAddr:iPort by sending packets to eAddr:ePort only + * if iAddr:iPort has previously sent a packet to hAddr:any. "Any" means the port number doesn't matter. + * + * @summary The NAT will drop your packets unless a peer within its network has previously messaged you from *any* port. + */ + export const ADDR_RESTRICTED: number; + /** + * Port-restricted cone NAT + * + * An external host (hAddr:hPort) can send packets to iAddr:iPort by sending + * packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to + * hAddr:hPort. + * + * @summary The NAT will drop your packets unless a peer within its network + * has previously messaged you from this *specific* port. + */ + export const PORT_RESTRICTED: number; + /** + * Symmetric NAT + * + * Only an external host that receives a packet from an internal host can send + * a packet back. + * + * @summary The NAT will only accept replies to a correspondence initialized + * by itself, the mapping it created is only valid for you. + */ + export const ENDPOINT_RESTRICTED: number; + export function isEndpointDependenceDefined(nat: any): boolean; + export function isFirewallDefined(nat: any): boolean; + export function isValid(nat: any): boolean; + export function toString(n: any): "UNRESTRICTED" | "ADDR_RESTRICTED" | "PORT_RESTRICTED" | "ENDPOINT_RESTRICTED" | "UNKNOWN"; + export function toStringStrategy(n: any): "STRATEGY_DEFER" | "STRATEGY_DIRECT_CONNECT" | "STRATEGY_TRAVERSAL_OPEN" | "STRATEGY_TRAVERSAL_CONNECT" | "STRATEGY_PROXY" | "STRATEGY_UNKNOWN"; + export const STRATEGY_DEFER: 0; + export const STRATEGY_DIRECT_CONNECT: 1; + export const STRATEGY_TRAVERSAL_OPEN: 2; + export const STRATEGY_TRAVERSAL_CONNECT: 3; + export const STRATEGY_PROXY: 4; + export function connectionStrategy(a: any, b: any): 0 | 1 | 2 | 3 | 4; } -declare module "socket:language" { + +declare module "socket:latica/index" { /** - * Look up a language name or code by query. - * @param {string} query - * @param {object=} [options] - * @param {boolean=} [options.strict = false] - * @return {?LanguageQueryResult[]} + * Computes rate limit predicate value for a port and address pair for a given + * threshold updating an input rates map. This method is accessed concurrently, + * the rates object makes operations atomic to avoid race conditions. + * + * @param {Map} rates + * @param {number} type + * @param {number} port + * @param {string} address + * @return {boolean} */ - export function lookup(query: string, options?: object | undefined, ...args: any[]): LanguageQueryResult[] | null; + export function rateLimit(rates: Map, type: number, port: number, address: string, subclusterIdQuota: any): boolean; /** - * Describe a language by tag - * @param {string} query - * @param {object=} [options] - * @param {boolean=} [options.strict = true] - * @return {?LanguageDescription[]} + * Retry delay in milliseconds for ping. + * @type {number} */ - export function describe(query: string, options?: object | undefined): LanguageDescription[] | null; + export const PING_RETRY: number; /** - * A list of ISO 639-1 language names. - * @type {string[]} + * Probe wait timeout in milliseconds. + * @type {number} */ - export const names: string[]; + export const PROBE_WAIT: number; /** - * A list of ISO 639-1 language codes. - * @type {string[]} + * Default keep alive timeout. + * @type {number} */ - export const codes: string[]; + export const DEFAULT_KEEP_ALIVE: number; /** - * A list of RFC 5646 language tag identifiers. - * @see {@link http://tools.ietf.org/html/rfc5646} + * Default rate limit threshold in milliseconds. + * @type {number} */ - export const tags: Enumeration; + export const DEFAULT_RATE_LIMIT_THRESHOLD: number; + export function getRandomPort(ports: object, p: number | null): number; /** - * A list of RFC 5646 language tag titles corresponding - * to language tags. - * @see {@link http://tools.ietf.org/html/rfc5646} + * A `RemotePeer` represents an initial, discovered, or connected remote peer. + * Typically, you will not need to create instances of this class directly. */ - export const descriptions: Enumeration; + export class RemotePeer { + /** + * `RemotePeer` class constructor. + * @param {{ + * peerId?: string, + * address?: string, + * port?: number, + * natType?: number, + * clusters: object, + * reflectionId?: string, + * distance?: number, + * publicKey?: string, + * privateKey?: string, + * clock?: number, + * lastUpdate?: number, + * lastRequest?: number + * }} o + */ + constructor(o: { + peerId?: string; + address?: string; + port?: number; + natType?: number; + clusters: object; + reflectionId?: string; + distance?: number; + publicKey?: string; + privateKey?: string; + clock?: number; + lastUpdate?: number; + lastRequest?: number; + }, peer: any); + peerId: any; + address: any; + port: number; + natType: any; + clusters: {}; + pingId: any; + distance: number; + connected: boolean; + opening: number; + probed: number; + proxy: any; + clock: number; + uptime: number; + lastUpdate: number; + lastRequest: number; + localPeer: any; + write(sharedKey: any, args: any): Promise; + } /** - * A container for a language query response containing an ISO language - * name and code. - * @see {@link https://www.sitepoint.com/iso-2-letter-language-codes} + * `Peer` class factory. + * @param {{ createSocket: function('udp4', null, object?): object }} options */ - export class LanguageQueryResult { + export class Peer { /** - * `LanguageQueryResult` class constructor. - * @param {string} code - * @param {string} name - * @param {string[]} [tags] + * Test a peerID is valid + * + * @param {string} pid + * @returns boolean */ - constructor(code: string, name: string, tags?: string[]); + static isValidPeerId(pid: string): boolean; /** - * The language code corresponding to the query. - * @type {string} + * Test a reflectionID is valid + * + * @param {string} rid + * @returns boolean */ - get code(): string; + static isValidReflectionId(rid: string): boolean; /** - * The language name corresponding to the query. - * @type {string} + * Test a pingID is valid + * + * @param {string} pid + * @returns boolean */ - get name(): string; + static isValidPingId(pid: string): boolean; /** - * The language tags corresponding to the query. - * @type {string[]} + * Returns the online status of the browser, else true. + * + * note: globalThis.navigator was added to node in v22. + * + * @returns boolean + */ + static onLine(): boolean; + /** + * `Peer` class constructor. + * @param {object=} opts - Options + * @param {Buffer} opts.peerId - A 32 byte buffer (ie, `Encryption.createId()`). + * @param {Buffer} opts.clusterId - A 32 byte buffer (ie, `Encryption.createClusterId()`). + * @param {number=} opts.port - A port number. + * @param {number=} opts.probeInternalPort - An internal port number (semi-private for testing). + * @param {number=} opts.probeExternalPort - An external port number (semi-private for testing). + * @param {number=} opts.natType - A nat type. + * @param {string=} opts.address - An ipv4 address. + * @param {number=} opts.keepalive - The interval of the main loop. + * @param {function=} opts.siblingResolver - A function that can be used to determine canonical data in case two packets have concurrent clock values. + * @param {object} dgram - A nodejs compatible implementation of the dgram module (sans multicast). + */ + constructor(persistedState: {}, dgram: object); + port: any; + address: any; + natType: number; + nextNatType: number; + clusters: {}; + syncs: {}; + reflectionId: any; + reflectionTimeout: any; + reflectionStage: number; + reflectionRetry: number; + reflectionFirstResponder: any; + peerId: string; + isListening: boolean; + ctime: number; + lastUpdate: number; + lastSync: number; + closing: boolean; + clock: number; + unpublished: {}; + cache: any; + uptime: number; + maxHops: number; + bdpCache: number[]; + dgram: any; + onListening: any; + onDelete: any; + sendQueue: any[]; + firewall: any; + rates: Map; + streamBuffer: Map; + gate: Map; + returnRoutes: Map; + metrics: { + i: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + DROPPED: number; + }; + o: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + }; + }; + peers: any; + encryption: Encryption; + config: any; + _onError: (err: any) => any; + socket: any; + probeSocket: any; + /** + * An implementation for clearning an interval that can be overridden by the test suite + * @param Number the number that identifies the timer + * @return {undefined} + * @ignore */ - get tags(): string[]; + _clearInterval(tid: any): undefined; /** - * JSON represenation of a `LanguageQueryResult` instance. - * @return {{ - * code: string, - * name: string, - * tags: string[] - * }} + * An implementation for clearning a timeout that can be overridden by the test suite + * @param Number the number that identifies the timer + * @return {undefined} + * @ignore */ - toJSON(): { - code: string; - name: string; - tags: string[]; - }; + _clearTimeout(tid: any): undefined; /** - * Internal inspect function. + * An implementation of an internal timer that can be overridden by the test suite + * @return {Number} * @ignore - * @return {LanguageQueryResult} */ - inspect(): LanguageQueryResult; - #private; - } - /** - * A container for a language code, tag, and description. - */ - export class LanguageDescription { + _setInterval(fn: any, t: any): number; /** - * `LanguageDescription` class constructor. - * @param {string} code - * @param {string} tag - * @param {string} description + * An implementation of an timeout timer that can be overridden by the test suite + * @return {Number} + * @ignore */ - constructor(code: string, tag: string, description: string); + _setTimeout(fn: any, t: any): number; + _onDebug(...args: any[]): void; /** - * The language code corresponding to the language - * @type {string} + * A method that encapsulates the listing procedure + * @return {undefined} + * @ignore + */ + _listen(): undefined; + init(cb: any): Promise; + onReady: any; + mainLoopTimer: number; + /** + * Continuously evaluate the state of the peer and its network + * @return {undefined} + * @ignore + */ + _mainLoop(ts: any): undefined; + /** + * Enqueue packets to be sent to the network + * @param {Buffer} data - An encoded packet + * @param {number} port - The desination port of the remote host + * @param {string} address - The destination address of the remote host + * @param {Socket=this.socket} socket - The socket to send on + * @return {undefined} + * @ignore + */ + send(data: Buffer, port: number, address: string, socket?: any): undefined; + /** + * @private + */ + private stream; + /** + * @private + */ + private _scheduleSend; + sendTimeout: number; + /** + * @private + */ + private _dequeue; + /** + * Send any unpublished packets + * @return {undefined} + * @ignore + */ + sendUnpublished(): undefined; + /** + * Get the serializable state of the peer (can be passed to the constructor or create method) + * @return {undefined} + */ + getState(): undefined; + getInfo(): Promise<{ + address: any; + port: any; + clock: number; + uptime: number; + natType: number; + natName: string; + peerId: string; + }>; + cacheInsert(packet: any): Promise; + addIndexedPeer(info: any): Promise; + reconnect(): Promise; + disconnect(): Promise; + probeReflectionTimeout: any; + sealUnsigned(...args: any[]): Promise; + openUnsigned(...args: any[]): Promise; + seal(...args: any[]): Promise; + open(...args: any[]): Promise; + addEncryptionKey(...args: any[]): Promise; + /** + * Get a selection of known peers + * @return {Array} + * @ignore + */ + getPeers(packet: any, peers: any, ignorelist: any, filter?: (o: any) => any): Array; + /** + * Send an eventually consistent packet to a selection of peers (fanout) + * @return {undefined} + * @ignore + */ + mcast(packet: any, ignorelist?: any[]): undefined; + /** + * The process of determining this peer's NAT behavior (firewall and dependentness) + * @return {undefined} + * @ignore + */ + requestReflection(): undefined; + /** + * Ping another peer + * @return {PacketPing} + * @ignore + */ + ping(peer: any, withRetry: any, props: any, socket: any): PacketPing; + /** + * Get a peer + * @return {RemotePeer} + * @ignore + */ + getPeer(id: any): RemotePeer; + /** + * This should be called at least once when an app starts to multicast + * this peer, and starts querying the network to discover peers. + * @param {object} keys - Created by `Encryption.createKeyPair()`. + * @param {object=} args - Options + * @param {number=MAX_BANDWIDTH} args.rateLimit - How many requests per second to allow for this subclusterId. + * @return {RemotePeer} + */ + join(sharedKey: any, args?: object | undefined): RemotePeer; + /** + * @param {Packet} T - The constructor to be used to create packets. + * @param {Any} message - The message to be split and packaged. + * @return {Array>} + * @ignore + */ + _message2packets(T: Packet, message: Any, args: any): Array>; + /** + * Sends a packet into the network that will be replicated and buffered. + * Each peer that receives it will buffer it until TTL and then replicate + * it provided it has has not exceeded their maximum number of allowed hops. + * + * @param {object} keys - the public and private key pair created by `Encryption.createKeyPair()`. + * @param {object} args - The arguments to be applied. + * @param {Buffer} args.message - The message to be encrypted by keys and sent. + * @param {Packet=} args.packet - The previous packet in the packet chain. + * @param {Buffer} args.usr1 - 32 bytes of arbitrary clusterId in the protocol framing. + * @param {Buffer} args.usr2 - 32 bytes of arbitrary clusterId in the protocol framing. + * @return {Array} + */ + publish(sharedKey: any, args: { + message: Buffer; + packet?: Packet | undefined; + usr1: Buffer; + usr2: Buffer; + }): Array; + /** + * @return {undefined} + */ + sync(peer: any, ptime?: number): undefined; + close(): void; + /** + * Deploy a query into the network + * @return {undefined} + * + */ + query(query: any): undefined; + /** + * + * This is a default implementation for deciding what to summarize + * from the cache when receiving a request to sync. that can be overridden + * + */ + cachePredicate(ts: any): (packet: any) => boolean; + /** + * A connection was made, add the peer to the local list of known + * peers and call the onConnection if it is defined by the user. + * + * @return {undefined} + * @ignore + */ + _onConnection(packet: any, peerId: any, port: any, address: any, proxy: any, socket: any): undefined; + /** + * Received a Sync Packet + * @return {undefined} + * @ignore + */ + _onSync(packet: any, port: any, address: any): undefined; + /** + * Received a Query Packet + * + * a -> b -> c -> (d) -> c -> b -> a + * + * @return {undefined} + * @example + * + * ```js + * peer.onQuery = (packet) => { + * // + * // read a database or something + * // + * return { + * message: Buffer.from('hello'), + * publicKey: '', + * privateKey: '' + * } + * } + * ``` + */ + _onQuery(packet: any, port: any, address: any): undefined; + /** + * Received a Ping Packet + * @return {undefined} + * @ignore + */ + _onPing(packet: any, port: any, address: any): undefined; + /** + * Received a Pong Packet + * @return {undefined} + * @ignore + */ + _onPong(packet: any, port: any, address: any): undefined; + reflectionFirstReponderTimeout: number; + /** + * Received an Intro Packet + * @return {undefined} + * @ignore */ - get code(): string; + _onIntro(packet: any, port: any, address: any, _: any, opts?: { + attempts: number; + }): undefined; + socketPool: any[]; /** - * The language tag corresponding to the language. - * @type {string} + * Received an Join Packet + * @return {undefined} + * @ignore */ - get tag(): string; + _onJoin(packet: any, port: any, address: any, data: any): undefined; /** - * The language description corresponding to the language. - * @type {string} + * Received an Publish Packet + * @return {undefined} + * @ignore */ - get description(): string; + _onPublish(packet: any, port: any, address: any, data: any): undefined; /** - * JSON represenation of a `LanguageDescription` instance. - * @return {{ - * code: string, - * tag: string, - * description: string - * }} + * Received an Stream Packet + * @return {undefined} + * @ignore */ - toJSON(): { - code: string; - tag: string; - description: string; - }; + _onStream(packet: any, port: any, address: any, data: any): undefined; /** - * Internal inspect function. + * Received any packet on the probe port to determine the firewall: + * are you port restricted, host restricted, or unrestricted. + * @return {undefined} * @ignore - * @return {LanguageDescription} */ - inspect(): LanguageDescription; - #private; - } - namespace _default { - export { codes }; - export { describe }; - export { lookup }; - export { names }; - export { tags }; + _onProbeMessage(data: any, { port, address }: { + port: any; + address: any; + }): undefined; + /** + * When a packet is received it is decoded, the packet contains the type + * of the message. Based on the message type it is routed to a function. + * like WebSockets, don't answer queries unless we know its another SRP peer. + * + * @param {Buffer|Uint8Array} data + * @param {{ port: number, address: string }} info + */ + _onMessage(data: Buffer | Uint8Array, { port, address }: { + port: number; + address: string; + }): Promise; } - export default _default; - import Enumeration from "socket:enumeration"; + export default Peer; + import { Packet } from "socket:latica/packets"; + import { sha256 } from "socket:latica/packets"; + import { Cache } from "socket:latica/cache"; + import { Encryption } from "socket:latica/encryption"; + import * as NAT from "socket:latica/nat"; + import { Buffer } from "socket:buffer"; + import { PacketPing } from "socket:latica/packets"; + import { PacketPublish } from "socket:latica/packets"; + export { Packet, sha256, Cache, Encryption, NAT }; } -declare module "socket:i18n" { - /** - * Get messages for `locale` pattern. This function could return many results - * for various locales given a `locale` pattern. such as `fr`, which could - * return results for `fr`, `fr-FR`, `fr-BE`, etc. - * @ignore - * @param {string} locale - * @return {object[]} - */ - export function getMessagesForLocale(locale: string): object[]; - /** - * Returns user preferred ISO 639 language codes or RFC 5646 language tags. - * @return {string[]} - */ - export function getAcceptLanguages(): string[]; - /** - * Returns the current user ISO 639 language code or RFC 5646 language tag. - * @return {?string} - */ - export function getUILanguage(): string | null; - /** - * Gets a localized message string for the specified message name. - * @param {string} messageName - * @param {object|string[]=} [substitutions = []] - * @param {object=} [options] - * @param {string=} [options.locale = null] - * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} - * @see {@link https://www.ibm.com/docs/en/rbd/9.5.1?topic=syslib-getmessage} - * @return {?string} - */ - export function getMessage(messageName: string, substitutions?: (object | string[]) | undefined, options?: object | undefined): string | null; - /** - * Gets a localized message description string for the specified message name. - * @param {string} messageName - * @param {object=} [options] - * @param {string=} [options.locale = null] - * @return {?string} - */ - export function getMessageDescription(messageName: string, options?: object | undefined): string | null; + +declare module "socket:latica/proxy" { + export default PeerWorkerProxy; /** - * A cache of loaded locale messages. - * @type {Map} + * `Proxy` class factory, returns a Proxy class that is a proxy to the Peer. + * @param {{ createSocket: function('udp4', null, object?): object }} options */ - export const cache: Map; + export class PeerWorkerProxy { + constructor(options: any, port: any, fn: any); + init(): Promise; + reconnect(): Promise; + disconnect(): Promise; + getInfo(): Promise; + getMetrics(): Promise; + getState(): Promise; + open(...args: any[]): Promise; + seal(...args: any[]): Promise; + sealUnsigned(...args: any[]): Promise; + openUnsigned(...args: any[]): Promise; + addEncryptionKey(...args: any[]): Promise; + send(...args: any[]): Promise; + sendUnpublished(...args: any[]): Promise; + cacheInsert(...args: any[]): Promise; + mcast(...args: any[]): Promise; + requestReflection(...args: any[]): Promise; + stream(...args: any[]): Promise; + join(...args: any[]): Promise; + publish(...args: any[]): Promise; + sync(...args: any[]): Promise; + close(...args: any[]): Promise; + query(...args: any[]): Promise; + compileCachePredicate(src: any): Promise; + callWorkerThread(prop: any, data: any): any; + callMainThread(prop: any, args: any): void; + resolveMainThread(seq: any, result: any): any; + #private; + } +} + +declare module "socket:latica/api" { + export default api; /** - * Default location of i18n locale messages - * @type {string} + * Initializes and returns the network bus. + * + * @async + * @function + * @param {object} options - Configuration options for the network bus. + * @param {object} events - A nodejs compatibe implementation of the events module. + * @param {object} dgram - A nodejs compatible implementation of the dgram module. + * @returns {Promise} - A promise that resolves to the initialized network bus. */ - export const DEFAULT_LOCALES_LOCATION: string; + export function api(options: object, events: object, dgram: object): Promise; +} + +declare module "socket:network" { + export default network; + export function network(options: any): Promise; + import { Cache } from "socket:latica/index"; + import { sha256 } from "socket:latica/index"; + import { Encryption } from "socket:latica/index"; + import { Packet } from "socket:latica/index"; + import { NAT } from "socket:latica/index"; + export { Cache, sha256, Encryption, Packet, NAT }; +} + +declare module "socket:service-worker" { /** - * An enumeration of supported ISO 639 language codes or RFC 5646 language tags. - * @type {Enumeration} - * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/LanguageCode} - * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} + * A reference to the opened environment. This value is an instance of an + * `Environment` if the scope is a ServiceWorker scope. + * @type {Environment|null} */ - export const LanguageCode: Enumeration; + export const env: Environment | null; namespace _default { - export { LanguageCode }; - export { getAcceptLanguages }; - export { getMessage }; - export { getUILanguage }; + export { ExtendableEvent }; + export { FetchEvent }; + export { Environment }; + export { Context }; + export { env }; } export default _default; - import Enumeration from "socket:enumeration"; + import { Environment } from "socket:service-worker/env"; + import { ExtendableEvent } from "socket:service-worker/events"; + import { FetchEvent } from "socket:service-worker/events"; + import { Context } from "socket:service-worker/context"; + export { ExtendableEvent, FetchEvent, Environment, Context }; } -declare module "socket:stream-relay/packets" { - /** - * The magic bytes prefixing every packet. They are the - * 2nd, 3rd, 5th, and 7th, prime numbers. - * @type {number[]} - */ - export const MAGIC_BYTES_PREFIX: number[]; - /** - * The version of the protocol. - */ - export const VERSION: 6; - /** - * The size in bytes of the prefix magic bytes. - */ - export const MAGIC_BYTES: 4; + +declare module "socket:string_decoder" { + export function StringDecoder(encoding: any): void; + export class StringDecoder { + constructor(encoding: any); + encoding: any; + text: typeof utf16Text | typeof base64Text; + end: typeof utf16End | typeof base64End | typeof simpleEnd; + fillLast: typeof utf8FillLast; + write: typeof simpleWrite; + lastNeed: number; + lastTotal: number; + lastChar: Uint8Array; + } + export default StringDecoder; + function utf16Text(buf: any, i: any): any; + class utf16Text { + constructor(buf: any, i: any); + lastNeed: number; + lastTotal: number; + } + function base64Text(buf: any, i: any): any; + class base64Text { + constructor(buf: any, i: any); + lastNeed: number; + lastTotal: number; + } + function utf16End(buf: any): any; + function base64End(buf: any): any; + function simpleEnd(buf: any): any; + function utf8FillLast(buf: any): any; + function simpleWrite(buf: any): any; +} + +declare module "socket:test/context" { + export default function _default(GLOBAL_TEST_RUNNER: any): void; +} + +declare module "socket:test/dom-helpers" { /** - * The maximum size of the user message. + * Converts querySelector string to an HTMLElement or validates an existing HTMLElement. + * + * @export + * @param {string|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @returns {Element} The HTMLElement, Element, or Window that corresponds to the selector. + * @throws {Error} Throws an error if the `selector` is not a string that resolves to an HTMLElement or not an instance of HTMLElement, Element, or Window. + * */ - export const MESSAGE_BYTES: 1024; + export function toElement(selector: string | Element): Element; /** - * The cache TTL in milliseconds. - */ - export const CACHE_TTL: number; - export namespace PACKET_SPEC { - namespace type { - let bytes: number; - let encoding: string; - } - namespace version { - let bytes_1: number; - export { bytes_1 as bytes }; - let encoding_1: string; - export { encoding_1 as encoding }; - export { VERSION as default }; - } - namespace clock { - let bytes_2: number; - export { bytes_2 as bytes }; - let encoding_2: string; - export { encoding_2 as encoding }; - let _default: number; - export { _default as default }; - } - namespace hops { - let bytes_3: number; - export { bytes_3 as bytes }; - let encoding_3: string; - export { encoding_3 as encoding }; - let _default_1: number; - export { _default_1 as default }; - } - namespace index { - let bytes_4: number; - export { bytes_4 as bytes }; - let encoding_4: string; - export { encoding_4 as encoding }; - let _default_2: number; - export { _default_2 as default }; - export let signed: boolean; - } - namespace ttl { - let bytes_5: number; - export { bytes_5 as bytes }; - let encoding_5: string; - export { encoding_5 as encoding }; - export { CACHE_TTL as default }; - } - namespace clusterId { - let bytes_6: number; - export { bytes_6 as bytes }; - let encoding_6: string; - export { encoding_6 as encoding }; - let _default_3: number[]; - export { _default_3 as default }; - } - namespace subclusterId { - let bytes_7: number; - export { bytes_7 as bytes }; - let encoding_7: string; - export { encoding_7 as encoding }; - let _default_4: number[]; - export { _default_4 as default }; - } - namespace previousId { - let bytes_8: number; - export { bytes_8 as bytes }; - let encoding_8: string; - export { encoding_8 as encoding }; - let _default_5: number[]; - export { _default_5 as default }; - } - namespace packetId { - let bytes_9: number; - export { bytes_9 as bytes }; - let encoding_9: string; - export { encoding_9 as encoding }; - let _default_6: number[]; - export { _default_6 as default }; - } - namespace nextId { - let bytes_10: number; - export { bytes_10 as bytes }; - let encoding_10: string; - export { encoding_10 as encoding }; - let _default_7: number[]; - export { _default_7 as default }; - } - namespace usr1 { - let bytes_11: number; - export { bytes_11 as bytes }; - let _default_8: number[]; - export { _default_8 as default }; - } - namespace usr2 { - let bytes_12: number; - export { bytes_12 as bytes }; - let _default_9: number[]; - export { _default_9 as default }; - } - namespace usr3 { - let bytes_13: number; - export { bytes_13 as bytes }; - let _default_10: number[]; - export { _default_10 as default }; - } - namespace usr4 { - let bytes_14: number; - export { bytes_14 as bytes }; - let _default_11: number[]; - export { _default_11 as default }; + * Waits for an element to appear in the DOM and resolves the promise when it does. + * + * @export + * @param {Object} args - Configuration arguments. + * @param {string} [args.selector] - The CSS selector to look for. + * @param {boolean} [args.visible=true] - Whether the element should be visible. + * @param {number} [args.timeout=defaultTimeout] - Time in milliseconds to wait before rejecting the promise. + * @param {() => HTMLElement | Element | null | undefined} [lambda] - An optional function that returns the element. Used if the `selector` is not provided. + * @returns {Promise} - A promise that resolves to the found element. + * + * @throws {Error} - Throws an error if neither `lambda` nor `selector` is provided. + * @throws {Error} - Throws an error if the element is not found within the timeout. + * + * @example + * ```js + * waitFor({ selector: '#my-element', visible: true, timeout: 5000 }) + * .then(el => console.log('Element found:', el)) + * .catch(err => console.log('Element not found:', err)); + * ``` + */ + export function waitFor(args: { + selector?: string; + visible?: boolean; + timeout?: number; + }, lambda?: () => HTMLElement | Element | null | undefined): Promise; + /** + * Waits for an element's text content to match a given string or regular expression. + * + * @export + * @param {Object} args - Configuration arguments. + * @param {Element} args.element - The root element from which to begin searching. + * @param {string} [args.text] - The text to search for within elements. + * @param {RegExp} [args.regex] - A regular expression to match against element text content. + * @param {boolean} [args.multipleTags=false] - Whether to look for text across multiple sibling elements. + * @param {number} [args.timeout=defaultTimeout] - Time in milliseconds to wait before rejecting the promise. + * @returns {Promise} - A promise that resolves to the found element or null. + * + * @example + * ```js + * waitForText({ element: document.body, text: 'Hello', timeout: 5000 }) + * .then(el => console.log('Element found:', el)) + * .catch(err => console.log('Element not found:', err)); + * ``` + */ + export function waitForText(args: { + element: Element; + text?: string; + regex?: RegExp; + multipleTags?: boolean; + timeout?: number; + }): Promise; + /** + * @export + * @param {Object} args - Arguments + * @param {string | Event} args.event - The event to dispatch. + * @param {HTMLElement | Element | window} [args.element=window] - The element to dispatch the event on. + * @returns {void} + * + * @throws {Error} Throws an error if the `event` is not a string that can be converted to a CustomEvent or not an instance of Event. + */ + export function event(args: { + event: string | Event; + element?: HTMLElement | Element | (Window & typeof globalThis); + }): void; + /** + * @export + * Copy pasted from https://raw.githubusercontent.com/testing-library/jest-dom/master/src/to-be-visible.js + * @param {Element | HTMLElement} element + * @param {Element | HTMLElement} [previousElement] + * @returns {boolean} + */ + export function isElementVisible(element: Element | HTMLElement, previousElement?: Element | HTMLElement): boolean; +} + +declare module "socket:test/index" { + /** + * @returns {number} - The default timeout for tests in milliseconds. + */ + export function getDefaultTestRunnerTimeout(): number; + /** + * @param {string} name + * @param {TestFn} [fn] + * @returns {void} + */ + export function only(name: string, fn?: TestFn): void; + /** + * @param {string} _name + * @param {TestFn} [_fn] + * @returns {void} + */ + export function skip(_name: string, _fn?: TestFn): void; + /** + * @param {boolean} strict + * @returns {void} + */ + export function setStrict(strict: boolean): void; + /** + * @typedef {{ + * (name: string, fn?: TestFn): void + * only(name: string, fn?: TestFn): void + * skip(name: string, fn?: TestFn): void + * }} testWithProperties + * @ignore + */ + /** + * @type {testWithProperties} + * @param {string} name + * @param {TestFn} [fn] + * @returns {void} + */ + export function test(name: string, fn?: TestFn): void; + export namespace test { + export { only }; + export { skip }; + export function linux(name: any, fn: any): void; + export function windows(name: any, fn: any): void; + export function win32(name: any, fn: any): void; + export function unix(name: any, fn: any): void; + export function macosx(name: any, fn: any): void; + export function macos(name: any, fn: any): void; + export function mac(name: any, fn: any): void; + export function darwin(name: any, fn: any): void; + export function iphone(name: any, fn: any): void; + export namespace iphone { + function simulator(name: any, fn: any): void; } - namespace message { - let bytes_15: number; - export { bytes_15 as bytes }; - let _default_12: number[]; - export { _default_12 as default }; + export function ios(name: any, fn: any): void; + export namespace ios { + function simulator(name: any, fn: any): void; } - namespace sig { - let bytes_16: number; - export { bytes_16 as bytes }; - let _default_13: number[]; - export { _default_13 as default }; + export function android(name: any, fn: any): void; + export namespace android { + function emulator(name: any, fn: any): void; } + export function desktop(name: any, fn: any): void; + export function mobile(name: any, fn: any): void; } /** - * The size in bytes of the total packet frame and message. + * @typedef {(t: Test) => (void | Promise)} TestFn */ - export const PACKET_BYTES: number; /** - * The maximum distance that a packet can be replicated. + * @class */ - export const MAX_HOPS: 16; - export function validatePacket(o: any, constraints: { - [key: string]: { - required: boolean; - type: string; + export class Test { + /** + * @constructor + * @param {string} name + * @param {TestFn} fn + * @param {TestRunner} runner + */ + constructor(name: string, fn: TestFn, runner: TestRunner); + /** + * @type {string} + * @ignore + */ + name: string; + /** + * @type {null|number} + * @ignore + */ + _planned: null | number; + /** + * @type {null|number} + * @ignore + */ + _actual: null | number; + /** + * @type {TestFn} + * @ignore + */ + fn: TestFn; + /** + * @type {TestRunner} + * @ignore + */ + runner: TestRunner; + /** + * @type{{ pass: number, fail: number }} + * @ignore + */ + _result: { + pass: number; + fail: number; }; - }): void; - /** - * Computes a SHA-256 hash of input returning a hex encoded string. - * @type {function(string|Buffer|Uint8Array): Promise} - */ - export const sha256: (arg0: string | Buffer | Uint8Array) => Promise; - export function decode(buf: Buffer): Packet; - export function getTypeFromBytes(buf: any): any; - export class Packet { - static ttl: number; - static maxLength: number; /** - * Returns an empty `Packet` instance. - * @return {Packet} + * @type {boolean} + * @ignore */ - static empty(): Packet; + done: boolean; /** - * @param {Packet|object} packet - * @return {Packet} + * @type {boolean} + * @ignore + */ + strict: boolean; + /** + * @param {string} msg + * @returns {void} + */ + comment(msg: string): void; + /** + * Plan the number of assertions. + * + * @param {number} n + * @returns {void} + */ + plan(n: number): void; + /** + * @template T + * @param {T} actual + * @param {T} expected + * @param {string} [msg] + * @returns {void} + */ + deepEqual(actual: T, expected: T, msg?: string): void; + /** + * @template T + * @param {T} actual + * @param {T} expected + * @param {string} [msg] + * @returns {void} + */ + notDeepEqual(actual: T, expected: T, msg?: string): void; + /** + * @template T + * @param {T} actual + * @param {T} expected + * @param {string} [msg] + * @returns {void} + */ + equal(actual: T, expected: T, msg?: string): void; + /** + * @param {unknown} actual + * @param {unknown} expected + * @param {string} [msg] + * @returns {void} + */ + notEqual(actual: unknown, expected: unknown, msg?: string): void; + /** + * @param {string} [msg] + * @returns {void} + */ + fail(msg?: string): void; + /** + * @param {unknown} actual + * @param {string} [msg] + * @returns {void} + */ + ok(actual: unknown, msg?: string): void; + /** + * @param {string} [msg] + * @returns {void} + */ + pass(msg?: string): void; + /** + * @param {Error | null | undefined} err + * @param {string} [msg] + * @returns {void} + */ + ifError(err: Error | null | undefined, msg?: string): void; + /** + * @param {Function} fn + * @param {RegExp | any} [expected] + * @param {string} [message] + * @returns {void} */ - static from(packet: Packet | object): Packet; + throws(fn: Function, expected?: RegExp | any, message?: string): void; /** - * Determines if input is a packet. - * @param {Buffer|Uint8Array|number[]|object|Packet} packet - * @return {boolean} + * Sleep for ms with an optional msg + * + * @param {number} ms + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.sleep(100) + * ``` */ - static isPacket(packet: Buffer | Uint8Array | number[] | object | Packet): boolean; + sleep(ms: number, msg?: string): Promise; /** - */ - static encode(p: any): Promise; - static decode(buf: any): Packet; + * Request animation frame with an optional msg. Falls back to a 0ms setTimeout when + * tests are run headlessly. + * + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.requestAnimationFrame() + * ``` + */ + requestAnimationFrame(msg?: string): Promise; /** - * `Packet` class constructor. - * @param {Packet|object?} options + * Dispatch the `click`` method on an element specified by selector. + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.click('.class button', 'Click a button') + * ``` */ - constructor(options?: Packet | (object | null)); + click(selector: string | HTMLElement | Element, msg?: string): Promise; /** - * @param {Packet} packet - * @return {Packet} + * Dispatch the click window.MouseEvent on an element specified by selector. + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.eventClick('.class button', 'Click a button with an event') + * ``` */ - copy(): Packet; - timestamp: any; - isComposed: any; - isReconciled: any; - meta: any; - } - export class PacketPing extends Packet { - static type: number; - constructor({ message, clusterId, subclusterId }: { - message: any; - clusterId: any; - subclusterId: any; - }); - } - export class PacketPong extends Packet { - static type: number; - constructor({ message, clusterId, subclusterId }: { - message: any; - clusterId: any; - subclusterId: any; - }); - } - export class PacketIntro extends Packet { - static type: number; - constructor({ clock, hops, clusterId, subclusterId, usr1, message }: { - clock: any; - hops: any; - clusterId: any; - subclusterId: any; - usr1: any; - message: any; - }); - } - export class PacketJoin extends Packet { - static type: number; - constructor({ clock, hops, clusterId, subclusterId, message }: { - clock: any; - hops: any; - clusterId: any; - subclusterId: any; - message: any; - }); - } - export class PacketPublish extends Packet { - static type: number; - constructor({ message, sig, packetId, clusterId, subclusterId, nextId, clock, hops, usr1, usr2, ttl, previousId }: { - message: any; - sig: any; - packetId: any; - clusterId: any; - subclusterId: any; - nextId: any; - clock: any; - hops: any; - usr1: any; - usr2: any; - ttl: any; - previousId: any; - }); - } - export class PacketStream extends Packet { - static type: number; - constructor({ message, sig, packetId, clusterId, subclusterId, nextId, clock, ttl, usr1, usr2, usr3, usr4, previousId }: { - message: any; - sig: any; - packetId: any; - clusterId: any; - subclusterId: any; - nextId: any; - clock: any; - ttl: any; - usr1: any; - usr2: any; - usr3: any; - usr4: any; - previousId: any; - }); - } - export class PacketSync extends Packet { - static type: number; - constructor({ packetId, message }: { - packetId: any; - message?: any; - }); - } - export class PacketQuery extends Packet { - static type: number; - constructor({ packetId, previousId, subclusterId, usr1, usr2, usr3, usr4, message }: { - packetId: any; - previousId: any; - subclusterId: any; - usr1: any; - usr2: any; - usr3: any; - usr4: any; - message?: {}; - }); - } - export default Packet; - import { Buffer } from "socket:buffer"; -} -declare module "socket:stream-relay/encryption" { - /** - * Class for handling encryption and key management. - */ - export class Encryption { + eventClick(selector: string | HTMLElement | Element, msg?: string): Promise; /** - * Creates a shared key based on the provided seed or generates a random one. - * @param {Uint8Array|string} seed - Seed for key generation. - * @returns {Promise} - Shared key. + * Dispatch an event on the target. + * + * @param {string | Event} event - The event name or Event instance to dispatch. + * @param {string|HTMLElement|Element} target - A CSS selector string, or an instance of HTMLElement, or Element to dispatch the event on. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.dispatchEvent('my-event', '#my-div', 'Fire the my-event event') + * ``` */ - static createSharedKey(seed: Uint8Array | string): Promise; + dispatchEvent(event: string | Event, target: string | HTMLElement | Element, msg?: string): Promise; /** - * Creates a key pair for signing and verification. - * @param {Uint8Array|string} seed - Seed for key generation. - * @returns {Promise<{ publicKey: Uint8Array, privateKey: Uint8Array }>} - Key pair. + * Call the focus method on element specified by selector. + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.focus('#my-div') + * ``` */ - static createKeyPair(seed: Uint8Array | string): Promise<{ - publicKey: Uint8Array; - privateKey: Uint8Array; - }>; + focus(selector: string | HTMLElement | Element, msg?: string): Promise; /** - * Creates an ID using SHA-256 hash. - * @param {string} str - String to hash. - * @returns {Promise} - SHA-256 hash. + * Call the blur method on element specified by selector. + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.blur('#my-div') + * ``` */ - static createId(str: string): Promise; + blur(selector: string | HTMLElement | Element, msg?: string): Promise; /** - * Creates a cluster ID using SHA-256 hash with specified output size. - * @param {string} str - String to hash. - * @returns {Promise} - SHA-256 hash with specified output size. + * Consecutively set the str value of the element specified by selector to simulate typing. + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @param {string} str - The string to type into the :focus element. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.typeValue('#my-div', 'Hello World', 'Type "Hello World" into #my-div') + * ``` */ - static createClusterId(str: string): Promise; + type(selector: string | HTMLElement | Element, str: string, msg?: string): Promise; /** - * Signs a message using the given secret key. - * @param {Buffer} b - The message to sign. - * @param {Uint8Array} sk - The secret key to use. - * @returns {Uint8Array} - Signature. + * appendChild an element el to a parent selector element. + * + * @param {string|HTMLElement|Element} parentSelector - A CSS selector string, or an instance of HTMLElement, or Element to appendChild on. + * @param {HTMLElement|Element} el - A element to append to the parent element. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * const myElement = createElement('div') + * await t.appendChild('#parent-selector', myElement, 'Append myElement into #parent-selector') + * ``` */ - static sign(b: Buffer, sk: Uint8Array): Uint8Array; + appendChild(parentSelector: string | HTMLElement | Element, el: HTMLElement | Element, msg?: string): Promise; /** - * Verifies the signature of a message using the given public key. - * @param {Buffer} b - The message to verify. - * @param {Uint8Array} sig - The signature to check. - * @param {Uint8Array} pk - The public key to use. - * @returns {number} - Returns non-zero if the buffer could not be verified. + * Remove an element from the DOM. + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element to remove from the DOM. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.removeElement('#dom-selector', 'Remove #dom-selector') + * ``` */ - static verify(b: Buffer, sig: Uint8Array, pk: Uint8Array): number; + removeElement(selector: string | HTMLElement | Element, msg?: string): Promise; /** - * Mapping of public keys to key objects. - * @type {Object.} + * Test if an element is visible + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element to test visibility on. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.elementVisible('#dom-selector','Element is visible') + * ``` */ - keys: { - [x: string]: { - publicKey: Uint8Array; - privateKey: Uint8Array; - ts: number; - }; - }; + elementVisible(selector: string | HTMLElement | Element, msg?: string): Promise; /** - * Adds a key pair to the keys mapping. - * @param {Uint8Array|string} publicKey - Public key. - * @param {Uint8Array} privateKey - Private key. + * Test if an element is invisible + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element to test visibility on. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.elementInvisible('#dom-selector','Element is invisible') + * ``` */ - add(publicKey: Uint8Array | string, privateKey: Uint8Array): void; + elementInvisible(selector: string | HTMLElement | Element, msg?: string): Promise; /** - * Removes a key from the keys mapping. - * @param {Uint8Array|string} publicKey - Public key. + * Test if an element is invisible + * + * @param {string|(() => HTMLElement|Element|null|undefined)} querySelectorOrFn - A query string or a function that returns an element. + * @param {Object} [opts] + * @param {boolean} [opts.visible] - The element needs to be visible. + * @param {number} [opts.timeout] - The maximum amount of time to wait. + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.waitFor('#dom-selector', { visible: true },'#dom-selector is on the page and visible') + * ``` */ - remove(publicKey: Uint8Array | string): void; + waitFor(querySelectorOrFn: string | (() => HTMLElement | Element | null | undefined), opts?: { + visible?: boolean; + timeout?: number; + }, msg?: string): Promise; /** - * Checks if a key is in the keys mapping. - * @param {Uint8Array|string} to - Public key or Uint8Array. - * @returns {boolean} - True if the key is present, false otherwise. + * @typedef {Object} WaitForTextOpts + * @property {string} [text] - The text to wait for + * @property {number} [timeout] + * @property {Boolean} [multipleTags] + * @property {RegExp} [regex] The regex to wait for */ - has(to: Uint8Array | string): boolean; /** - * Decrypts a sealed message for a specific receiver. - * @param {Buffer} message - The sealed message. - * @param {Object|string} v - Key object or public key. - * @returns {Buffer} - Decrypted message. - * @throws {Error} - Throws ENOKEY if the key is not found, EMALFORMED if the message is malformed, ENOTVERIFIED if the message cannot be verified. + * Test if an element is invisible + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @param {WaitForTextOpts | string | RegExp} [opts] + * @param {string} [msg] + * @returns {Promise} + * + * @example + * ```js + * await t.waitForText('#dom-selector', 'Text to wait for') + * ``` + * + * @example + * ```js + * await t.waitForText('#dom-selector', /hello/i) + * ``` + * + * @example + * ```js + * await t.waitForText('#dom-selector', { + * text: 'Text to wait for', + * multipleTags: true + * }) + * ``` */ - open(message: Buffer, v: any | string): Buffer; + waitForText(selector: string | HTMLElement | Element, opts?: { + /** + * - The text to wait for + */ + text?: string; + timeout?: number; + multipleTags?: boolean; + /** + * The regex to wait for + */ + regex?: RegExp; + } | string | RegExp, msg?: string): Promise; /** - * Opens a sealed message using the specified key. - * @param {Buffer} message - The sealed message. - * @param {Object|string} v - Key object or public key. - * @returns {Buffer} - Decrypted message. - * @throws {Error} - Throws ENOKEY if the key is not found. + * Run a querySelector as an assert and also get the results + * + * @param {string} selector - A CSS selector string, or an instance of HTMLElement, or Element to select. + * @param {string} [msg] + * @returns {HTMLElement | Element} + * + * @example + * ```js + * const element = await t.querySelector('#dom-selector') + * ``` */ - openMessage(message: Buffer, v: any | string): Buffer; + querySelector(selector: string, msg?: string): HTMLElement | Element; /** - * Seals a message for a specific receiver using their public key. + * Run a querySelectorAll as an assert and also get the results * - * `Seal(message, receiver)` performs an _encrypt-sign-encrypt_ (ESE) on - * a plaintext `message` for a `receiver` identity. This prevents repudiation - * attacks and doesn't rely on packet chain guarantees. + * @param {string} selector - A CSS selector string, or an instance of HTMLElement, or Element to select. + * @param {string} [msg] + @returns {Array} * - * let ct = Seal(sender | pt, receiver) - * let sig = Sign(ct, sk) - * let out = Seal(sig | ct) + * @example + * ```js + * const elements = await t.querySelectorAll('#dom-selector', '') + * ``` + */ + querySelectorAll(selector: string, msg?: string): Array; + /** + * Retrieves the computed styles for a given element. * - * In an setup between Alice & Bob, this means: - * - Only Bob sees the plaintext - * - Alice wrote the plaintext and the ciphertext - * - Only Bob can see that Alice wrote the plaintext and ciphertext - * - Bob cannot forward the message without invalidating Alice's signature. - * - The outer encryption serves to prevent an attacker from replacing Alice's - * signature. As with _sign-encrypt-sign (SES), ESE is a variant of - * including the recipient's name inside the plaintext, which is then signed - * and encrypted Alice signs her plaintext along with her ciphertext, so as - * to protect herself from a laintext-substitution attack. At the same time, - * Alice's signed plaintext gives Bob non-repudiation. + * @param {string|Element} selector - The CSS selector or the Element object for which to get the computed styles. + * @param {string} [msg] - An optional message to display when the operation is successful. Default message will be generated based on the type of selector. + * @returns {CSSStyleDeclaration} - The computed styles of the element. + * @throws {Error} - Throws an error if the element has no `ownerDocument` or if `ownerDocument.defaultView` is not available. * - * @see https://theworld.com/~dtd/sign_encrypt/sign_encrypt7.html + * @example + * ```js + * // Using CSS selector + * const style = getComputedStyle('.my-element', 'Custom success message'); + * ``` * - * @param {Buffer} message - The message to seal. - * @param {Object|string} v - Key object or public key. - * @returns {Buffer} - Sealed message. - * @throws {Error} - Throws ENOKEY if the key is not found. + * @example + * ```js + * // Using Element object + * const el = document.querySelector('.my-element'); + * const style = getComputedStyle(el); + * ``` */ - seal(message: Buffer, v: any | string): Buffer; - } - import Buffer from "socket:buffer"; -} -declare module "socket:stream-relay/cache" { - /** - * @typedef {Packet} CacheEntry - * @typedef {function(CacheEntry, CacheEntry): number} CacheEntrySiblingResolver - */ - /** - * Default cache sibling resolver that computes a delta between - * two entries clocks. - * @param {CacheEntry} a - * @param {CacheEntry} b - * @return {number} - */ - export function defaultSiblingResolver(a: CacheEntry, b: CacheEntry): number; - export function trim(buf: Buffer): any; - /** - * Default max size of a `Cache` instance. - */ - export const DEFAULT_MAX_SIZE: number; - /** - * Internal mapping of packet IDs to packet data used by `Cache`. - */ - export class CacheData extends Map { - constructor(); - constructor(entries?: readonly (readonly [any, any])[]); - constructor(); - constructor(iterable?: Iterable); + getComputedStyle(selector: string | Element, msg?: string): CSSStyleDeclaration; + /** + * @param {boolean} pass + * @param {unknown} actual + * @param {unknown} expected + * @param {string} description + * @param {string} operator + * @returns {void} + * @ignore + */ + _assert(pass: boolean, actual: unknown, expected: unknown, description: string, operator: string): void; + /** + * @returns {Promise<{ + * pass: number, + * fail: number + * }>} + */ + run(): Promise<{ + pass: number; + fail: number; + }>; } /** - * A class for storing a cache of packets by ID. This class includes a scheme - * for reconciling disjointed packet caches in a large distributed system. The - * following are key design characteristics. - * - * Space Efficiency: This scheme can be space-efficient because it summarizes - * the cache's contents in a compact binary format. By sharing these summaries, - * two computers can quickly determine whether their caches have common data or - * differences. - * - * Bandwidth Efficiency: Sharing summaries instead of the full data can save - * bandwidth. If the differences between the caches are small, sharing summaries - * allows for more efficient data synchronization. - * - * Time Efficiency: The time efficiency of this scheme depends on the size of - * the cache and the differences between the two caches. Generating summaries - * and comparing them can be faster than transferring and comparing the entire - * dataset, especially for large caches. - * - * Complexity: The scheme introduces some complexity due to the need to encode - * and decode summaries. In some cases, the overhead introduced by this - * complexity might outweigh the benefits, especially if the caches are - * relatively small. In this case, you should be using a query. - * - * Data Synchronization Needs: The efficiency also depends on the data - * synchronization needs. If the data needs to be synchronized in real-time, - * this scheme might not be suitable. It's more appropriate for cases where - * periodic or batch synchronization is acceptable. - * - * Scalability: The scheme's efficiency can vary depending on the scalability - * of the system. As the number of cache entries or computers involved - * increases, the complexity of generating and comparing summaries will stay - * bound to a maximum of 16Mb. - * + * @class */ - export class Cache { - static HASH_SIZE_BYTES: number; + export class TestRunner { /** - * The encodeSummary method provides a compact binary encoding of the output - * of summary() - * - * @param {Object} summary - the output of calling summary() - * @return {Buffer} - **/ - static encodeSummary(summary: any): Buffer; + * @constructor + * @param {(lines: string) => void} [report] + */ + constructor(report?: (lines: string) => void); /** - * The decodeSummary method decodes the output of encodeSummary() - * - * @param {Buffer} bin - the output of calling encodeSummary() - * @return {Object} summary - **/ - static decodeSummary(bin: Buffer): any; + * @type {(lines: string) => void} + * @ignore + */ + report: (lines: string) => void; /** - * `Cache` class constructor. - * @param {CacheData?} [data] + * @type {Test[]} + * @ignore */ - constructor(data?: CacheData | null, siblingResolver?: typeof defaultSiblingResolver); - data: CacheData; - maxSize: number; - siblingResolver: typeof defaultSiblingResolver; + tests: Test[]; /** - * Readonly count of the number of cache entries. - * @type {number} + * @type {Test[]} + * @ignore */ - get size(): number; + onlyTests: Test[]; /** - * Readonly size of the cache in bytes. - * @type {number} + * @type {boolean} + * @ignore */ - get bytes(): number; + scheduled: boolean; /** - * Inserts a `CacheEntry` value `v` into the cache at key `k`. - * @param {string} k - * @param {CacheEntry} v - * @return {boolean} + * @type {number} + * @ignore */ - insert(k: string, v: CacheEntry): boolean; + _id: number; /** - * Gets a `CacheEntry` value at key `k`. - * @param {string} k - * @return {CacheEntry?} + * @type {boolean} + * @ignore */ - get(k: string): CacheEntry | null; + completed: boolean; /** - * @param {string} k - * @return {boolean} + * @type {boolean} + * @ignore */ - delete(k: string): boolean; + rethrowExceptions: boolean; /** - * Predicate to determine if cache contains an entry at key `k`. - * @param {string} k - * @return {boolean} + * @type {boolean} + * @ignore */ - has(k: string): boolean; + strict: boolean; /** - * Composes an indexed packet into a new `Packet` - * @param {Packet} packet + * @type {function | void} + * @ignore */ - compose(packet: Packet, source?: CacheData): Promise; - sha1(value: any, toHex: any): Promise; + _onFinishCallback: Function | void; /** - * - * The summarize method returns a terse yet comparable summary of the cache - * contents. - * - * Think of the cache as a trie of hex characters, the summary returns a - * checksum for the current level of the trie and for its 16 children. - * - * This is similar to a merkel tree as equal subtrees can easily be detected - * without the need for further recursion. When the subtree checksums are - * inequivalent then further negotiation at lower levels may be required, this - * process continues until the two trees become synchonized. - * - * When the prefix is empty, the summary will return an array of 16 checksums - * these checksums provide a way of comparing that subtree with other peers. - * - * When a variable-length hexidecimal prefix is provided, then only cache - * member hashes sharing this prefix will be considered. - * - * For each hex character provided in the prefix, the trie will decend by one - * level, each level divides the 2^128 address space by 16. For exmaple... - * - * ``` - * Level 0 1 2 - * ---------------- - * 2b00 - * aa0e ━┓ ━┓ - * aa1b ┃ ┃ - * aae3 ┃ ┃ ━┓ - * aaea ┃ ┃ ┃ - * aaeb ┃ ━┛ ━┛ - * ab00 ┃ ━┓ - * ab1e ┃ ┃ - * ab2a ┃ ┃ - * abef ┃ ┃ - * abf0 ━┛ ━┛ - * bff9 - * ``` - * - * @param {string} prefix - a string of lowercased hexidecimal characters - * @return {Object} - * + * @returns {string} */ - summarize(prefix?: string, predicate?: (o: any) => boolean): any; - } - export default Cache; - export type CacheEntry = Packet; - export type CacheEntrySiblingResolver = (arg0: CacheEntry, arg1: CacheEntry) => number; - import { Buffer } from "socket:buffer"; - import { Packet } from "socket:stream-relay/packets"; -} -declare module "socket:stream-relay/nat" { - /** - * The NAT type is encoded using 5 bits: - * - * 0b00001 : the lsb indicates if endpoint dependence information is included - * 0b00010 : the second bit indicates the endpoint dependence value - * - * 0b00100 : the third bit indicates if firewall information is included - * 0b01000 : the fourth bit describes which requests can pass the firewall, only known IPs (0) or any IP (1) - * 0b10000 : the fifth bit describes which requests can pass the firewall, only known ports (0) or any port (1) - */ - /** - * Every remote will see the same IP:PORT mapping for this peer. - * - * :3333 ┌──────┐ - * :1111 ┌───▶ │ R1 │ - * ┌──────┐ ┌───────┐ │ └──────┘ - * │ P1 ├───▶│ NAT ├──┤ - * └──────┘ └───────┘ │ ┌──────┐ - * └───▶ │ R2 │ - * :3333 └──────┘ - */ - export const MAPPING_ENDPOINT_INDEPENDENT: 3; - /** - * Every remote will see a different IP:PORT mapping for this peer. - * - * :4444 ┌──────┐ - * :1111 ┌───▶ │ R1 │ - * ┌──────┐ ┌───────┐ │ └──────┘ - * │ P1 ├───▶│ NAT ├──┤ - * └──────┘ └───────┘ │ ┌──────┐ - * └───▶ │ R2 │ - * :5555 └──────┘ - */ - export const MAPPING_ENDPOINT_DEPENDENT: 1; - /** - * The firewall allows the port mapping to be accessed by: - * - Any IP:PORT combination (FIREWALL_ALLOW_ANY) - * - Any PORT on a previously connected IP (FIREWALL_ALLOW_KNOWN_IP) - * - Only from previously connected IP:PORT combinations (FIREWALL_ALLOW_KNOWN_IP_AND_PORT) - */ - export const FIREWALL_ALLOW_ANY: 28; - export const FIREWALL_ALLOW_KNOWN_IP: 12; - export const FIREWALL_ALLOW_KNOWN_IP_AND_PORT: 4; - /** - * The initial state of the nat is unknown and its value is 0 - */ - export const UNKNOWN: 0; - /** - * Full-cone NAT, also known as one-to-one NAT - * - * Any external host can send packets to iAddr:iPort by sending packets to eAddr:ePort. - * - * @summary its a packet party at this mapping and everyone's invited - */ - export const UNRESTRICTED: number; - /** - * (Address)-restricted-cone NAT - * - * An external host (hAddr:any) can send packets to iAddr:iPort by sending packets to eAddr:ePort only - * if iAddr:iPort has previously sent a packet to hAddr:any. "Any" means the port number doesn't matter. - * - * @summary The NAT will drop your packets unless a peer within its network has previously messaged you from *any* port. - */ - export const ADDR_RESTRICTED: number; - /** - * Port-restricted cone NAT - * - * An external host (hAddr:hPort) can send packets to iAddr:iPort by sending - * packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to - * hAddr:hPort. - * - * @summary The NAT will drop your packets unless a peer within its network - * has previously messaged you from this *specific* port. - */ - export const PORT_RESTRICTED: number; - /** - * Symmetric NAT - * - * Only an external host that receives a packet from an internal host can send - * a packet back. - * - * @summary The NAT will only accept replies to a correspondence initialized - * by itself, the mapping it created is only valid for you. - */ - export const ENDPOINT_RESTRICTED: number; - export function isEndpointDependenceDefined(nat: any): boolean; - export function isFirewallDefined(nat: any): boolean; - export function isValid(nat: any): boolean; - export function toString(n: any): "UNRESTRICTED" | "ADDR_RESTRICTED" | "PORT_RESTRICTED" | "ENDPOINT_RESTRICTED" | "UNKNOWN"; - export function toStringStrategy(n: any): "STRATEGY_DEFER" | "STRATEGY_DIRECT_CONNECT" | "STRATEGY_TRAVERSAL_OPEN" | "STRATEGY_TRAVERSAL_CONNECT" | "STRATEGY_PROXY" | "STRATEGY_UNKNOWN"; - export const STRATEGY_DEFER: 0; - export const STRATEGY_DIRECT_CONNECT: 1; - export const STRATEGY_TRAVERSAL_OPEN: 2; - export const STRATEGY_TRAVERSAL_CONNECT: 3; - export const STRATEGY_PROXY: 4; - export function connectionStrategy(a: any, b: any): 0 | 1 | 2 | 3 | 4; -} -declare module "socket:stream-relay/index" { - /** - * Computes rate limit predicate value for a port and address pair for a given - * threshold updating an input rates map. This method is accessed concurrently, - * the rates object makes operations atomic to avoid race conditions. - * - * @param {Map} rates - * @param {number} type - * @param {number} port - * @param {string} address - * @return {boolean} - */ - export function rateLimit(rates: Map, type: number, port: number, address: string, subclusterIdQuota: any): boolean; - export function debug(pid: any, ...args: any[]): void; - /** - * Retry delay in milliseconds for ping. - * @type {number} - */ - export const PING_RETRY: number; - /** - * Probe wait timeout in milliseconds. - * @type {number} - */ - export const PROBE_WAIT: number; - /** - * Default keep alive timeout. - * @type {number} - */ - export const DEFAULT_KEEP_ALIVE: number; - /** - * Default rate limit threshold in milliseconds. - * @type {number} - */ - export const DEFAULT_RATE_LIMIT_THRESHOLD: number; - export function getRandomPort(ports: object, p: number | null): number; - /** - * A `RemotePeer` represents an initial, discovered, or connected remote peer. - * Typically, you will not need to create instances of this class directly. - */ - export class RemotePeer { + nextId(): string; /** - * `RemotePeer` class constructor. - * @param {{ - * peerId?: string, - * address?: string, - * port?: number, - * natType?: number, - * clusters: object, - * reflectionId?: string, - * distance?: number, - * publicKey?: string, - * privateKey?: string, - * clock?: number, - * lastUpdate?: number, - * lastRequest?: number - * }} o + * @type {number} */ - constructor(o: { - peerId?: string; - address?: string; - port?: number; - natType?: number; - clusters: object; - reflectionId?: string; - distance?: number; - publicKey?: string; - privateKey?: string; - clock?: number; - lastUpdate?: number; - lastRequest?: number; - }, peer: any); - peerId: any; - address: any; - port: number; - natType: any; - clusters: {}; - pingId: any; - distance: number; - connected: boolean; - opening: number; - probed: number; - proxy: any; - clock: number; - uptime: number; - lastUpdate: number; - lastRequest: number; - localPeer: any; - write(sharedKey: any, args: any): Promise; + get length(): number; + /** + * @param {string} name + * @param {TestFn} fn + * @param {boolean} only + * @returns {void} + */ + add(name: string, fn: TestFn, only: boolean): void; + /** + * @returns {Promise} + */ + run(): Promise; + /** + * @param {(result: { total: number, success: number, fail: number }) => void} callback + * @returns {void} + */ + onFinish(callback: (result: { + total: number; + success: number; + fail: number; + }) => void): void; } - export function wrap(dgram: any): { - new (persistedState?: object | null): { - port: any; - address: any; - natType: number; - nextNatType: number; - clusters: {}; - reflectionId: any; - reflectionTimeout: any; - reflectionStage: number; - reflectionRetry: number; - reflectionFirstResponder: any; - peerId: string; - isListening: boolean; - ctime: number; - lastUpdate: number; - lastSync: number; - closing: boolean; - clock: number; - unpublished: {}; - cache: any; - uptime: number; - maxHops: number; - bdpCache: number[]; - onListening: any; - onDelete: any; - sendQueue: any[]; - firewall: any; - rates: Map; - streamBuffer: Map; - gate: Map; - returnRoutes: Map; - metrics: { - i: { - 0: number; - 1: number; - 2: number; - 3: number; - 4: number; - 5: number; - 6: number; - 7: number; - 8: number; - REJECTED: number; - }; - o: { - 0: number; - 1: number; - 2: number; - 3: number; - 4: number; - 5: number; - 6: number; - 7: number; - 8: number; - }; - }; - peers: any; - encryption: Encryption; - config: any; - _onError: (err: any) => any; - socket: any; - probeSocket: any; - /** - * An implementation for clearning an interval that can be overridden by the test suite - * @param Number the number that identifies the timer - * @return {undefined} - * @ignore - */ - _clearInterval(tid: any): undefined; - /** - * An implementation for clearning a timeout that can be overridden by the test suite - * @param Number the number that identifies the timer - * @return {undefined} - * @ignore - */ - _clearTimeout(tid: any): undefined; - /** - * An implementation of an internal timer that can be overridden by the test suite - * @return {Number} - * @ignore - */ - _setInterval(fn: any, t: any): number; - /** - * An implementation of an timeout timer that can be overridden by the test suite - * @return {Number} - * @ignore - */ - _setTimeout(fn: any, t: any): number; - /** - * A method that encapsulates the listing procedure - * @return {undefined} - * @ignore - */ - _listen(): undefined; - init(cb: any): Promise; - onReady: any; - mainLoopTimer: number; - /** - * Continuously evaluate the state of the peer and its network - * @return {undefined} - * @ignore - */ - _mainLoop(ts: any): undefined; - /** - * Enqueue packets to be sent to the network - * @param {Buffer} data - An encoded packet - * @param {number} port - The desination port of the remote host - * @param {string} address - The destination address of the remote host - * @param {Socket=this.socket} socket - The socket to send on - * @return {undefined} - * @ignore - */ - send(data: Buffer, port: number, address: string, socket?: any): undefined; - /** - * @private - */ - _scheduleSend(): void; - sendTimeout: number; - /** - * @private - */ - _dequeue(): void; - /** - * Send any unpublished packets - * @return {undefined} - * @ignore - */ - sendUnpublished(): undefined; - /** - * Get the serializable state of the peer (can be passed to the constructor or create method) - * @return {undefined} - */ - getState(): undefined; - /** - * Get a selection of known peers - * @return {Array} - * @ignore - */ - getPeers(packet: any, peers: any, ignorelist: any, filter?: (o: any) => any): Array; - /** - * Send an eventually consistent packet to a selection of peers (fanout) - * @return {undefined} - * @ignore - */ - mcast(packet: any, ignorelist?: any[]): undefined; - /** - * The process of determining this peer's NAT behavior (firewall and dependentness) - * @return {undefined} - * @ignore - */ - requestReflection(): undefined; - probeReflectionTimeout: any; - /** - * Ping another peer - * @return {PacketPing} - * @ignore - */ - ping(peer: any, withRetry: any, props: any, socket: any): PacketPing; - getPeer(id: any): any; - /** - * This should be called at least once when an app starts to multicast - * this peer, and starts querying the network to discover peers. - * @param {object} keys - Created by `Encryption.createKeyPair()`. - * @param {object=} args - Options - * @param {number=MAX_BANDWIDTH} args.rateLimit - How many requests per second to allow for this subclusterId. - * @return {RemotePeer} - */ - join(sharedKey: any, args?: object | undefined): RemotePeer; - /** - * @param {Packet} T - The constructor to be used to create packets. - * @param {Any} message - The message to be split and packaged. - * @return {Array>} - * @ignore - */ - _message2packets(T: Packet, message: Any, args: any): Array>; - /** - * Sends a packet into the network that will be replicated and buffered. - * Each peer that receives it will buffer it until TTL and then replicate - * it provided it has has not exceeded their maximum number of allowed hops. - * - * @param {object} keys - the public and private key pair created by `Encryption.createKeyPair()`. - * @param {object} args - The arguments to be applied. - * @param {Buffer} args.message - The message to be encrypted by keys and sent. - * @param {Packet=} args.packet - The previous packet in the packet chain. - * @param {Buffer} args.usr1 - 32 bytes of arbitrary clusterId in the protocol framing. - * @param {Buffer} args.usr2 - 32 bytes of arbitrary clusterId in the protocol framing. - * @return {Array} - */ - publish(sharedKey: any, args: { - message: Buffer; - packet?: Packet | undefined; - usr1: Buffer; - usr2: Buffer; - }): Array; - /** - * @return {undefined} - */ - sync(peer: any): undefined; - close(): void; - /** - * Deploy a query into the network - * @return {undefined} - * - */ - query(query: any): undefined; - /** - * - * This is a default implementation for deciding what to summarize - * from the cache when receiving a request to sync. that can be overridden - * - */ - cachePredicate(packet: any): boolean; - /** - * A connection was made, add the peer to the local list of known - * peers and call the onConnection if it is defined by the user. - * - * @return {undefined} - * @ignore - */ - _onConnection(packet: any, peerId: any, port: any, address: any, proxy: any, socket: any): undefined; - connections: Map; - /** - * Received a Sync Packet - * @return {undefined} - * @ignore - */ - _onSync(packet: any, port: any, address: any): undefined; - /** - * Received a Query Packet - * - * a -> b -> c -> (d) -> c -> b -> a - * - * @return {undefined} - * @example - * - * ```js - * peer.onQuery = (packet) => { - * // - * // read a database or something - * // - * return { - * message: Buffer.from('hello'), - * publicKey: '', - * privateKey: '' - * } - * } - * ``` - */ - _onQuery(packet: any, port: any, address: any): undefined; - /** - * Received a Ping Packet - * @return {undefined} - * @ignore - */ - _onPing(packet: any, port: any, address: any): undefined; - /** - * Received a Pong Packet - * @return {undefined} - * @ignore - */ - _onPong(packet: any, port: any, address: any): undefined; - /** - * Received an Intro Packet - * @return {undefined} - * @ignore - */ - _onIntro(packet: any, port: any, address: any, _: any, opts?: { - attempts: number; - }): undefined; - socketPool: any[]; - /** - * Received an Join Packet - * @return {undefined} - * @ignore - */ - _onJoin(packet: any, port: any, address: any, data: any): undefined; - /** - * Received an Publish Packet - * @return {undefined} - * @ignore - */ - _onPublish(packet: any, port: any, address: any, data: any): undefined; - /** - * Received an Stream Packet - * @return {undefined} - * @ignore - */ - _onStream(packet: any, port: any, address: any, data: any): undefined; - /** - * Received any packet on the probe port to determine the firewall: - * are you port restricted, host restricted, or unrestricted. - * @return {undefined} - * @ignore - */ - _onProbeMessage(data: any, { port, address }: { - port: any; - address: any; - }): undefined; - /** - * When a packet is received it is decoded, the packet contains the type - * of the message. Based on the message type it is routed to a function. - * like WebSockets, don't answer queries unless we know its another SRP peer. - * - * @param {Buffer|Uint8Array} data - * @param {{ port: number, address: string }} info - */ - _onMessage(data: Buffer | Uint8Array, { port, address }: { - port: number; - address: string; - }): Promise; - }; + /** + * @ignore + */ + export const GLOBAL_TEST_RUNNER: TestRunner; + export default test; + export type testWithProperties = { + (name: string, fn?: TestFn): void; + only(name: string, fn?: TestFn): void; + skip(name: string, fn?: TestFn): void; }; - export default wrap; - import { Packet } from "socket:stream-relay/packets"; - import { sha256 } from "socket:stream-relay/packets"; - import { Cache } from "socket:stream-relay/cache"; - import { Encryption } from "socket:stream-relay/encryption"; - import * as NAT from "socket:stream-relay/nat"; - import { Buffer } from "socket:buffer"; - import { PacketPing } from "socket:stream-relay/packets"; - import { PacketPublish } from "socket:stream-relay/packets"; - export { Packet, sha256, Cache, Encryption, NAT }; -} -declare module "socket:stream-relay/sugar" { - function _default(dgram: object, events: object): Promise; - export default _default; -} -declare module "socket:node/index" { - export default network; - export const network: Promise; - import { Cache } from "socket:stream-relay/index"; - import { sha256 } from "socket:stream-relay/index"; - import { Encryption } from "socket:stream-relay/index"; - import { Packet } from "socket:stream-relay/index"; - import { NAT } from "socket:stream-relay/index"; - export { Cache, sha256, Encryption, Packet, NAT }; -} -declare module "socket:index" { - import { network } from "socket:node/index"; - import { Cache } from "socket:node/index"; - import { sha256 } from "socket:node/index"; - import { Encryption } from "socket:node/index"; - import { Packet } from "socket:node/index"; - import { NAT } from "socket:node/index"; - export { network, Cache, sha256, Encryption, Packet, NAT }; -} -declare module "socket:test/fast-deep-equal" { - export default function equal(a: any, b: any): boolean; + export type TestFn = (t: Test) => (void | Promise); } -declare module "socket:test/context" { - export default function _default(GLOBAL_TEST_RUNNER: any): void; + +declare module "socket:test" { + export * from "socket:test/index"; + export default test; + import test from "socket:test/index"; } -declare module "socket:test/dom-helpers" { + +declare module "socket:commonjs/builtins" { /** - * Converts querySelector string to an HTMLElement or validates an existing HTMLElement. - * - * @export - * @param {string|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. - * @returns {Element} The HTMLElement, Element, or Window that corresponds to the selector. - * @throws {Error} Throws an error if the `selector` is not a string that resolves to an HTMLElement or not an instance of HTMLElement, Element, or Window. - * + * Defines a builtin module by name making a shallow copy of the + * module exports. + * @param {string} + * @param {object} exports */ - export function toElement(selector: string | Element): Element; + export function defineBuiltin(name: any, exports: object, copy?: boolean): void; /** - * Waits for an element to appear in the DOM and resolves the promise when it does. - * - * @export - * @param {Object} args - Configuration arguments. - * @param {string} [args.selector] - The CSS selector to look for. - * @param {boolean} [args.visible=true] - Whether the element should be visible. - * @param {number} [args.timeout=defaultTimeout] - Time in milliseconds to wait before rejecting the promise. - * @param {() => HTMLElement | Element | null | undefined} [lambda] - An optional function that returns the element. Used if the `selector` is not provided. - * @returns {Promise} - A promise that resolves to the found element. - * - * @throws {Error} - Throws an error if neither `lambda` nor `selector` is provided. - * @throws {Error} - Throws an error if the element is not found within the timeout. - * - * @example - * ```js - * waitFor({ selector: '#my-element', visible: true, timeout: 5000 }) - * .then(el => console.log('Element found:', el)) - * .catch(err => console.log('Element not found:', err)); - * ``` + * Predicate to determine if a given module name is a builtin module. + * @param {string} name + * @param {{ builtins?: object }} + * @return {boolean} */ - export function waitFor(args: { - selector?: string; - visible?: boolean; - timeout?: number; - }, lambda?: () => HTMLElement | Element | null | undefined): Promise; + export function isBuiltin(name: string, options?: any): boolean; /** - * Waits for an element's text content to match a given string or regular expression. - * - * @export - * @param {Object} args - Configuration arguments. - * @param {Element} args.element - The root element from which to begin searching. - * @param {string} [args.text] - The text to search for within elements. - * @param {RegExp} [args.regex] - A regular expression to match against element text content. - * @param {boolean} [args.multipleTags=false] - Whether to look for text across multiple sibling elements. - * @param {number} [args.timeout=defaultTimeout] - Time in milliseconds to wait before rejecting the promise. - * @returns {Promise} - A promise that resolves to the found element or null. - * - * @example - * ```js - * waitForText({ element: document.body, text: 'Hello', timeout: 5000 }) - * .then(el => console.log('Element found:', el)) - * .catch(err => console.log('Element not found:', err)); - * ``` + * Gets a builtin module by name. + * @param {string} name + * @param {{ builtins?: object }} [options] + * @return {any} */ - export function waitForText(args: { - element: Element; - text?: string; - regex?: RegExp; - multipleTags?: boolean; - timeout?: number; - }): Promise; + export function getBuiltin(name: string, options?: { + builtins?: object; + }): any; /** - * @export - * @param {Object} args - Arguments - * @param {string | Event} args.event - The event to dispatch. - * @param {HTMLElement | Element | window} [args.element=window] - The element to dispatch the event on. - * @returns {void} - * - * @throws {Error} Throws an error if the `event` is not a string that can be converted to a CustomEvent or not an instance of Event. + * A mapping of builtin modules + * @type {object} */ - export function event(args: { - event: string | Event; - element?: HTMLElement | Element | (Window & typeof globalThis); - }): void; + export const builtins: object; /** - * @export - * Copy pasted from https://raw.githubusercontent.com/testing-library/jest-dom/master/src/to-be-visible.js - * @param {Element | HTMLElement} element - * @param {Element | HTMLElement} [previousElement] - * @returns {boolean} + * Known runtime specific builtin modules. + * @type {Set} */ - export function isElementVisible(element: Element | HTMLElement, previousElement?: Element | HTMLElement): boolean; + export const runtimeModules: Set; + export default builtins; } -declare module "socket:test/index" { - /** - * @returns {number} - The default timeout for tests in milliseconds. - */ - export function getDefaultTestRunnerTimeout(): number; + +declare module "socket:commonjs/cache" { /** - * @param {string} name - * @param {TestFn} [fn] - * @returns {void} + * @typedef {{ + * types?: object, + * loader?: import('./loader.js').Loader + * }} CacheOptions */ - export function only(name: string, fn?: TestFn): void; + export const CACHE_CHANNEL_MESSAGE_ID: "id"; + export const CACHE_CHANNEL_MESSAGE_REPLICATE: "replicate"; /** - * @param {string} _name - * @param {TestFn} [_fn] - * @returns {void} + * @typedef {{ + * name: string + * }} StorageOptions */ - export function skip(_name: string, _fn?: TestFn): void; /** - * @param {boolean} strict - * @returns {void} + * An storage context object with persistence and durability + * for service worker storages. */ - export function setStrict(strict: boolean): void; + export class Storage extends EventTarget { + /** + * Maximum entries that will be restored from storage into the context object. + * @type {number} + */ + static MAX_CONTEXT_ENTRIES: number; + /** + * A mapping of known `Storage` instances. + * @type {Map} + */ + static instances: Map; + /** + * Opens an storage for a particular name. + * @param {StorageOptions} options + * @return {Promise} + */ + static open(options: StorageOptions): Promise; + /** + * `Storage` class constructor + * @ignore + * @param {StorageOptions} options + */ + constructor(options: StorageOptions); + /** + * A reference to the currently opened storage database. + * @type {import('../internal/database.js').Database} + */ + get database(): import("socket:internal/database").Database; + /** + * `true` if the storage is opened, otherwise `false`. + * @type {boolean} + */ + get opened(): boolean; + /** + * `true` if the storage is opening, otherwise `false`. + * @type {boolean} + */ + get opening(): boolean; + /** + * A proxied object for reading and writing storage state. + * Values written to this object must be cloneable with respect to the + * structured clone algorithm. + * @see {https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm} + * @type {Proxy} + */ + get context(): ProxyConstructor; + /** + * The current storage name. This value is also used as the + * internal database name. + * @type {string} + */ + get name(): string; + /** + * A promise that resolves when the storage is opened. + * @type {Promise?} + */ + get ready(): Promise; + /** + * @ignore + * @param {Promise} promise + */ + forwardRequest(promise: Promise): Promise; + /** + * Resets the current storage to an empty state. + */ + reset(): Promise; + /** + * Synchronizes database entries into the storage context. + */ + sync(options?: any): Promise; + /** + * Opens the storage. + * @ignore + */ + open(options?: any): Promise; + /** + * Closes the storage database, purging existing state. + * @ignore + */ + close(): Promise; + #private; + } /** - * @typedef {{ - * (name: string, fn?: TestFn): void - * only(name: string, fn?: TestFn): void - * skip(name: string, fn?: TestFn): void - * }} testWithProperties - * @ignore + * A container for `Snapshot` data storage. */ + export class SnapshotData { + /** + * `SnapshotData` class constructor. + * @param {object=} [data] + */ + constructor(data?: object | undefined); + toJSON: () => this; + [Symbol.toStringTag]: string; + } /** - * @type {testWithProperties} - * @param {string} name - * @param {TestFn} [fn] - * @returns {void} + * A container for storing a snapshot of the cache data. */ - export function test(name: string, fn?: TestFn): void; - export namespace test { - export { only }; - export { skip }; + export class Snapshot { + /** + * @type {typeof SnapshotData} + */ + static Data: typeof SnapshotData; + /** + * A reference to the snapshot data. + * @type {Snapshot.Data} + */ + get data(): typeof SnapshotData; + /** + * @ignore + * @return {object} + */ + toJSON(): object; + #private; } /** - * @typedef {(t: Test) => (void | Promise)} TestFn + * An interface for managing and performing operations on a collection + * of `Cache` objects. */ + export class CacheCollection { + /** + * `CacheCollection` class constructor. + * @ignore + * @param {Cache[]|Record=} [collection] + */ + constructor(collection?: (Cache[] | Record) | undefined); + /** + * Adds a `Cache` instance to the collection. + * @param {string|Cache} name + * @param {Cache=} [cache] + * @param {boolean} + */ + add(name: string | Cache, cache?: Cache | undefined): any; + /** + * Calls a method on each `Cache` object in the collection. + * @param {string} method + * @param {...any} args + * @return {Promise>} + */ + call(method: string, ...args: any[]): Promise>; + restore(): Promise>; + reset(): Promise>; + snapshot(): Promise>; + get(key: any): Promise>; + delete(key: any): Promise>; + keys(key: any): Promise>; + values(key: any): Promise>; + clear(key: any): Promise>; + } /** - * @class + * A container for a shared cache that lives for the life time of + * application execution. Updates to this storage are replicated to other + * instances in the application context, including windows and workers. */ - export class Test { + export class Cache { /** - * @constructor + * A globally shared type mapping for the cache to use when + * derserializing a value. + * @type {Map} + */ + static types: Map; + /** + * A globally shared cache store keyed by cache name. This is useful so + * when multiple instances of a `Cache` are created, they can share the + * same data store, reducing duplications. + * @type {Record} + */ + static shared: Record>; + /** + * A mapping of opened `Storage` instances. + * @type {Map} + */ + static storages: Map; + /** + * The `Cache.Snapshot` class. + * @type {typeof Snapshot} + */ + static Snapshot: typeof Snapshot; + /** + * The `Cache.Storage` class + * @type {typeof Storage} + */ + static Storage: typeof Storage; + /** + * Creates a snapshot of the current cache which can be serialized and + * stored in persistent storage. + * @return {Snapshot} + */ + static snapshot(): Snapshot; + /** + * Restore caches from persistent storage. + * @param {string[]} names + * @return {Promise} + */ + static restore(names: string[]): Promise; + /** + * `Cache` class constructor. * @param {string} name - * @param {TestFn} fn - * @param {TestRunner} runner + * @param {CacheOptions=} [options] */ - constructor(name: string, fn: TestFn, runner: TestRunner); + constructor(name: string, options?: CacheOptions | undefined); /** + * The unique ID for this cache. * @type {string} - * @ignore */ - name: string; + get id(): string; /** - * @type {null|number} - * @ignore + * The loader associated with this cache. + * @type {import('./loader.js').Loader} */ - _planned: null | number; + get loader(): import("socket:commonjs/loader").Loader; /** - * @type {null|number} - * @ignore + * A reference to the persisted storage. + * @type {Storage} */ - _actual: null | number; + get storage(): Storage; /** - * @type {TestFn} - * @ignore + * The cache name + * @type {string} */ - fn: TestFn; + get name(): string; /** - * @type {TestRunner} - * @ignore + * The underlying cache data map. + * @type {Map} */ - runner: TestRunner; + get data(): Map; /** - * @type{{ pass: number, fail: number }} - * @ignore + * The broadcast channel associated with this cach. + * @type {BroadcastChannel} */ - _result: { - pass: number; - fail: number; - }; + get channel(): BroadcastChannel; /** - * @type {boolean} - * @ignore + * The size of the cache. + * @type {number} */ - done: boolean; + get size(): number; /** - * @type {boolean} - * @ignore + * @type {Map} */ - strict: boolean; + get types(): Map; /** - * @param {string} msg - * @returns {void} + * Resets the cache map and persisted storage. */ - comment(msg: string): void; + reset(): Promise; /** - * Plan the number of assertions. - * - * @param {number} n - * @returns {void} + * Restores cache data from storage. */ - plan(n: number): void; + restore(): Promise; /** - * @template T - * @param {T} actual - * @param {T} expected - * @param {string} [msg] - * @returns {void} + * Creates a snapshot of the current cache which can be serialized and + * stored in persistent storage. + * @return {Snapshot.Data} */ - deepEqual(actual: T, expected: T, msg?: string): void; + snapshot(): typeof SnapshotData; /** - * @template T - * @param {T} actual - * @param {T} expected - * @param {string} [msg] - * @returns {void} + * Get a value at `key`. + * @param {string} key + * @return {object|undefined} */ - notDeepEqual(actual: T_1, expected: T_1, msg?: string): void; + get(key: string): object | undefined; /** - * @template T - * @param {T} actual - * @param {T} expected - * @param {string} [msg] - * @returns {void} + * Set `value` at `key`. + * @param {string} key + * @param {object} value + * @return {Cache} */ - equal(actual: T_2, expected: T_2, msg?: string): void; + set(key: string, value: object): Cache; /** - * @param {unknown} actual - * @param {unknown} expected - * @param {string} [msg] - * @returns {void} + * Returns `true` if `key` is in cache, otherwise `false`. + * @param {string} + * @return {boolean} */ - notEqual(actual: unknown, expected: unknown, msg?: string): void; + has(key: any): boolean; /** - * @param {string} [msg] - * @returns {void} + * Delete a value at `key`. + * This does not replicate to shared caches. + * @param {string} key + * @return {boolean} */ - fail(msg?: string): void; + delete(key: string): boolean; /** - * @param {unknown} actual - * @param {string} [msg] - * @returns {void} + * Returns an iterator for all cache keys. + * @return {object} */ - ok(actual: unknown, msg?: string): void; + keys(): object; /** - * @param {string} [msg] - * @returns {void} + * Returns an iterator for all cache values. + * @return {object} */ - pass(msg?: string): void; + values(): object; /** - * @param {Error | null | undefined} err - * @param {string} [msg] - * @returns {void} + * Returns an iterator for all cache entries. + * @return {object} */ - ifError(err: Error | null | undefined, msg?: string): void; + entries(): object; /** - * @param {Function} fn - * @param {RegExp | any} [expected] - * @param {string} [message] - * @returns {void} + * Clears all entries in the cache. + * This does not replicate to shared caches. + * @return {undefined} */ - throws(fn: Function, expected?: RegExp | any, message?: string): void; + clear(): undefined; /** - * Sleep for ms with an optional msg - * - * @param {number} ms - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.sleep(100) - * ``` + * Enumerates entries in map calling `callback(value, key + * @param {function(object, string, Cache): any} callback */ - sleep(ms: number, msg?: string): Promise; + forEach(callback: (arg0: object, arg1: string, arg2: Cache) => any): void; /** - * Request animation frame with an optional msg. Falls back to a 0ms setTimeout when - * tests are run headlessly. - * - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.requestAnimationFrame() - * ``` + * Broadcasts a replication to other shared caches. */ - requestAnimationFrame(msg?: string): Promise; + replicate(): this; /** - * Dispatch the `click`` method on an element specified by selector. - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.click('.class button', 'Click a button') - * ``` + * Destroys the cache. This function stops the broadcast channel and removes + * and listeners */ - click(selector: string | HTMLElement | Element, msg?: string): Promise; + destroy(): void; /** - * Dispatch the click window.MouseEvent on an element specified by selector. - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.eventClick('.class button', 'Click a button with an event') - * ``` + * @ignore */ - eventClick(selector: string | HTMLElement | Element, msg?: string): Promise; + [Symbol.iterator](): any; + #private; + } + export default Cache; + export type CacheOptions = { + types?: object; + loader?: import("socket:commonjs/loader").Loader; + }; + export type StorageOptions = { + name: string; + }; +} + +declare module "socket:commonjs/loader" { + /** + * @typedef {{ + * extensions?: string[] | Set + * origin?: URL | string, + * statuses?: Cache + * cache?: { response?: Cache, status?: Cache }, + * headers?: Headers | Map | object | string[][] + * }} LoaderOptions + */ + /** + * @typedef {{ + * loader?: Loader, + * origin?: URL | string + * }} RequestOptions + */ + /** + * @typedef {{ + * headers?: Headers | object | array[], + * status?: number + * }} RequestStatusOptions + */ + /** + * @typedef {{ + * headers?: Headers | object + * }} RequestLoadOptions + */ + /** + * @typedef {{ + * request?: Request, + * headers?: Headers, + * status?: number, + * buffer?: ArrayBuffer, + * text?: string + * }} ResponseOptions + */ + /** + * A container for the status of a CommonJS resource. A `RequestStatus` object + * represents meta data for a `Request` that comes from a preflight + * HTTP HEAD request. + */ + export class RequestStatus { /** - * Dispatch an event on the target. - * - * @param {string | Event} event - The event name or Event instance to dispatch. - * @param {string|HTMLElement|Element} target - A CSS selector string, or an instance of HTMLElement, or Element to dispatch the event on. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.dispatchEvent('my-event', '#my-div', 'Fire the my-event event') - * ``` + * Creates a `RequestStatus` from JSON input. + * @param {object} json + * @return {RequestStatus} + */ + static from(json: object, options: any): RequestStatus; + /** + * `RequestStatus` class constructor. + * @param {Request} request + * @param {RequestStatusOptions} [options] + */ + constructor(request: Request, options?: RequestStatusOptions); + set request(request: Request); + /** + * The `Request` object associated with this `RequestStatus` object. + * @type {Request} + */ + get request(): Request; + /** + * The unique ID of this `RequestStatus`, which is the absolute URL as a string. + * @type {string} + */ + get id(): string; + /** + * The origin for this `RequestStatus` object. + * @type {string} + */ + get origin(): string; + /** + * A HTTP status code for this `RequestStatus` object. + * @type {number|undefined} + */ + get status(): number; + /** + * An alias for `status`. + * @type {number|undefined} */ - dispatchEvent(event: string | Event, target: string | HTMLElement | Element, msg?: string): Promise; + get value(): number; /** - * Call the focus method on element specified by selector. - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.focus('#my-div') - * ``` + * @ignore */ - focus(selector: string | HTMLElement | Element, msg?: string): Promise; + get valueOf(): number; /** - * Call the blur method on element specified by selector. - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.blur('#my-div') - * ``` + * The HTTP headers for this `RequestStatus` object. + * @type {Headers} */ - blur(selector: string | HTMLElement | Element, msg?: string): Promise; + get headers(): Headers; /** - * Consecutively set the str value of the element specified by selector to simulate typing. - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. - * @param {string} str - The string to type into the :focus element. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.typeValue('#my-div', 'Hello World', 'Type "Hello World" into #my-div') - * ``` + * The resource location for this `RequestStatus` object. This value is + * determined from the 'Content-Location' header, if available, otherwise + * it is derived from the request URL pathname (including the query string). + * @type {string} */ - type(selector: string | HTMLElement | Element, str: string, msg?: string): Promise; + get location(): string; /** - * appendChild an element el to a parent selector element. - * - * @param {string|HTMLElement|Element} parentSelector - A CSS selector string, or an instance of HTMLElement, or Element to appendChild on. - * @param {HTMLElement|Element} el - A element to append to the parent element. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * const myElement = createElement('div') - * await t.appendChild('#parent-selector', myElement, 'Append myElement into #parent-selector') - * ``` + * `true` if the response status is considered OK, otherwise `false`. + * @type {boolean} */ - appendChild(parentSelector: string | HTMLElement | Element, el: HTMLElement | Element, msg?: string): Promise; + get ok(): boolean; /** - * Remove an element from the DOM. - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element to remove from the DOM. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.removeElement('#dom-selector', 'Remove #dom-selector') - * ``` + * Loads the internal state for this `RequestStatus` object. + * @param {RequestLoadOptions|boolean} [options] + * @return {RequestStatus} */ - removeElement(selector: string | HTMLElement | Element, msg?: string): Promise; + load(options?: RequestLoadOptions | boolean): RequestStatus; /** - * Test if an element is visible - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element to test visibility on. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.elementVisible('#dom-selector','Element is visible') - * ``` + * Converts this `RequestStatus` to JSON. + * @ignore + * @return {{ + * id: string, + * origin: string | null, + * status: number, + * headers: Array + * request: object | null | undefined + * }} */ - elementVisible(selector: string | HTMLElement | Element, msg?: string): Promise; + toJSON(includeRequest?: boolean): { + id: string; + origin: string | null; + status: number; + headers: Array; + request: object | null | undefined; + }; + #private; + } + /** + * A container for a synchronous CommonJS request to local resource or + * over the network. + */ + export class Request { /** - * Test if an element is invisible - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element to test visibility on. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.elementInvisible('#dom-selector','Element is invisible') - * ``` + * Creates a `Request` instance from JSON input + * @param {object} json + * @param {RequestOptions=} [options] + * @return {Request} */ - elementInvisible(selector: string | HTMLElement | Element, msg?: string): Promise; + static from(json: object, options?: RequestOptions | undefined): Request; /** - * Test if an element is invisible - * - * @param {string|(() => HTMLElement|Element|null|undefined)} querySelectorOrFn - A query string or a function that returns an element. - * @param {Object} [opts] - * @param {boolean} [opts.visible] - The element needs to be visible. - * @param {number} [opts.timeout] - The maximum amount of time to wait. - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.waitFor('#dom-selector', { visible: true },'#dom-selector is on the page and visible') - * ``` + * `Request` class constructor. + * @param {URL|string} url + * @param {URL|string=} [origin] + * @param {RequestOptions=} [options] */ - waitFor(querySelectorOrFn: string | (() => HTMLElement | Element | null | undefined), opts?: { - visible?: boolean; - timeout?: number; - }, msg?: string): Promise; + constructor(url: URL | string, origin?: (URL | string) | undefined, options?: RequestOptions | undefined); /** - * @typedef {Object} WaitForTextOpts - * @property {string} [text] - The text to wait for - * @property {number} [timeout] - * @property {Boolean} [multipleTags] - * @property {RegExp} [regex] The regex to wait for + * The unique ID of this `Request`, which is the absolute URL as a string. + * @type {string} */ + get id(): string; /** - * Test if an element is invisible - * - * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. - * @param {WaitForTextOpts | string | RegExp} [opts] - * @param {string} [msg] - * @returns {Promise} - * - * @example - * ```js - * await t.waitForText('#dom-selector', 'Text to wait for') - * ``` - * - * @example - * ```js - * await t.waitForText('#dom-selector', /hello/i) - * ``` - * - * @example - * ```js - * await t.waitForText('#dom-selector', { - * text: 'Text to wait for', - * multipleTags: true - * }) - * ``` + * The absolute `URL` of this `Request` object. + * @type {URL} */ - waitForText(selector: string | HTMLElement | Element, opts?: string | RegExp | { - /** - * - The text to wait for - */ - text?: string; - timeout?: number; - multipleTags?: boolean; - /** - * The regex to wait for - */ - regex?: RegExp; - }, msg?: string): Promise; + get url(): URL; /** - * Run a querySelector as an assert and also get the results - * - * @param {string} selector - A CSS selector string, or an instance of HTMLElement, or Element to select. - * @param {string} [msg] - * @returns {HTMLElement | Element} - * - * @example - * ```js - * const element = await t.querySelector('#dom-selector') - * ``` + * The origin for this `Request`. + * @type {string} */ - querySelector(selector: string, msg?: string): HTMLElement | Element; + get origin(): string; /** - * Run a querySelectorAll as an assert and also get the results - * - * @param {string} selector - A CSS selector string, or an instance of HTMLElement, or Element to select. - * @param {string} [msg] - @returns {Array} - * - * @example - * ```js - * const elements = await t.querySelectorAll('#dom-selector', '') - * ``` + * The `Loader` for this `Request` object. + * @type {Loader?} */ - querySelectorAll(selector: string, msg?: string): Array; + get loader(): Loader; /** - * Retrieves the computed styles for a given element. - * - * @param {string|Element} selector - The CSS selector or the Element object for which to get the computed styles. - * @param {string} [msg] - An optional message to display when the operation is successful. Default message will be generated based on the type of selector. - * @returns {CSSStyleDeclaration} - The computed styles of the element. - * @throws {Error} - Throws an error if the element has no `ownerDocument` or if `ownerDocument.defaultView` is not available. - * - * @example - * ```js - * // Using CSS selector - * const style = getComputedStyle('.my-element', 'Custom success message'); - * ``` - * - * @example - * ```js - * // Using Element object - * const el = document.querySelector('.my-element'); - * const style = getComputedStyle(el); - * ``` + * The `RequestStatus` for this `Request` + * @type {RequestStatus} */ - getComputedStyle(selector: string | Element, msg?: string): CSSStyleDeclaration; + get status(): RequestStatus; /** - * @param {boolean} pass - * @param {unknown} actual - * @param {unknown} expected - * @param {string} description - * @param {string} operator - * @returns {void} - * @ignore + * Loads the CommonJS source file, optionally checking the `Loader` cache + * first, unless ignored when `options.cache` is `false`. + * @param {RequestLoadOptions=} [options] + * @return {Response} */ - _assert(pass: boolean, actual: unknown, expected: unknown, description: string, operator: string): void; + load(options?: RequestLoadOptions | undefined): Response; /** - * @returns {Promise<{ - * pass: number, - * fail: number - * }>} + * Converts this `Request` to JSON. + * @ignore + * @return {{ + * url: string, + * status: object | undefined + * }} */ - run(): Promise<{ - pass: number; - fail: number; - }>; + toJSON(includeStatus?: boolean): { + url: string; + status: object | undefined; + }; + #private; } /** - * @class + * A container for a synchronous CommonJS request response for a local resource + * or over the network. */ - export class TestRunner { + export class Response { /** - * @constructor - * @param {(lines: string) => void} [report] + * Creates a `Response` from JSON input + * @param {obejct} json + * @param {ResponseOptions=} [options] + * @return {Response} */ - constructor(report?: (lines: string) => void); + static from(json: obejct, options?: ResponseOptions | undefined): Response; /** - * @type {(lines: string) => void} - * @ignore + * `Response` class constructor. + * @param {Request|ResponseOptions} request + * @param {ResponseOptions=} [options] */ - report: (lines: string) => void; + constructor(request: Request | ResponseOptions, options?: ResponseOptions | undefined); /** - * @type {Test[]} + * The unique ID of this `Response`, which is the absolute + * URL of the request as a string. + * @type {string} + */ + get id(): string; + /** + * The `Request` object associated with this `Response` object. + * @type {Request} + */ + get request(): Request; + /** + * The response headers from the associated request. + * @type {Headers} + */ + get headers(): Headers; + /** + * The `Loader` associated with this `Response` object. + * @type {Loader?} + */ + get loader(): Loader; + /** + * The `Response` status code from the associated `Request` object. + * @type {number} + */ + get status(): number; + /** + * The `Response` string from the associated `Request` + * @type {string} + */ + get text(): string; + /** + * The `Response` array buffer from the associated `Request` + * @type {ArrayBuffer?} + */ + get buffer(): ArrayBuffer; + /** + * `true` if the response is considered OK, otherwise `false`. + * @type {boolean} + */ + get ok(): boolean; + /** + * Converts this `Response` to JSON. * @ignore + * @return {{ + * id: string, + * text: string, + * status: number, + * buffer: number[] | null, + * headers: Array + * }} */ - tests: Test[]; + toJSON(): { + id: string; + text: string; + status: number; + buffer: number[] | null; + headers: Array; + }; + #private; + } + /** + * A container for loading CommonJS module sources + */ + export class Loader { /** - * @type {Test[]} - * @ignore + * A request class used by `Loader` objects. + * @type {typeof Request} */ - onlyTests: Test[]; + static Request: typeof Request; /** - * @type {boolean} - * @ignore + * A response class used by `Loader` objects. + * @type {typeof Request} */ - scheduled: boolean; + static Response: typeof Request; /** - * @type {number} - * @ignore + * Resolves a given module URL to an absolute URL with an optional `origin`. + * @param {URL|string} url + * @param {URL|string} [origin] + * @return {string} */ - _id: number; + static resolve(url: URL | string, origin?: URL | string): string; /** - * @type {boolean} - * @ignore + * Default extensions for a loader. + * @type {Set} */ - completed: boolean; + static defaultExtensions: Set; /** - * @type {boolean} - * @ignore + * `Loader` class constructor. + * @param {string|URL|LoaderOptions} origin + * @param {LoaderOptions=} [options] */ - rethrowExceptions: boolean; + constructor(origin: string | URL | LoaderOptions, options?: LoaderOptions | undefined); /** - * @type {boolean} - * @ignore + * The internal caches for this `Loader` object. + * @type {{ response: Cache, status: Cache }} */ - strict: boolean; + get cache(): { + response: Cache; + status: Cache; + }; /** - * @type {function | void} - * @ignore + * Headers used in too loader requests. + * @type {Headers} */ - _onFinishCallback: Function | void; + get headers(): Headers; /** - * @returns {string} + * A set of supported `Loader` extensions. + * @type {Set} */ - nextId(): string; + get extensions(): Set; + set origin(origin: string); /** - * @type {number} + * The origin of this `Loader` object. + * @type {string} */ - get length(): number; + get origin(): string; /** - * @param {string} name - * @param {TestFn} fn - * @param {boolean} only - * @returns {void} + * Loads a CommonJS module source file at `url` with an optional `origin`, which + * defaults to the application origin. + * @param {URL|string} url + * @param {URL|string|object} [origin] + * @param {RequestOptions=} [options] + * @return {Response} */ - add(name: string, fn: TestFn, only: boolean): void; + load(url: URL | string, origin?: URL | string | object, options?: RequestOptions | undefined): Response; /** - * @returns {Promise} + * Queries the status of a CommonJS module source file at `url` with an + * optional `origin`, which defaults to the application origin. + * @param {URL|string} url + * @param {URL|string|object} [origin] + * @param {RequestOptions=} [options] + * @return {RequestStatus} */ - run(): Promise; + status(url: URL | string, origin?: URL | string | object, options?: RequestOptions | undefined): RequestStatus; /** - * @param {(result: { total: number, success: number, fail: number }) => void} callback - * @returns {void} + * Resolves a given module URL to an absolute URL based on the loader origin. + * @param {URL|string} url + * @param {URL|string} [origin] + * @return {string} */ - onFinish(callback: (result: { - total: number; - success: number; - fail: number; - }) => void): void; - } - /** - * @ignore - */ - export const GLOBAL_TEST_RUNNER: TestRunner; - export default test; - export type testWithProperties = { - (name: string, fn?: TestFn): void; - only(name: string, fn?: TestFn): void; - skip(name: string, fn?: TestFn): void; - }; - export type TestFn = (t: Test) => (void | Promise); -} -declare module "socket:test" { - export * from "socket:test/index"; - export default test; - import test from "socket:test/index"; -} -declare module "socket:internal/globals" { - /** - * Gets a runtime global value by name. - * @ignore - * @param {string} name - * @return {any|null} - */ - export function get(name: string): any | null; - /** - * Symbolic global registry - * @ignore - */ - export class GlobalsRegistry { - get global(): any; - symbol(name: any): symbol; - register(name: any, value: any): any; - get(name: any): any; - } - export default registry; - const registry: any; -} -declare module "socket:internal/shared-worker" { - export function getSharedWorkerImplementationForPlatform(): { - new (scriptURL: string | URL, options?: string | WorkerOptions): SharedWorker; - prototype: SharedWorker; - } | typeof SharedHybridWorkerProxy | typeof SharedHybridWorker; - export class SharedHybridWorkerProxy extends EventTarget { - constructor(url: any, options: any); - onChannelMessage(event: any): void; - get id(): any; - get port(): any; - #private; - } - export class SharedHybridWorker extends EventTarget { - constructor(url: any, nameOrOptions: any); - get port(): any; + resolve(url: URL | string, origin?: URL | string): string; #private; } - export const SharedWorker: { - new (scriptURL: string | URL, options?: string | WorkerOptions): SharedWorker; - prototype: SharedWorker; - } | typeof SharedHybridWorkerProxy | typeof SharedHybridWorker; - export default SharedWorker; -} -declare module "socket:worker" { - export { SharedWorker }; - /** - * @type {import('dom').Worker} - */ - export const Worker: any; - export default Worker; - import SharedWorker from "socket:internal/shared-worker"; -} -declare module "socket:vm" { - /** - * @ignore - * @param {object[]} transfer - * @param {object} object - * @param {object=} [options] - * @return {object[]} - */ - export function findMessageTransfers(transfers: any, object: object, options?: object | undefined): object[]; - /** - * @ignore - * @param {object} context - */ - export function applyInputContextReferences(context: object): void; - /** - * @ignore - * @param {object} context - */ - export function applyOutputContextReferences(context: object): void; - /** - * @ignore - * @param {object} context - */ - export function filterNonTransferableValues(context: object): void; - /** - * @ignore - * @param {object=} [currentContext] - * @param {object=} [updatedContext] - * @param {object=} [contextReference] - * @return {{ deletions: string[], merges: string[] }} - */ - export function applyContextDifferences(currentContext?: object | undefined, updatedContext?: object | undefined, contextReference?: object | undefined, preserveScriptArgs?: boolean): { - deletions: string[]; - merges: string[]; + export default Loader; + export type LoaderOptions = { + extensions?: string[] | Set; + origin?: URL | string; + statuses?: Cache; + cache?: { + response?: Cache; + status?: Cache; + }; + headers?: Headers | Map | object | string[][]; }; - /** - * Wrap a JavaScript function source. - * @ignore - * @param {string} source - * @param {object=} [options] - */ - export function wrapFunctionSource(source: string, options?: object | undefined): string; - /** - * Gets the VM context window. - * This function will create it if it does not already exist. - * The current window will be used on Android or iOS platforms as there can - * only be one window. - * @return {Promise; - /** - * Gets the `SharedWorker` that for the VM context. - * @return {Promise} - */ - export function getContextWorker(): Promise; - /** - * Terminates the VM script context window. - * @ignore - */ - export function terminateContextWindow(): Promise; - /** - * Terminates the VM script context worker. - * @ignore - */ - export function terminateContextWorker(): Promise; - /** - * Creates a prototype object of known global reserved intrinsics. - * @ignore - */ - export function createIntrinsics(): any; - /** - * Creates a global proxy object for context execution. - * @ignore - * @param {object} context - * @return {Proxy} - */ - export function createGlobalObject(context: object): ProxyConstructor; + export type RequestOptions = { + loader?: Loader; + origin?: URL | string; + }; + export type RequestStatusOptions = { + headers?: Headers | object | any[][]; + status?: number; + }; + export type RequestLoadOptions = { + headers?: Headers | object; + }; + export type ResponseOptions = { + request?: Request; + headers?: Headers; + status?: number; + buffer?: ArrayBuffer; + text?: string; + }; + import { Headers } from "socket:ipc"; + import URL from "socket:url"; + import { Cache } from "socket:commonjs/cache"; +} + +declare module "socket:commonjs/package" { /** * @ignore * @param {string} source * @return {boolean} */ - export function detectFunctionSourceType(source: string): boolean; - /** - * Compiles `source` with `options` into a function. - * @ignore - * @param {string} source - * @param {object=} [options] - * @return {function} - */ - export function compileFunction(source: string, options?: object | undefined): Function; - /** - * Run `source` JavaScript in given context. The script context execution - * context is preserved until the `context` object that points to it is - * garbage collected or there are no longer any references to it and its - * associated `Script` instance. - * @param {string} source - * @param {ScriptOptions=} [options] - * @param {object=} [context] - * @return {Promise} - */ - export function runInContext(source: string, options?: ScriptOptions | undefined, context?: object | undefined): Promise; - /** - * Run `source` JavaScript in new context. The script context is destroyed after - * execution. This is typically a "one off" isolated run. - * @param {string} source - * @param {ScriptOptions=} [options] - * @param {object=} [context] - * @return {Promise} - */ - export function runInNewContext(source: string, options?: ScriptOptions | undefined, context?: object | undefined): Promise; - /** - * Run `source` JavaScript in this current context (`globalThis`). - * @param {string} source - * @param {ScriptOptions=} [options] - * @return {Promise} - */ - export function runInThisContext(source: string, options?: ScriptOptions | undefined): Promise; + export function detectESMSource(source: string): boolean; /** - * @ignore - * @param {Reference} reference + * @typedef {{ + * manifest?: string, + * index?: string, + * description?: string, + * version?: string, + * license?: string, + * exports?: object, + * type?: 'commonjs' | 'module', + * info?: object, + * origin?: string, + * dependencies?: Dependencies | object | Map + * }} PackageOptions + */ + /** + * @typedef {import('./loader.js').RequestOptions & { + * type?: 'commonjs' | 'module' + * prefix?: string + * }} PackageLoadOptions + */ + /** + * {import('./loader.js').RequestOptions & { + * load?: boolean, + * type?: 'commonjs' | 'module', + * browser?: boolean, + * children?: string[] + * extensions?: string[] | Set + * }} PackageResolveOptions */ - export function putReference(reference: Reference): void; /** - * Create a `Reference` for a `value` in a script `context`. - * @param {any} value - * @param {object} context - * @return {Reference} + * @typedef {{ + * organization: string | null, + * name: string, + * version: string | null, + * pathname: string, + * url: URL, + * isRelative: boolean, + * hasManifest: boolean + * }} ParsedPackageName */ - export function createReference(value: any, context: object): Reference; /** - * Get a script context by ID or values - * @param {string|object|function} id - * @return {Reference?} - */ - export function getReference(id: string | object | Function): Reference | null; + * @typedef {{ + * require?: string | string[], + * import?: string | string[], + * default?: string | string[], + * default?: string | string[], + * worker?: string | string[], + * browser?: string | string[] + * }} PackageExports + /** - * Remove a script context reference by ID. - * @param {string} id + * The default package index file such as 'index.js' + * @type {string} */ - export function removeReference(id: string): void; + export const DEFAULT_PACKAGE_INDEX: string; /** - * Get all transferable values in the `object` hierarchy. - * @param {object} object - * @return {object[]} + * The default package manifest file name such as 'package.json' + * @type {string} */ - export function getTrasferables(object: object): object[]; + export const DEFAULT_PACKAGE_MANIFEST_FILE_NAME: string; /** - * A container for a context worker message channel that looks like a "worker". - * @ignore + * The default package path prefix such as 'node_modules/' + * @type {string} */ - export class ContextWorkerInterface extends EventTarget { - get channel(): any; - get port(): any; - destroy(): void; - #private; - } + export const DEFAULT_PACKAGE_PREFIX: string; /** - * A container proxy for a context worker message channel that - * looks like a "worker". - * @ignore - */ - export class ContextWorkerInterfaceProxy extends EventTarget { - constructor(globals: any); - get port(): any; - #private; - } + * The default package version, when one is not provided + * @type {string} + */ + export const DEFAULT_PACKAGE_VERSION: string; /** - * Global reserved values that a script context may not modify. - * @type {string[]} + * The default license for a package' + * @type {string} */ - export const RESERVED_GLOBAL_INTRINSICS: string[]; + export const DEFAULT_LICENSE: string; /** - * A unique reference to a value owner by a "context object" and a - * `Script` instance. + * A container for a package name that includes a package organization identifier, + * its fully qualified name, or for relative package names, its pathname */ - export class Reference { + export class Name { /** - * `Reference` class constructor. - * @param {string} id - * @param {any} value - * @param {object=} [context] + * Parses a package name input resolving the actual module name, including an + * organization name given. If a path includes a manifest file + * ('package.json'), then the directory containing that file is considered a + * valid package and it will be included in the returned value. If a relative + * path is given, then the path is returned if it is a valid pathname. This + * function returns `null` for bad input. + * @param {string|URL} input + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @return {ParsedPackageName?} */ - constructor(id: string, value: any, context?: object | undefined); + static parse(input: string | URL, options?: { + origin?: string | URL; + manifest?: string; + } | undefined): ParsedPackageName | null; /** - * The unique id of the reference + * Returns `true` if the given `input` can be parsed by `Name.parse` or given + * as input to the `Name` class constructor. + * @param {string|URL} input + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @return {boolean} + */ + static canParse(input: string | URL, options?: { + origin?: string | URL; + manifest?: string; + } | undefined): boolean; + /** + * Creates a new `Name` from input. + * @param {string|URL} input + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @return {Name} + */ + static from(input: string | URL, options?: { + origin?: string | URL; + manifest?: string; + } | undefined): Name; + /** + * `Name` class constructor. + * @param {string|URL|NameOptions|Name} name + * @param {{ origin?: string | URL, manifest?: string }=} [options] + * @throws TypeError + */ + constructor(name: string | URL | NameOptions | Name, options?: { + origin?: string | URL; + manifest?: string; + } | undefined); + /** + * The id of this package name. * @type {string} */ get id(): string; /** - * The underling primitive type of the reference value. - * @ignore - * @type {'undefined'|'object'|'number'|'boolean'|'function'|'symbol'} + * The actual package name. + * @type {string} */ - get type(): "number" | "boolean" | "symbol" | "undefined" | "object" | "function"; + get name(): string; /** - * The underlying value of the reference. - * @type {any?} + * An alias for 'name'. + * @type {string} */ - get value(): any; + get value(): string; /** - * The `Script` this value belongs to, if available. - * @type {Script?} + * The origin of the package, if available. + * This value may be `null`. + * @type {string?} */ - get script(): Script; + get origin(): string; /** - * The "context object" this reference value belongs to. - * @type {object?} + * The package version if available. + * This value may be `null`. + * @type {string?} */ - get context(): any; + get version(): string; /** - * Releases strongly held value and weak references - * to the "context object". + * The actual package pathname, if given in name string. + * This value is always a string defaulting to '.' if no path + * was given in name string. + * @type {string} */ - release(): void; + get pathname(): string; /** - * Converts this `Reference` to a JSON object. - * @param {boolean=} [includeValue = false] + * The organization name. + * This value may be `null`. + * @type {string?} */ - toJSON(includeValue?: boolean | undefined): { - __vmScriptReference__: boolean; - id: string; - type: "number" | "boolean" | "symbol" | "undefined" | "object" | "function"; - value: any; - } | { - __vmScriptReference__: boolean; - id: string; - type: "number" | "boolean" | "symbol" | "undefined" | "object" | "function"; - value?: undefined; - }; + get organization(): string; + /** + * `true` if the package name was relative, otherwise `false`. + * @type {boolean} + */ + get isRelative(): boolean; + /** + * Converts this package name to a string. + * @ignore + * @return {string} + */ + toString(): string; + /** + * Converts this `Name` instance to JSON. + * @ignore + * @return {object} + */ + toJSON(): object; #private; } /** - * @typedef {{ - * filename?: string, - * context?: object - * }} ScriptOptions + * A container for package dependencies that map a package name to a `Package` instance. */ + export class Dependencies { + constructor(parent: any, options?: any); + get map(): Map; + get origin(): any; + add(name: any, info?: any): void; + get(name: any, options?: any): any; + entries(): IterableIterator<[any, any]>; + keys(): IterableIterator; + values(): IterableIterator; + load(options?: any): void; + [Symbol.iterator](): IterableIterator<[any, any]>; + #private; + } /** - * A `Script` is a container for raw JavaScript to be executed in - * a completely isolated virtual machine context, optionally with - * user supplied context. Context objects references are not actually - * shared, but instead provided to the script execution context using the - * structured cloning algorithm used by the Message Channel API. Context - * differences are computed and applied after execution so the user supplied - * context object realizes context changes after script execution. All script - * sources run in an "async" context so a "top level await" should work. + * A container for CommonJS module metadata, often in a `package.json` file. */ - export class Script extends EventTarget { + export class Package { /** - * `Script` class constructor - * @param {string} source - * @param {ScriptOptions} [options] + * A high level class for a package name. + * @type {typeof Name} */ - constructor(source: string, options?: ScriptOptions); + static Name: typeof Name; /** - * The script identifier. + * A high level container for package dependencies. + * @type {typeof Dependencies} */ - get id(): any; + static Dependencies: typeof Dependencies; /** - * The source for this script. + * Creates and loads a package + * @param {string|URL|NameOptions|Name} name + * @param {PackageOptions & PackageLoadOptions=} [options] + * @return {Package} + */ + static load(name: string | URL | NameOptions | Name, options?: (PackageOptions & PackageLoadOptions) | undefined): Package; + /** + * `Package` class constructor. + * @param {string|URL|NameOptions|Name} name + * @param {PackageOptions=} [options] + */ + constructor(name: string | URL | NameOptions | Name, options?: PackageOptions | undefined); + /** + * The unique ID of this `Package`, which is the absolute + * URL of the directory that contains its manifest file. * @type {string} */ - get source(): string; + get id(): string; /** - * The filename for this script. + * The absolute URL to the package manifest file * @type {string} */ - get filename(): string; + get url(): string; /** - * A promise that resolves when the script is ready. - * @type {Promise} + * A reference to the package subpath imports and browser mappings. + * These values are typically used with its corresponding `Module` + * instance require resolvers. + * @type {object} */ - get ready(): Promise; + get imports(): any; /** - * Destroy the script execution context. - * @return {Promise} + * A loader for this package, if available. This value may be `null`. + * @type {Loader} */ - destroy(): Promise; + get loader(): Loader; /** - * Run `source` JavaScript in given context. The script context execution - * context is preserved until the `context` object that points to it is - * garbage collected or there are no longer any references to it and its - * associated `Script` instance. - * @param {ScriptOptions=} [options] - * @param {object=} [context] - * @return {Promise} + * `true` if the package was actually "loaded", otherwise `false`. + * @type {boolean} */ - runInContext(context?: object | undefined, options?: ScriptOptions | undefined): Promise; + get loaded(): boolean; /** - * Run `source` JavaScript in new context. The script context is destroyed after - * execution. This is typically a "one off" isolated run. - * @param {ScriptOptions=} [options] - * @param {object=} [context] - * @return {Promise} + * The name of the package. + * @type {string} */ - runInNewContext(context?: object | undefined, options?: ScriptOptions | undefined): Promise; + get name(): string; /** - * Run `source` JavaScript in this current context (`globalThis`). - * @param {ScriptOptions=} [options] - * @return {Promise} + * The description of the package. + * @type {string} */ - runInThisContext(options?: ScriptOptions | undefined): Promise; + get description(): string; + /** + * The organization of the package. This value may be `null`. + * @type {string?} + */ + get organization(): string; + /** + * The license of the package. + * @type {string} + */ + get license(): string; + /** + * The version of the package. + * @type {string} + */ + get version(): string; + /** + * The origin for this package. + * @type {string} + */ + get origin(): string; + /** + * The exports mappings for the package + * @type {object} + */ + get exports(): any; + /** + * The package type. + * @type {'commonjs'|'module'} + */ + get type(): "module" | "commonjs"; + /** + * The raw package metadata object. + * @type {object?} + */ + get info(): any; + /** + * @type {Dependencies} + */ + get dependencies(): Dependencies; + /** + * An alias for `entry` + * @type {string?} + */ + get main(): string; + /** + * The entry to the package + * @type {string?} + */ + get entry(): string; + /** + * Load the package information at an optional `origin` with + * optional request `options`. + * @param {PackageLoadOptions=} [options] + * @throws SyntaxError + * @return {boolean} + */ + load(origin?: any, options?: PackageLoadOptions | undefined): boolean; + /** + * Resolve a file's `pathname` within the package. + * @param {string|URL} pathname + * @param {PackageResolveOptions=} [options] + * @return {string} + */ + resolve(pathname: string | URL, options?: PackageResolveOptions | undefined): string; #private; } - namespace _default { - export { compileFunction }; - export { createReference }; - export { getContextWindow }; - export { getContextWorker }; - export { getReference }; - export { getTrasferables }; - export { putReference }; - export { Reference }; - export { removeReference }; - export { runInContext }; - export { runInNewContext }; - export { runInThisContext }; - export { Script }; + export default Package; + export type PackageOptions = { + manifest?: string; + index?: string; + description?: string; + version?: string; + license?: string; + exports?: object; + type?: "commonjs" | "module"; + info?: object; + origin?: string; + dependencies?: Dependencies | object | Map; + }; + export type PackageLoadOptions = import("socket:commonjs/loader").RequestOptions & { + type?: "commonjs" | "module"; + prefix?: string; + }; + export type ParsedPackageName = { + organization: string | null; + name: string; + version: string | null; + pathname: string; + url: URL; + isRelative: boolean; + hasManifest: boolean; + }; + /** + * /** + * The default package index file such as 'index.js' + */ + export type PackageExports = { + require?: string | string[]; + import?: string | string[]; + default?: string | string[]; + default?: string | string[]; + worker?: string | string[]; + browser?: string | string[]; + }; + import URL from "socket:url"; + import { Loader } from "socket:commonjs/loader"; +} + +declare module "socket:commonjs/module" { + /** + * CommonJS module scope with module scoped globals. + * @ignore + * @param {object} exports + * @param {function(string): any} require + * @param {Module} module + * @param {string} __filename + * @param {string} __dirname + * @param {typeof process} process + * @param {object} global + */ + export function CommonJSModuleScope(exports: object, require: (arg0: string) => any, module: Module, __filename: string, __dirname: string, process: any, global: object): void; + /** + * Creates a `require` function from a given module URL. + * @param {string|URL} url + * @param {ModuleOptions=} [options] + * @return {RequireFunction} + */ + export function createRequire(url: string | URL, options?: ModuleOptions | undefined): RequireFunction; + /** + * @typedef {function(string, Module, function(string): any): any} ModuleResolver + */ + /** + * @typedef {import('./require.js').RequireFunction} RequireFunction + */ + /** + * @typedef {import('./package.js').PackageOptions} PackageOptions + */ + /** + * @typedef {{ + * prefix?: string, + * request?: import('./loader.js').RequestOptions, + * builtins?: object + * } CreateRequireOptions + */ + /** + * @typedef {{ + * resolvers?: ModuleResolver[], + * importmap?: ImportMap, + * loader?: Loader | object, + * loaders?: object, + * package?: Package | PackageOptions + * parent?: Module, + * state?: State + * }} ModuleOptions + */ + /** + * @typedef {{ + * extensions?: object + * }} ModuleLoadOptions + */ + export const builtinModules: any; + /** + * CommonJS module scope source wrapper. + * @type {string} + */ + export const COMMONJS_WRAPPER: string; + /** + * A container for imports. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} + */ + export class ImportMap { + set imports(imports: any); + /** + * The imports object for the importmap. + * @type {object} + */ + get imports(): any; + /** + * Extends the current imports object. + * @param {object} imports + * @return {ImportMap} + */ + extend(importmap: any): ImportMap; + #private; + } + /** + * A container for `Module` instance state. + */ + export class State { + /** + * `State` class constructor. + * @ignore + * @param {object|State=} [state] + */ + constructor(state?: (object | State) | undefined); + loading: boolean; + loaded: boolean; + error: any; + } + /** + * The module scope for a loaded module. + * This is a special object that is seal, frozen, and only exposes an + * accessor the 'exports' field. + * @ignore + */ + export class ModuleScope { + /** + * `ModuleScope` class constructor. + * @param {Module} module + */ + constructor(module: Module); + get id(): any; + get filename(): any; + get loaded(): any; + get children(): any; + set exports(exports: any); + get exports(): any; + toJSON(): { + id: any; + filename: any; + children: any; + exports: any; + }; + #private; + } + /** + * An abstract base class for loading a module. + */ + export class ModuleLoader { + /** + * Creates a `ModuleLoader` instance from the `module` currently being loaded. + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {ModuleLoader} + */ + static from(module: Module, options?: ModuleLoadOptions | undefined): ModuleLoader; + /** + * Creates a new `ModuleLoader` instance from the `module` currently + * being loaded with the `source` string to parse and load with optional + * `ModuleLoadOptions` options. + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + static load(module: Module, options?: ModuleLoadOptions | undefined): boolean; + /** + * @param {Module} module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + load(module: Module, options?: ModuleLoadOptions | undefined): boolean; } - export default _default; - export type ScriptOptions = { - filename?: string; - context?: object; - }; - import { SharedWorker } from "socket:worker"; -} -declare module "socket:module" { - export function isBuiltin(name: any): boolean; /** - * Creates a `require` function from a source URL. - * @param {URL|string} sourcePath - * @return {function} + * A JavaScript module loader */ - export function createRequire(sourcePath: URL | string): Function; - export default exports; - /** - * A limited set of builtins exposed to CommonJS modules. - */ - export const builtins: { - buffer: typeof buffer; - console: import("socket:console").Console; - dgram: typeof dgram; - dns: typeof dns; - 'dns/promises': typeof dns.promises; - events: typeof events; - extension: { - load: typeof import("socket:extension").load; - stats: typeof import("socket:extension").stats; - }; - fs: typeof fs; - 'fs/promises': typeof fs.promises; - gc: any; - ipc: typeof ipc; - module: typeof exports; - os: typeof os; - path: typeof path; - process: any; - stream: typeof stream; - test: typeof test; - util: typeof util; - url: any; - vm: { - compileFunction: typeof import("socket:vm").compileFunction; - createReference: typeof import("socket:vm").createReference; - getContextWindow: typeof import("socket:vm").getContextWindow; - getContextWorker: typeof import("socket:vm").getContextWorker; - getReference: typeof import("socket:vm").getReference; - getTrasferables: typeof import("socket:vm").getTrasferables; - putReference: typeof import("socket:vm").putReference; - Reference: typeof import("socket:vm").Reference; - removeReference: typeof import("socket:vm").removeReference; - runInContext: typeof import("socket:vm").runInContext; - runInNewContext: typeof import("socket:vm").runInNewContext; - runInThisContext: typeof import("socket:vm").runInThisContext; - Script: typeof import("socket:vm").Script; - }; - }; - export const builtinModules: { - buffer: typeof buffer; - console: import("socket:console").Console; - dgram: typeof dgram; - dns: typeof dns; - 'dns/promises': typeof dns.promises; - events: typeof events; - extension: { - load: typeof import("socket:extension").load; - stats: typeof import("socket:extension").stats; - }; - fs: typeof fs; - 'fs/promises': typeof fs.promises; - gc: any; - ipc: typeof ipc; - module: typeof exports; - os: typeof os; - path: typeof path; - process: any; - stream: typeof stream; - test: typeof test; - util: typeof util; - url: any; - vm: { - compileFunction: typeof import("socket:vm").compileFunction; - createReference: typeof import("socket:vm").createReference; - getContextWindow: typeof import("socket:vm").getContextWindow; - getContextWorker: typeof import("socket:vm").getContextWorker; - getReference: typeof import("socket:vm").getReference; - getTrasferables: typeof import("socket:vm").getTrasferables; - putReference: typeof import("socket:vm").putReference; - Reference: typeof import("socket:vm").Reference; - removeReference: typeof import("socket:vm").removeReference; - runInContext: typeof import("socket:vm").runInContext; - runInNewContext: typeof import("socket:vm").runInNewContext; - runInThisContext: typeof import("socket:vm").runInThisContext; - Script: typeof import("socket:vm").Script; - }; - }; + export class JavaScriptModuleLoader extends ModuleLoader { + } /** - * CommonJS module scope source wrapper. - * @type {string} + * A JSON module loader. */ - export const COMMONJS_WRAPPER: string; + export class JSONModuleLoader extends ModuleLoader { + } /** - * The main entry source origin. - * @type {string} + * A WASM module loader + */ - export const MAIN_SOURCE_ORIGIN: string; - export namespace scope { - let current: any; - let previous: any; + export class WASMModuleLoader extends ModuleLoader { } /** * A container for a loaded CommonJS module. All errors bubble * to the "main" module and global object (if possible). */ export class Module extends EventTarget { - static set current(module: exports.Module); /** * A reference to the currently scoped module. * @type {Module?} */ - static get current(): exports.Module; - static set previous(module: exports.Module); + static current: Module | null; /** * A reference to the previously scoped module. * @type {Module?} */ - static get previous(): exports.Module; + static previous: Module | null; /** - * Module cache. - * @ignore + * A cache of loaded modules + * @type {Map} */ - static cache: any; + static cache: Map; /** - * Custom module resolvers. - * @type {Array} + * An array of globally available module loader resolvers. + * @type {ModuleResolver[]} */ - static resolvers: Array; + static resolvers: ModuleResolver[]; /** - * CommonJS module scope source wrapper. - * @ignore + * Globally available 'importmap' for all loaded modules. + * @type {ImportMap} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} */ - static wrapper: string; + static importmap: ImportMap; /** - * Creates a `require` function from a source URL. - * @param {URL|string} sourcePath - * @return {function} + * A limited set of builtins exposed to CommonJS modules. + * @type {object} + */ + static builtins: object; + /** + * A limited set of builtins exposed to CommonJS modules. + * @type {object} */ - static createRequire(sourcePath: URL | string): Function; + static builtinModules: object; + /** + * CommonJS module scope source wrapper components. + * @type {string[]} + */ + static wrapper: string[]; + /** + * An array of global require paths, relative to the origin. + * @type {string[]} + */ + static globalPaths: string[]; + /** + * Globabl module loaders + * @type {object} + */ + static loaders: object; /** * The main entry module, lazily created. * @type {Module} */ - static get main(): exports.Module; + static get main(): Module; /** * Wraps source in a CommonJS module scope. + * @param {string} source */ - static wrap(source: any): string; + static wrap(source: string): string; + /** + * Compiles given JavaScript module source. + * @param {string} source + * @param {{ url?: URL | string }=} [options] + * @return {function( + * object, + * function(string): any, + * Module, + * string, + * string, + * typeof process, + * object + * ): any} + */ + static compile(source: string, options?: { + url?: URL | string; + } | undefined): (arg0: object, arg1: (arg0: string) => any, arg2: Module, arg3: string, arg4: string, arg5: typeof process, arg6: object) => any; /** * Creates a `Module` from source URL and optionally a parent module. - * @param {string|URL|Module} [sourcePath] - * @param {string|URL|Module} [parent] + * @param {string|URL|Module} url + * @param {ModuleOptions=} [options] */ - static from(sourcePath?: string | URL | Module, parent?: string | URL | Module): any; + static from(url: string | URL | Module, options?: ModuleOptions | undefined): any; + /** + * Creates a `require` function from a given module URL. + * @param {string|URL} url + * @param {ModuleOptions=} [options] + */ + static createRequire(url: string | URL, options?: ModuleOptions | undefined): any; /** * `Module` class constructor. - * @ignore + * @param {string|URL} url + * @param {ModuleOptions=} [options] */ - constructor(id: any, parent?: any, sourcePath?: any); + constructor(url: string | URL, options?: ModuleOptions | undefined); /** - * The module id, most likely a file name. + * A unique ID for this module. * @type {string} */ - id: string; + get id(): string; /** - * The parent module, if given. - * @type {Module?} + * A reference to the "main" module. + * @type {Module} */ - parent: Module | null; + get main(): Module; /** - * `true` if the module did load successfully. - * @type {boolean} + * Child modules of this module. + * @type {Module[]} */ - loaded: boolean; + get children(): Module[]; /** - * The module's exports. - * @type {any} + * A reference to the module cache. Possibly shared with all + * children modules. + * @type {object} */ - exports: any; + get cache(): any; /** - * The filename of the module. - * @type {string} + * A reference to the module package. + * @type {Package} */ - filename: string; + get package(): Package; /** - * Modules children to this one, as in they were required in this - * module scope context. - * @type {Array} + * The `ImportMap` for this module. + * @type {ImportMap} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} */ - children: Array; + get importmap(): ImportMap; /** - * The original source URL to load this module. - * @type {string} + * The module level resolvers. + * @type {ModuleResolver[]} */ - sourcePath: string; + get resolvers(): ModuleResolver[]; /** - * `true` if the module is the main module. + * `true` if the module is currently loading, otherwise `false`. * @type {boolean} */ - get isMain(): boolean; + get loading(): boolean; /** - * `true` if the module was loaded by name, not file path. + * `true` if the module is currently loaded, otherwise `false`. * @type {boolean} */ - get isNamed(): boolean; + get loaded(): boolean; /** - * @type {URL} + * An error associated with the module if it failed to load. + * @type {Error?} */ - get url(): URL; + get error(): Error; + /** + * The exports of the module + * @type {object} + */ + get exports(): any; + /** + * The scope of the module given to parsed modules. + * @type {ModuleScope} + */ + get scope(): ModuleScope; /** + * The origin of the loaded module. * @type {string} */ - get pathname(): string; + get origin(): string; + /** + * The parent module for this module. + * @type {Module?} + */ + get parent(): Module; + /** + * The `Loader` for this module. + * @type {Loader} + */ + get loader(): Loader; /** + * The filename of the module. * @type {string} */ - get path(): string; + get filename(): string; /** - * Loads the module, synchronously returning `true` upon success, - * otherwise `false`. - * @return {boolean} + * Known source loaders for this module keyed by file extension. + * @type {object} */ - load(): boolean; + get loaders(): any; /** - * Creates a require function for loaded CommonJS modules - * child to this module. - * @return {function(string): any} + * Factory for creating a `require()` function based on a module context. + * @param {CreateRequireOptions=} [options] + * @return {RequireFunction} */ - createRequire(): (arg0: string) => any; + createRequire(options?: CreateRequireOptions | undefined): RequireFunction; /** - * Requires a module at `filename` that will be loaded as a child - * to this module. - * @param {string} filename + * Creates a `Module` from source the URL with this module as + * the parent. + * @param {string|URL|Module} url + * @param {ModuleOptions=} [options] + */ + createModule(url: string | URL | Module, options?: ModuleOptions | undefined): any; + /** + * Requires a module at for a given `input` which can be a relative file, + * named module, or an absolute URL within the context of this odule. + * @param {string|URL} input + * @param {RequireOptions=} [options] + * @throws ModuleNotFoundError + * @throws ReferenceError + * @throws SyntaxError + * @throws TypeError * @return {any} */ - require(filename: string): any; + require(url: any, options?: RequireOptions | undefined): any; + /** + * Loads the module + * @param {ModuleLoadOptions=} [options] + * @return {boolean} + */ + load(options?: ModuleLoadOptions | undefined): boolean; + resolve(input: any): string; /** * @ignore */ [Symbol.toStringTag](): string; + #private; + } + export namespace Module { + export { Module }; + } + export default Module; + export type ModuleResolver = (arg0: string, arg1: Module, arg2: (arg0: string) => any) => any; + export type RequireFunction = import("socket:commonjs/require").RequireFunction; + export type PackageOptions = import("socket:commonjs/package").PackageOptions; + export type CreateRequireOptions = { + prefix?: string; + request?: import("socket:commonjs/loader").RequestOptions; + builtins?: object; + }; + export type ModuleOptions = { + resolvers?: ModuleResolver[]; + importmap?: ImportMap; + loader?: Loader | object; + loaders?: object; + package?: Package | PackageOptions; + parent?: Module; + state?: State; + }; + export type ModuleLoadOptions = { + extensions?: object; + }; + import { Package } from "socket:commonjs/package"; + import { Loader } from "socket:commonjs/loader"; + import process from "socket:process"; +} + +declare module "socket:commonjs/require" { + /** + * Factory for creating a `require()` function based on a module context. + * @param {CreateRequireOptions} options + * @return {RequireFunction} + */ + export function createRequire(options: CreateRequireOptions): RequireFunction; + /** + * @typedef {function(string, import('./module.js').Module, function(string): any): any} RequireResolver + */ + /** + * @typedef {{ + * module: import('./module.js').Module, + * prefix?: string, + * request?: import('./loader.js').RequestOptions, + * builtins?: object, + * resolvers?: RequireFunction[] + * }} CreateRequireOptions + */ + /** + * @typedef {function(string): any} RequireFunction + */ + /** + * @typedef {import('./package.js').PackageOptions} PackageOptions + */ + /** + * @typedef {import('./package.js').PackageResolveOptions} PackageResolveOptions + */ + /** + * @typedef { + * PackageResolveOptions & + * PackageOptions & + * { origins?: string[] | URL[] } + * } ResolveOptions + */ + /** + * @typedef {ResolveOptions & { + * resolvers?: RequireResolver[], + * importmap?: import('./module.js').ImportMap, + * cache?: boolean + * }} RequireOptions + */ + /** + * An array of global require paths, relative to the origin. + * @type {string[]} + */ + export const globalPaths: string[]; + /** + * An object attached to a `require()` function that contains metadata + * about the current module context. + */ + export class Meta { + /** + * `Meta` class constructor. + * @param {import('./module.js').Module} module + */ + constructor(module: import("socket:commonjs/module").Module); + /** + * The referrer (parent) of this module. + * @type {string} + */ + get referrer(): string; + /** + * The referrer (parent) of this module. + * @type {string} + */ + get url(): string; + #private; + } + export default createRequire; + export type RequireResolver = (arg0: string, arg1: import("socket:commonjs/module").Module, arg2: (arg0: string) => any) => any; + export type CreateRequireOptions = { + module: import("socket:commonjs/module").Module; + prefix?: string; + request?: import("socket:commonjs/loader").RequestOptions; + builtins?: object; + resolvers?: RequireFunction[]; + }; + export type RequireFunction = (arg0: string) => any; + export type PackageOptions = import("socket:commonjs/package").PackageOptions; + export type PackageResolveOptions = import("socket:commonjs/package").PackageResolveOptions; + export type RequireOptions = ResolveOptions & { + resolvers?: RequireResolver[]; + importmap?: import("socket:commonjs/module").ImportMap; + cache?: boolean; + }; +} + +declare module "socket:commonjs" { + export default exports; + import * as exports from "socket:commonjs"; + import builtins from "socket:commonjs/builtins"; + import Cache from "socket:commonjs/cache"; + import createRequire from "socket:commonjs/require"; + import Loader from "socket:commonjs/loader"; + import Module from "socket:commonjs/module"; + import Package from "socket:commonjs/package"; + + export { builtins, Cache, createRequire, Loader, Module, Package }; +} + +declare module "socket:fetch/fetch" { + export function Headers(headers: any): void; + export class Headers { + constructor(headers: any); + map: {}; + append(name: any, value: any): void; + delete(name: any): void; + get(name: any): any; + has(name: any): boolean; + set(name: any, value: any): void; + forEach(callback: any, thisArg: any): void; + keys(): { + next: () => { + done: boolean; + value: any; + }; + }; + values(): { + next: () => { + done: boolean; + value: any; + }; + }; + entries(): { + next: () => { + done: boolean; + value: any; + }; + }; + } + export function Request(input: any, options: any): void; + export class Request { + constructor(input: any, options: any); + url: string; + credentials: any; + headers: Headers; + method: any; + mode: any; + signal: any; + referrer: any; + clone(): Request; + } + export function Response(bodyInit: any, options: any): void; + export class Response { + constructor(bodyInit: any, options: any); + type: string; + status: any; + ok: boolean; + statusText: string; + headers: Headers; + url: any; + clone(): Response; + } + export namespace Response { + function error(): Response; + function redirect(url: any, status: any): Response; + } + export function fetch(input: any, init: any): Promise; + export class DOMException { + private constructor(); + } + namespace _default { + export { fetch }; + export { Headers }; + export { Request }; + export { Response }; + } + export default _default; +} + +declare module "socket:fetch/index" { + export default fetch; + import { fetch } from "socket:fetch/fetch"; + import { Headers } from "socket:fetch/fetch"; + import { Request } from "socket:fetch/fetch"; + import { Response } from "socket:fetch/fetch"; + export { fetch, Headers, Request, Response }; +} + +declare module "socket:fetch" { + export * from "socket:fetch/index"; + export default fetch; + import fetch from "socket:fetch/index"; +} + +declare module "socket:i18n" { + /** + * Get messages for `locale` pattern. This function could return many results + * for various locales given a `locale` pattern. such as `fr`, which could + * return results for `fr`, `fr-FR`, `fr-BE`, etc. + * @ignore + * @param {string} locale + * @return {object[]} + */ + export function getMessagesForLocale(locale: string): object[]; + /** + * Returns user preferred ISO 639 language codes or RFC 5646 language tags. + * @return {string[]} + */ + export function getAcceptLanguages(): string[]; + /** + * Returns the current user ISO 639 language code or RFC 5646 language tag. + * @return {?string} + */ + export function getUILanguage(): string | null; + /** + * Gets a localized message string for the specified message name. + * @param {string} messageName + * @param {object|string[]=} [substitutions = []] + * @param {object=} [options] + * @param {string=} [options.locale = null] + * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} + * @see {@link https://www.ibm.com/docs/en/rbd/9.5.1?topic=syslib-getmessage} + * @return {?string} + */ + export function getMessage(messageName: string, substitutions?: (object | string[]) | undefined, options?: object | undefined): string | null; + /** + * Gets a localized message description string for the specified message name. + * @param {string} messageName + * @param {object=} [options] + * @param {string=} [options.locale = null] + * @return {?string} + */ + export function getMessageDescription(messageName: string, options?: object | undefined): string | null; + /** + * A cache of loaded locale messages. + * @type {Map} + */ + export const cache: Map; + /** + * Default location of i18n locale messages + * @type {string} + */ + export const DEFAULT_LOCALES_LOCATION: string; + /** + * An enumeration of supported ISO 639 language codes or RFC 5646 language tags. + * @type {Enumeration} + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/LanguageCode} + * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} + */ + export const LanguageCode: Enumeration; + namespace _default { + export { LanguageCode }; + export { getAcceptLanguages }; + export { getMessage }; + export { getUILanguage }; } - export type ModuleResolver = (arg0: string, arg1: Module, arg2: Function) => undefined; - import { URL } from "socket:url/index"; - import * as exports from "socket:module"; - import buffer from "socket:buffer"; - import dgram from "socket:dgram"; - import dns from "socket:dns"; - import events from "socket:events"; - import fs from "socket:fs"; - import ipc from "socket:ipc"; - import os from "socket:os"; - import { posix as path } from "socket:path"; - import stream from "socket:stream"; - import test from "socket:test"; - import util from "socket:util"; - + export default _default; + import Enumeration from "socket:enumeration"; } -declare module "socket:network" { + +declare module "socket:node/index" { export default network; - export const network: Promise; - import { Cache } from "socket:stream-relay/index"; - import { sha256 } from "socket:stream-relay/index"; - import { Encryption } from "socket:stream-relay/index"; - import { Packet } from "socket:stream-relay/index"; - import { NAT } from "socket:stream-relay/index"; + export function network(options: any): Promise; + import { Cache } from "socket:latica/index"; + import { sha256 } from "socket:latica/index"; + import { Encryption } from "socket:latica/index"; + import { Packet } from "socket:latica/index"; + import { NAT } from "socket:latica/index"; export { Cache, sha256, Encryption, Packet, NAT }; } + +declare module "socket:index" { + import { network } from "socket:node/index"; + import { Cache } from "socket:node/index"; + import { sha256 } from "socket:node/index"; + import { Encryption } from "socket:node/index"; + import { Packet } from "socket:node/index"; + import { NAT } from "socket:node/index"; + export { network, Cache, sha256, Encryption, Packet, NAT }; +} + +declare module "socket:latica" { + export * from "socket:latica/index"; + export default def; + import def from "socket:latica/index"; +} + +declare module "socket:module" { + export const builtinModules: any; + export default Module; + export type ModuleOptions = import("socket:commonjs/module").ModuleOptions; + export type ModuleResolver = import("socket:commonjs/module").ModuleResolver; + export type ModuleLoadOptions = import("socket:commonjs/module").ModuleLoadOptions; + export type RequireFunction = import("socket:commonjs/module").RequireFunction; + export type CreateRequireOptions = import("socket:commonjs/module").CreateRequireOptions; + import { createRequire } from "socket:commonjs/module"; + import { Module } from "socket:commonjs/module"; + import builtins from "socket:commonjs/builtins"; + import { isBuiltin } from "socket:commonjs/builtins"; + export { createRequire, Module, builtins, isBuiltin }; +} + declare module "socket:node-esm-loader" { export function resolve(specifier: any, ctx: any, next: any): Promise; export default resolve; } + declare module "socket:internal/permissions" { /** * Query for a permission status. @@ -8504,6 +15861,7 @@ declare module "socket:internal/permissions" { } import Enumeration from "socket:enumeration"; } + declare module "socket:notification" { /** * Show a notification. Creates a `Notification` instance and displays @@ -8625,7 +15983,7 @@ declare module "socket:notification" { requireInteraction?: boolean | undefined; silent?: boolean | undefined; vibrate?: number[] | undefined; - }); + }, allowServiceWorkerGlobalScope?: boolean); /** * An array of actions to display in the notification. * @type {NotificationAction[]} @@ -8659,7 +16017,7 @@ declare module "socket:notification" { */ get dir(): "auto" | "ltr" | "rtl"; /** - * A string containing the URL of an icon to be displayed in the notification. + A string containing the URL of an icon to be displayed in the notification. * @type {string} */ get icon(): string; @@ -8710,6 +16068,11 @@ declare module "socket:notification" { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#vibration_patterns} */ get vibrate(): number[]; + /** + * @ignore + * @return {object} + */ + toJSON(): object; #private; } /** @@ -8737,18 +16100,27 @@ declare module "socket:notification" { * @param {boolean=} [options.force = false] * @return {Promise<'granted'|'default'|'denied'>} */ - static requestPermission(options?: object | undefined): Promise<'granted' | 'default' | 'denied'>; + static requestPermission(options?: object | undefined): Promise<"granted" | "default" | "denied">; /** * `Notification` class constructor. * @param {string} title * @param {NotificationOptions=} [options] */ - constructor(title: string, options?: NotificationOptions | undefined, ...args: any[]); + constructor(title: string, options?: NotificationOptions | undefined, existingState?: any, ...args: any[]); + /** + * @ignore + */ + get options(): any; /** * A unique identifier for this notification. * @type {string} */ get id(): string; + /** + * `true` if the notification was closed, otherwise `false`. + * @type {boolea} + */ + get closed(): boolea; set onclick(onclick: Function); /** * The click event is dispatched when the user clicks on @@ -8881,11 +16253,560 @@ declare module "socket:notification" { import { Enumeration } from "socket:enumeration"; import URL from "socket:url"; } -declare module "socket:stream-relay" { - export * from "socket:stream-relay/index"; - export default def; - import def from "socket:stream-relay/index"; + +declare module "socket:shared-worker" { + /** + * A reference to the opened environment. This value is an instance of an + * `Environment` if the scope is a ServiceWorker scope. + * @type {Environment|null} + */ + export const env: Environment | null; + export default SharedWorker; + import { SharedWorker } from "socket:shared-worker/index"; + export { Environment, SharedWorker }; +} + +declare module "socket:signal" { + export * from "socket:process/signal"; + export default signal; + import signal from "socket:process/signal"; +} + +declare module "socket:service-worker/instance" { + export function createServiceWorker(currentState?: any, options?: any): any; + export const SHARED_WORKER_URL: string; + export const ServiceWorker: { + new (): ServiceWorker; + prototype: ServiceWorker; + } | { + new (): { + onmessage: any; + onerror: any; + onstatechange: any; + readonly state: any; + readonly scriptURL: any; + postMessage(): void; + addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void; + dispatchEvent(event: Event): boolean; + removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void; + }; + }; + export default createServiceWorker; +} + +declare module "socket:worker" { + export default Worker; + import { SharedWorker } from "socket:shared-worker/index"; + import { ServiceWorker } from "socket:service-worker/instance"; + import { Worker } from "socket:worker_threads"; + export { SharedWorker, ServiceWorker, Worker }; +} + +declare module "socket:child_process/worker" { + export {}; +} + +declare module "socket:internal/callsite" { + /** + * Creates an ordered and link array of `CallSite` instances from a + * given `Error`. + * @param {Error} error + * @param {string} source + * @return {CallSite[]} + */ + export function createCallSites(error: Error, source: string): CallSite[]; + /** + * @typedef {{ + * sourceURL: string | null, + * symbol: string, + * column: number | undefined, + * line: number | undefined, + * native: boolean + * }} ParsedStackFrame + */ + /** + * A container for location data related to a `StackFrame` + */ + export class StackFrameLocation { + /** + * Creates a `StackFrameLocation` from JSON input. + * @param {object=} json + * @return {StackFrameLocation} + */ + static from(json?: object | undefined): StackFrameLocation; + /** + * The line number of the location of the stack frame, if available. + * @type {number | undefined} + */ + lineNumber: number | undefined; + /** + * The column number of the location of the stack frame, if available. + * @type {number | undefined} + */ + columnNumber: number | undefined; + /** + * The source URL of the location of the stack frame, if available. This value + * may be `null`. + * @type {string?} + */ + sourceURL: string | null; + /** + * `true` if the stack frame location is in native location, otherwise + * this value `false` (default). + * @type + */ + isNative: any; + /** + * Converts this `StackFrameLocation` to a JSON object. + * @ignore + * @return {{ + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }} + */ + toJSON(): { + lineNumber: number | undefined; + columnNumber: number | undefined; + sourceURL: string | null; + isNative: boolean; + }; + } + /** + * A stack frame container related to a `CallSite`. + */ + export class StackFrame { + /** + * Parses a raw stack frame string into structured data. + * @param {string} rawStackFrame + * @return {ParsedStackFrame} + */ + static parse(rawStackFrame: string): ParsedStackFrame; + /** + * Creates a new `StackFrame` from an `Error` and raw stack frame + * source `rawStackFrame`. + * @param {Error} error + * @param {string} rawStackFrame + * @return {StackFrame} + */ + static from(error: Error, rawStackFrame: string): StackFrame; + /** + * `StackFrame` class constructor. + * @param {Error} error + * @param {ParsedStackFrame=} [frame] + * @param {string=} [source] + */ + constructor(error: Error, frame?: ParsedStackFrame | undefined, source?: string | undefined); + /** + * The stack frame location data. + * @type {StackFrameLocation} + */ + location: StackFrameLocation; + /** + * The `Error` associated with this `StackFrame` instance. + * @type {Error?} + */ + error: Error | null; + /** + * The name of the function where the stack frame is located. + * @type {string?} + */ + symbol: string | null; + /** + * The raw stack frame source string. + * @type {string?} + */ + source: string | null; + /** + * Converts this `StackFrameLocation` to a JSON object. + * @ignore + * @return {{ + * location: { + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * }} + */ + toJSON(): { + location: { + lineNumber: number | undefined; + columnNumber: number | undefined; + sourceURL: string | null; + isNative: boolean; + }; + isNative: boolean; + symbol: string | null; + source: string | null; + error: { + message: string; + name: string; + stack: string; + } | null; + }; + } + /** + * A v8 compatible interface and container for call site information. + */ + export class CallSite { + /** + * An internal symbol used to refer to the index of a promise in + * `Promise.all` or `Promise.any` function call site. + * @ignore + * @type {symbol} + */ + static PromiseElementIndexSymbol: symbol; + /** + * An internal symbol used to indicate that a call site is in a `Promise.all` + * function call. + * @ignore + * @type {symbol} + */ + static PromiseAllSymbol: symbol; + /** + * An internal symbol used to indicate that a call site is in a `Promise.any` + * function call. + * @ignore + * @type {symbol} + */ + static PromiseAnySymbol: symbol; + /** + * An internal source symbol used to store the original `Error` stack source. + * @ignore + * @type {symbol} + */ + static StackSourceSymbol: symbol; + /** + * `CallSite` class constructor + * @param {Error} error + * @param {string} rawStackFrame + * @param {CallSite=} previous + */ + constructor(error: Error, rawStackFrame: string, previous?: CallSite | undefined); + /** + * The `Error` associated with the call site. + * @type {Error} + */ + get error(): Error; + /** + * The previous `CallSite` instance, if available. + * @type {CallSite?} + */ + get previous(): CallSite; + /** + * A reference to the `StackFrame` data. + * @type {StackFrame} + */ + get frame(): StackFrame; + /** + * This function _ALWAYS__ returns `globalThis` as `this` cannot be determined. + * @return {object} + */ + getThis(): object; + /** + * This function _ALWAYS__ returns `null` as the type name of `this` + * cannot be determined. + * @return {null} + */ + getTypeName(): null; + /** + * This function _ALWAYS__ returns `undefined` as the current function + * reference cannot be determined. + * @return {undefined} + */ + getFunction(): undefined; + /** + * Returns the name of the function in at the call site, if available. + * @return {string|undefined} + */ + getFunctionName(): string | undefined; + /** + * An alias to `getFunctionName() + * @return {string} + */ + getMethodName(): string; + /** + * Get the filename of the call site location, if available, otherwise this + * function returns 'unknown location'. + * @return {string} + */ + getFileName(): string; + /** + * Returns the location source URL defaulting to the global location. + * @return {string} + */ + getScriptNameOrSourceURL(): string; + /** + * Returns a hash value of the source URL return by `getScriptNameOrSourceURL()` + * @return {string} + */ + getScriptHash(): string; + /** + * Returns the line number of the call site location. + * This value may be `undefined`. + * @return {number|undefined} + */ + getLineNumber(): number | undefined; + /** + * @ignore + * @return {number} + */ + getPosition(): number; + /** + * Attempts to get an "enclosing" line number, potentially the previous + * line number of the call site + * @param {number|undefined} + */ + getEnclosingLineNumber(): any; + /** + * Returns the column number of the call site location. + * This value may be `undefined`. + * @return {number|undefined} + */ + getColumnNumber(): number | undefined; + /** + * Attempts to get an "enclosing" column number, potentially the previous + * line number of the call site + * @param {number|undefined} + */ + getEnclosingColumnNumber(): any; + /** + * Gets the origin of where `eval()` was called if this call site function + * originated from a call to `eval()`. This function may return `undefined`. + * @return {string|undefined} + */ + getEvalOrigin(): string | undefined; + /** + * This function _ALWAYS__ returns `false` as `this` cannot be determined so + * "top level" detection is not possible. + * @return {boolean} + */ + isTopLevel(): boolean; + /** + * Returns `true` if this call site originated from a call to `eval()`. + * @return {boolean} + */ + isEval(): boolean; + /** + * Returns `true` if the call site is in a native location, otherwise `false`. + * @return {boolean} + */ + isNative(): boolean; + /** + * This function _ALWAYS_ returns `false` as constructor detection + * is not possible. + * @return {boolean} + */ + isConstructor(): boolean; + /** + * Returns `true` if the call site is in async context, otherwise `false`. + * @return {boolean} + */ + isAsync(): boolean; + /** + * Returns `true` if the call site is in a `Promise.all()` function call, + * otherwise `false. + * @return {boolean} + */ + isPromiseAll(): boolean; + /** + * Gets the index of the promise element that was followed in a + * `Promise.all()` or `Promise.any()` function call. If not available, then + * this function returns `null`. + * @return {number|null} + */ + getPromiseIndex(): number | null; + /** + * Converts this call site to a string. + * @return {string} + */ + toString(): string; + /** + * Converts this `CallSite` to a JSON object. + * @ignore + * @return {{ + * frame: { + * location: { + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * } + * }} + */ + toJSON(): { + frame: { + location: { + lineNumber: number | undefined; + columnNumber: number | undefined; + sourceURL: string | null; + isNative: boolean; + }; + isNative: boolean; + symbol: string | null; + source: string | null; + error: { + message: string; + name: string; + stack: string; + } | null; + }; + }; + set [$previous](previous: any); + /** + * Private accessor to "friend class" `CallSiteList`. + * @ignore + */ + get [$previous](): any; + #private; + } + /** + * An array based list container for `CallSite` instances. + */ + export class CallSiteList extends Array { + /** + * Creates a `CallSiteList` instance from `Error` input. + * @param {Error} error + * @param {string} source + * @return {CallSiteList} + */ + static from(error: Error, source: string): CallSiteList; + /** + * `CallSiteList` class constructor. + * @param {Error} error + * @param {string[]=} [sources] + */ + constructor(error: Error, sources?: string[] | undefined); + /** + * A reference to the `Error` for this `CallSiteList` instance. + * @type {Error} + */ + get error(): Error; + /** + * An array of stack frame source strings. + * @type {string[]} + */ + get sources(): string[]; + /** + * The original stack string derived from the sources. + * @type {string} + */ + get stack(): string; + /** + * Adds `CallSite` instances to the top of the list, linking previous + * instances to the next one. + * @param {...CallSite} callsites + * @return {number} + */ + unshift(...callsites: CallSite[]): number; + /** + * A no-op function as `CallSite` instances cannot be added to the end + * of the list. + * @return {number} + */ + push(): number; + /** + * Pops a `CallSite` off the end of the list. + * @return {CallSite|undefined} + */ + pop(): CallSite | undefined; + /** + * Converts this `CallSiteList` to a JSON object. + * @return {{ + * frame: { + * location: { + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * } + * }[]} + */ + toJSON(): { + frame: { + location: { + lineNumber: number | undefined; + columnNumber: number | undefined; + sourceURL: string | null; + isNative: boolean; + }; + isNative: boolean; + symbol: string | null; + source: string | null; + error: { + message: string; + name: string; + stack: string; + } | null; + }; + }[]; + #private; + } + export default CallSite; + export type ParsedStackFrame = { + sourceURL: string | null; + symbol: string; + column: number | undefined; + line: number | undefined; + native: boolean; + }; + const $previous: unique symbol; } + +declare module "socket:internal/error" { + /** + * The default `Error` class stack trace limit. + * @type {number} + */ + export const DEFAULT_ERROR_STACK_TRACE_LIMIT: number; + export const DefaultPlatformError: ErrorConstructor; + export const Error: ErrorConstructor; + export const URIError: ErrorConstructor; + export const EvalError: ErrorConstructor; + export const TypeError: ErrorConstructor; + export const RangeError: ErrorConstructor; + export const MediaError: ErrorConstructor; + export const SyntaxError: ErrorConstructor; + export const ReferenceError: ErrorConstructor; + export const AggregateError: ErrorConstructor; + export const RTCError: ErrorConstructor; + export const OverconstrainedError: ErrorConstructor; + export const GeolocationPositionError: ErrorConstructor; + export const ApplePayError: ErrorConstructor; + namespace _default { + export { Error }; + export { URIError }; + export { EvalError }; + export { TypeError }; + export { RangeError }; + export { MediaError }; + export { SyntaxError }; + export { ReferenceError }; + export { AggregateError }; + export { RTCError }; + export { OverconstrainedError }; + export { GeolocationPositionError }; + export { ApplePayError }; + } + export default _default; +} + declare module "socket:internal/geolocation" { /** * Get the current position of the device. @@ -8919,12 +16840,88 @@ declare module "socket:internal/geolocation" { let clearWatch: Function; } namespace _default { - export { getCurrentPosition }; - export { watchPosition }; - export { clearWatch }; + export { getCurrentPosition }; + export { watchPosition }; + export { clearWatch }; + } + export default _default; +} + +declare module "socket:internal/post-message" { + const _default: any; + export default _default; +} + +declare module "socket:service-worker/notification" { + export function showNotification(registration: any, title: any, options: any): Promise; + export function getNotifications(registration: any, options?: any): Promise; + namespace _default { + export { showNotification }; + export { getNotifications }; } export default _default; } + +declare module "socket:service-worker/registration" { + export class ServiceWorkerRegistration { + constructor(info: any, serviceWorker: any); + get scope(): any; + get updateViaCache(): string; + get installing(): any; + get waiting(): any; + get active(): any; + set onupdatefound(onupdatefound: any); + get onupdatefound(): any; + get navigationPreload(): any; + getNotifications(): Promise; + showNotification(title: any, options: any): Promise; + unregister(): Promise; + update(): Promise; + #private; + } + export default ServiceWorkerRegistration; +} + +declare module "socket:service-worker/container" { + /** + * Predicate to determine if service workers are allowed + * @return {boolean} + */ + export function isServiceWorkerAllowed(): boolean; + /** + * A `ServiceWorkerContainer` implementation that is attached to the global + * `globalThis.navigator.serviceWorker` object. + */ + export class ServiceWorkerContainer extends EventTarget { + get ready(): any; + get controller(): any; + /** + * A special initialization function for augmenting the global + * `globalThis.navigator.serviceWorker` platform `ServiceWorkerContainer` + * instance. + * + * All functions MUST be sure to what a lexically bound `this` becomes as the + * target could change with respect to the `internal` `Map` instance which + * contains private implementation properties relevant to the runtime + * `ServiceWorkerContainer` internal state implementations. + * @ignore + */ + init(): Promise; + register(scriptURL: any, options?: any): Promise; + getRegistration(clientURL: any): Promise; + getRegistrations(): Promise; + startMessages(): void; + } + export default ServiceWorkerContainer; + import { ServiceWorkerRegistration } from "socket:service-worker/registration"; +} + +declare module "socket:internal/service-worker" { + export const serviceWorker: ServiceWorkerContainer; + export default serviceWorker; + import { ServiceWorkerContainer } from "socket:service-worker/container"; +} + declare module "socket:internal/webassembly" { /** * The `instantiateStreaming()` function compiles and instantiates a WebAssembly @@ -8948,6 +16945,31 @@ declare module "socket:internal/webassembly" { } export default _default; } + +declare module "socket:internal/scheduler" { + export * from "socket:timers/scheduler"; + export default scheduler; + import scheduler from "socket:timers/scheduler"; +} + +declare module "socket:internal/timers" { + export function setTimeout(callback: any, ...args: any[]): number; + export function clearTimeout(timeout: any): any; + export function setInterval(callback: any, ...args: any[]): number; + export function clearInterval(interval: any): any; + export function setImmediate(callback: any, ...args: any[]): number; + export function clearImmediate(immediate: any): any; + namespace _default { + export { setTimeout }; + export { setInterval }; + export { setImmediate }; + export { clearTimeout }; + export { clearInterval }; + export { clearImmediate }; + } + export default _default; +} + declare module "socket:internal/pickers" { /** * @typedef {{ @@ -8955,6 +16977,7 @@ declare module "socket:internal/pickers" { * mode?: 'read' | 'readwrite' * startIn?: FileSystemHandle | 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos', * }} ShowDirectoryPickerOptions + * */ /** * Shows a directory picker which allows the user to select a directory. @@ -8981,7 +17004,7 @@ declare module "socket:internal/pickers" { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker} * @return {Promise} */ - export function showOpenFilePicker(options?: ShowOpenFilePickerOptions): Promise; + export function showOpenFilePicker(options?: ShowOpenFilePickerOptions | undefined): Promise; /** * @typedef {{ * id?: string, @@ -9001,7 +17024,7 @@ declare module "socket:internal/pickers" { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker} * @return {Promise} */ - export function showSaveFilePicker(options?: ShowSaveFilePickerOptions): Promise; + export function showSaveFilePicker(options?: ShowSaveFilePickerOptions | undefined): Promise; /** * Key-value store for general usage by the file pickers" * @ignore @@ -9023,8 +17046,8 @@ declare module "socket:internal/pickers" { export default _default; export type ShowDirectoryPickerOptions = { id?: string; - mode?: 'read' | 'readwrite'; - startIn?: FileSystemHandle | 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos'; + mode?: "read" | "readwrite"; + startIn?: FileSystemHandle | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; }; /** * ]?: string[] @@ -9034,15 +17057,15 @@ declare module "socket:internal/pickers" { export type object = { id?: string; excludeAcceptAllOption?: boolean; - startIn?: FileSystemHandle | 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos'; + startIn?: FileSystemHandle | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; types?: Array<{ description?: string; - [keyof]; + [keyof]: any; }>; }; - import { FileSystemHandle } from "socket:fs/web"; } -declare module "socket:internal/monkeypatch" { + +declare module "socket:internal/primitives" { export function init(): { natives: {}; patches: {}; @@ -9055,6 +17078,7 @@ declare module "socket:internal/monkeypatch" { const natives: {}; const patches: {}; } + declare module "socket:internal/init" { namespace _default { export { location }; @@ -9062,6 +17086,7 @@ declare module "socket:internal/init" { export default _default; import location from "socket:location"; } + declare module "socket:internal/worker" { export function onWorkerMessage(event: any): Promise; export function addEventListener(eventName: any, callback: any, ...args: any[]): any; @@ -9069,6 +17094,7 @@ declare module "socket:internal/worker" { export function dispatchEvent(event: any): any; export function postMessage(message: any, ...args: any[]): any; export function close(): any; + export function importScripts(...scripts: any[]): void; export const WorkerGlobalScopePrototype: any; /** * The absolute `URL` of the internal worker initialization entry. @@ -9103,6 +17129,7 @@ declare module "socket:internal/worker" { export { RUNTIME_WORKER_ID }; export { removeEventListener }; export { addEventListener }; + export { importScripts }; export { dispatchEvent }; export { postMessage }; export { source }; @@ -9111,6 +17138,537 @@ declare module "socket:internal/worker" { } export default _default; } + +declare module "socket:latica/worker" { + export {}; +} + +declare module "socket:npm/module" { + /** + * @typedef {{ + * package: Package + * origin: string, + * type: 'commonjs' | 'module', + * url: string + * }} ModuleResolution + */ + /** + * Resolves an NPM module for a given `specifier` and an optional `origin`. + * @param {string|URL} specifier + * @param {string|URL=} [origin] + * @param {{ prefix?: string, type?: 'commonjs' | 'module' }} [options] + * @return {ModuleResolution|null} + */ + export function resolve(specifier: string | URL, origin?: (string | URL) | undefined, options?: { + prefix?: string; + type?: "commonjs" | "module"; + }): ModuleResolution | null; + namespace _default { + export { resolve }; + } + export default _default; + export type ModuleResolution = { + package: Package; + origin: string; + type: "commonjs" | "module"; + url: string; + }; + import { Package } from "socket:commonjs/package"; +} + +declare module "socket:npm/service-worker" { + /** + * @ignore + * @param {Request} + * @param {object} env + * @param {import('../service-worker/context.js').Context} ctx + * @return {Promise} + */ + export function onRequest(request: any, env: object, ctx: import("socket:service-worker/context").Context): Promise; + /** + * Handles incoming 'npm:///' requests. + * @param {Request} request + * @param {object} env + * @param {import('../service-worker/context.js').Context} ctx + * @return {Response?} + */ + export default function _default(request: Request, env: object, ctx: import("socket:service-worker/context").Context): Response | null; +} + +declare module "socket:service-worker/global" { + export class ServiceWorkerGlobalScope { + get isServiceWorkerScope(): boolean; + get ExtendableEvent(): typeof ExtendableEvent; + get FetchEvent(): typeof FetchEvent; + get serviceWorker(): any; + set registration(value: any); + get registration(): any; + get clients(): import("socket:service-worker/clients").Clients; + set onactivate(listener: any); + get onactivate(): any; + set onmessage(listener: any); + get onmessage(): any; + set oninstall(listener: any); + get oninstall(): any; + set onfetch(listener: any); + get onfetch(): any; + skipWaiting(): Promise; + } + const _default: ServiceWorkerGlobalScope; + export default _default; + import { ExtendableEvent } from "socket:service-worker/events"; + import { FetchEvent } from "socket:service-worker/events"; +} + +declare module "socket:service-worker/init" { + export function onRegister(event: any): Promise; + export function onUnregister(event: any): Promise; + export function onSkipWaiting(event: any): Promise; + export function onActivate(event: any): Promise; + export function onFetch(event: any): Promise; + export function onNotificationShow(event: any, target: any): any; + export function onNotificationClose(event: any, target: any): void; + export function onGetNotifications(event: any, target: any): any; + export const workers: Map; + export class ServiceWorkerInstance extends Worker { + constructor(filename: any, options: any); + get info(): any; + get notifications(): any[]; + onMessage(event: any): Promise; + #private; + } + export class ServiceWorkerInfo { + constructor(data: any); + id: any; + url: any; + hash: any; + scope: any; + scriptURL: any; + get pathname(): string; + } + const _default: any; + export default _default; +} +declare function isTypedArray(object: any): boolean; +declare function isTypedArray(object: any): boolean; +declare function isArrayBuffer(object: any): object is ArrayBuffer; +declare function isArrayBuffer(object: any): object is ArrayBuffer; +declare function findMessageTransfers(transfers: any, object: any, options?: any): any; +declare function findMessageTransfers(transfers: any, object: any, options?: any): any; +declare const Uint8ArrayPrototype: Uint8Array; +declare const TypedArrayPrototype: any; +declare const TypedArray: any; +declare const ports: any[]; + +declare module "socket:service-worker/storage" { + /** + * A factory for creating storage interfaces. + * @param {'memoryStorage'|'localStorage'|'sessionStorage'} type + * @return {Promise} + */ + export function createStorageInterface(type: "memoryStorage" | "localStorage" | "sessionStorage"): Promise; + /** + * @typedef {{ done: boolean, value: string | undefined }} IndexIteratorResult + */ + /** + * An iterator interface for an `Index` instance. + */ + export class IndexIterator { + /** + * `IndexIterator` class constructor. + * @ignore + * @param {Index} index + */ + constructor(index: Index); + /** + * `true` if the iterator is "done", otherwise `false`. + * @type {boolean} + */ + get done(): boolean; + /** + * Returns the next `IndexIteratorResult`. + * @return {IndexIteratorResult} + */ + next(): IndexIteratorResult; + /** + * Mark `IndexIterator` as "done" + * @return {IndexIteratorResult} + */ + return(): IndexIteratorResult; + #private; + } + /** + * A container used by the `Provider` to index keys and values + */ + export class Index { + /** + * A reference to the keys in this index. + * @type {string[]} + */ + get keys(): string[]; + /** + * A reference to the values in this index. + * @type {string[]} + */ + get values(): string[]; + /** + * The number of entries in this index. + * @type {number} + */ + get length(): number; + /** + * Returns the key at a given `index`, if it exists otherwise `null`. + * @param {number} index} + * @return {string?} + */ + key(index: number): string | null; + /** + * Returns the value at a given `index`, if it exists otherwise `null`. + * @param {number} index} + * @return {string?} + */ + value(index: number): string | null; + /** + * Inserts a value in the index. + * @param {string} key + * @param {string} value + */ + insert(key: string, value: string): void; + /** + * Computes the index of a key in this index. + * @param {string} key + * @return {number} + */ + indexOf(key: string): number; + /** + * Clears all keys and values in the index. + */ + clear(): void; + /** + * Returns an entry at `index` if it exists, otherwise `null`. + * @param {number} index + * @return {string[]|null} + */ + entry(index: number): string[] | null; + /** + * Removes entries at a given `index`. + * @param {number} index + * @return {boolean} + */ + remove(index: number): boolean; + /** + * Returns an array of computed entries in this index. + * @return {IndexIterator} + */ + entries(): IndexIterator; + /** + * @ignore + * @return {IndexIterator} + */ + [Symbol.iterator](): IndexIterator; + #private; + } + /** + * A base class for a storage provider. + */ + export class Provider { + /** + * An error currently associated with the provider, likely from an + * async operation. + * @type {Error?} + */ + get error(): Error; + /** + * A promise that resolves when the provider is ready. + * @type {Promise} + */ + get ready(): Promise; + /** + * A reference the service worker storage ID, which is the service worker + * registration ID. + * @type {string} + * @throws DOMException + */ + get id(): string; + /** + * A reference to the provider `Index` + * @type {Index} + * @throws DOMException + */ + get index(): Index; + /** + * The number of entries in the provider. + * @type {number} + * @throws DOMException + */ + get length(): number; + /** + * Returns `true` if the provider has a value for a given `key`. + * @param {string} key} + * @return {boolean} + * @throws DOMException + */ + has(key: string): boolean; + /** + * Get a value by `key`. + * @param {string} key + * @return {string?} + * @throws DOMException + */ + get(key: string): string | null; + /** + * Sets a `value` by `key` + * @param {string} key + * @param {string} value + * @throws DOMException + */ + set(key: string, value: string): void; + /** + * Removes a value by `key`. + * @param {string} key + * @return {boolean} + * @throws DOMException + */ + remove(key: string): boolean; + /** + * Clear all keys and values. + * @throws DOMException + */ + clear(): void; + /** + * The keys in the provider index. + * @return {string[]} + * @throws DOMException + */ + keys(): string[]; + /** + * The values in the provider index. + * @return {string[]} + * @throws DOMException + */ + values(): string[]; + /** + * Returns the key at a given `index` + * @param {number} index + * @return {string|null} + * @throws DOMException + */ + key(index: number): string | null; + /** + * Loads the internal index with keys and values. + * @return {Promise} + */ + load(): Promise; + #private; + } + /** + * An in-memory storage provider. It just used the built-in provider `Index` + * for storing key-value entries. + */ + export class MemoryStorageProvider extends Provider { + } + /** + * A session storage provider that persists for the runtime of the + * application and through service worker restarts. + */ + export class SessionStorageProvider extends Provider { + /** + * Remove a value by `key`. + * @param {string} key + * @return {string?} + * @throws DOMException + * @throws NotFoundError + */ + remove(key: string): string | null; + } + /** + * A local storage provider that persists until the data is cleared. + */ + export class LocalStorageProvider extends Provider { + } + /** + * A generic interface for storage implementations + */ + export class Storage { + /** + * A factory for creating a `Storage` instance that is backed + * by a storage provider. Extending classes should define a `Provider` + * class that is statically available on the extended `Storage` class. + * @param {symbol} token + * @return {Promise>} + */ + static create(token: symbol): Promise; + /** + * `Storage` class constructor. + * @ignore + * @param {symbol} token + * @param {Provider} provider + */ + constructor(token: symbol, provider: Provider); + /** + * A readonly reference to the storage provider. + * @type {Provider} + */ + get provider(): Provider; + /** + * The number of entries in the storage. + * @type {number} + */ + get length(): number; + /** + * Returns `true` if the storage has a value for a given `key`. + * @param {string} key + * @return {boolean} + * @throws TypeError + */ + hasItem(key: string, ...args: any[]): boolean; + /** + * Clears the storage of all entries + */ + clear(): void; + /** + * Returns the key at a given `index` + * @param {number} index + * @return {string|null} + */ + key(index: number, ...args: any[]): string | null; + /** + * Get a storage value item for a given `key`. + * @param {string} key + * @return {string|null} + */ + getItem(key: string, ...args: any[]): string | null; + /** + * Removes a storage value entry for a given `key`. + * @param {string} + * @return {boolean} + */ + removeItem(key: any, ...args: any[]): boolean; + /** + * Sets a storage item `value` for a given `key`. + * @param {string} key + * @param {string} value + */ + setItem(key: string, value: string, ...args: any[]): void; + /** + * @ignore + */ + get [Symbol.toStringTag](): string; + #private; + } + /** + * An in-memory `Storage` interface. + */ + export class MemoryStorage extends Storage { + static Provider: typeof MemoryStorageProvider; + } + /** + * A locally persisted `Storage` interface. + */ + export class LocalStorage extends Storage { + static Provider: typeof LocalStorageProvider; + } + /** + * A session `Storage` interface. + */ + export class SessionStorage extends Storage { + static Provider: typeof SessionStorageProvider; + } + namespace _default { + export { Storage }; + export { LocalStorage }; + export { MemoryStorage }; + export { SessionStorage }; + export { createStorageInterface }; + } + export default _default; + export type IndexIteratorResult = { + done: boolean; + value: string | undefined; + }; +} + +declare module "socket:service-worker/worker" { + export function onReady(): void; + export function onMessage(event: any): Promise; + const _default: any; + export default _default; + export namespace SERVICE_WORKER_READY_TOKEN { + let __service_worker_ready: boolean; + } + export namespace module { + let exports: {}; + } + export const events: Set; + export namespace stages { + let register: Deferred; + let install: Deferred; + let activate: Deferred; + } + import { Deferred } from "socket:async"; +} + +declare module "socket:shared-worker/debug" { + export function debug(...args: any[]): void; + export default debug; +} + +declare module "socket:shared-worker/global" { + export class SharedWorkerGlobalScope { + get isSharedWorkerScope(): boolean; + set onconnect(listener: any); + get onconnect(): any; + } + const _default: SharedWorkerGlobalScope; + export default _default; +} + +declare module "socket:shared-worker/init" { + export function onInstall(event: any): Promise; + export function onUninstall(event: any): Promise; + export function onConnect(event: any): Promise; + export const workers: Map; + export { channel }; + export class SharedWorkerInstance extends Worker { + constructor(filename: any, options: any); + get info(): any; + onMessage(event: any): Promise; + #private; + } + export class SharedWorkerInfo { + constructor(data: any); + id: any; + port: any; + client: any; + scriptURL: any; + url: any; + hash: any; + get pathname(): string; + } + const _default: any; + export default _default; + import { channel } from "socket:shared-worker/index"; +} + +declare module "socket:shared-worker/state" { + export const state: any; + export default state; +} + +declare module "socket:shared-worker/worker" { + export function onReady(): void; + export function onMessage(event: any): Promise; + const _default: any; + export default _default; + export namespace SHARED_WORKER_READY_TOKEN { + let __shared_worker_ready: boolean; + } + export namespace module { + let exports: {}; + } + export const connections: Set; +} + declare module "socket:test/harness" { /** * @typedef {import('./index').Test} Test @@ -9158,12 +17716,12 @@ declare module "socket:test/harness" { * @param {new (options: object) => T} harnessClass * @returns {TapeTestFn} */ - export function wrapHarness(tapzero: typeof import("socket:test/index"), harnessClass: new (options: object) => T): exports.TapeTestFn; + export function wrapHarness(tapzero: typeof import("socket:test/index"), harnessClass: new (options: object) => T): TapeTestFn; export default exports; /** * @template {Harness} T */ - export class TapeHarness { + export class TapeHarness { /** * @param {import('./index.js')} tapzero * @param {new (options: object) => T} harnessClass @@ -9216,7 +17774,7 @@ declare module "socket:test/harness" { bootstrap(): Promise; close(): Promise; }; - export type TapeTestFn = { + export type TapeTestFn = { (name: string, cb?: (harness: T, test: Test) => (void | Promise)): void; (name: string, opts: object, cb: (harness: T, test: Test) => (void | Promise)): void; only(name: string, cb?: (harness: T, test: Test) => (void | Promise)): void; @@ -9227,13 +17785,15 @@ declare module "socket:test/harness" { import * as exports from "socket:test/harness"; } + declare module "socket:vm/init" { export {}; } -declare function reportError(e: any): void; -declare function reportError(err: any): void; declare function isTypedArray(object: any): boolean; -declare function isArrayBuffer(object: any): boolean; +declare function isTypedArray(object: any): boolean; +declare function isArrayBuffer(object: any): object is ArrayBuffer; +declare function isArrayBuffer(object: any): object is ArrayBuffer; +declare function findMessageTransfers(transfers: any, object: any, options?: any): any; declare function findMessageTransfers(transfers: any, object: any, options?: any): any; declare const Uint8ArrayPrototype: Uint8Array; declare const TypedArrayPrototype: any; @@ -9286,6 +17846,7 @@ declare class State { init(): void; onPortMessage(port: any, event: any): void; } + declare module "socket:vm/world" { export {}; } diff --git a/api/internal/async/hooks.js b/api/internal/async/hooks.js new file mode 100644 index 0000000000..632a9f69dd --- /dev/null +++ b/api/internal/async/hooks.js @@ -0,0 +1,285 @@ +import { wrap as asyncWrap } from '../../async/wrap.js' +import { toProperCase } from '../../util.js' +import { Variable } from '../../async/context.js' +import gc from '../../gc.js' + +let currentAsyncResourceId = 1 + +/** + * The default top level async resource ID + * @type {number} + */ +export const TOP_LEVEL_ASYNC_RESOURCE_ID = 1 + +/** + * The internal async hook state. + */ +export const state = { + defaultExecutionAsyncId: -1 +} + +/** + * The current async hooks enabled. + */ +export const hooks = { + init: [], + before: [], + after: [], + destroy: [], + promiseResolve: [] +} + +/** + * A base class for the `AsyncResource` class or other higher level async + * resource classes. + */ +export class CoreAsyncResource { + #type = null + #destroyed = false + #asyncId = getNextAsyncResourceId() + #triggerAsyncId = getDefaultExecutionAsyncId() + #requireManualDestroy = false + + /** + * `CoreAsyncResource` class constructor. + * @param {string} type + * @param {object|number=} [options] + */ + constructor (type, options = null) { + if (!type || typeof type !== 'string') { + throw new TypeError( + `Expecting 'type' to be a string. Received: ${type}` + ) + } + + if (typeof options === 'number') { + options = { triggerAsyncId: options } + } + + this.#type = type + + if (Number.isFinite(options?.triggerAsyncId) && options.triggerAsyncId > 0) { + this.#triggerAsyncId = options.triggerAsyncId + } else if (options?.triggerAsyncId !== undefined) { + throw new TypeError( + // eslint-disable-next-line + `Expecting 'options.triggerAsyncId' to be a positive number.` + + `Received: ${options.triggerAsyncId}` + ) + } + + if (typeof options?.requireManualDestroy === 'boolean') { + this.#requireManualDestroy = options.requireManualDestroy + } else if (options?.requireManualDestroy !== undefined) { + throw new TypeError( + // eslint-disable-next-line + `Expecting 'options.requireManualDestroy' to be a boolean.` + + `Received: ${options.requireManualDestroy}` + ) + } + + dispatch('init', this.asyncId(), this.type, this.triggerAsyncId(), this) + + if (!this.#requireManualDestroy) { + gc.ref(this) + } + } + + /** + * The `CoreAsyncResource` type. + * @type {string} + */ + get type () { + return this.#type + } + + /** + * `true` if the `CoreAsyncResource` was destroyed, otherwise `false`. This + * value is only set to `true` if `emitDestroy()` was called, likely from + * destroying the resource manually. + * @type {boolean} + */ + get destroyed () { + return this.#destroyed + } + + /** + * The unique async resource ID. + * @return {number} + */ + asyncId () { + return this.#asyncId + } + + /** + * The trigger async resource ID. + * @return {number} + */ + triggerAsyncId () { + return this.#triggerAsyncId + } + + /** + * Manually emits destroy hook for the resource. + * @return {CoreAsyncResource} + */ + emitDestroy () { + if (!this.#destroyed) { + this.#destroyed = true + } + + dispatch('destroy', this.asyncId(), this.type, this.triggerAsyncId(), this) + return this + } + + /** + * Binds function `fn` with an optional this `thisArg` binding to run + * in the execution context of this `CoreAsyncResource`. + * @param {function} fn + * @param {object=} [thisArg] + * @return {function} + */ + bind (fn, thisArg = undefined) { + const resource = this + let binding = null + + if (thisArg === undefined) { + binding = function (...args) { + args.unshift(fn, this) + return Reflect.apply(resource.runInAsyncScope, resource, args) + } + } else { + binding = resource.runInAsyncScope.bind(this, fn, thisArg) + } + + Object.defineProperty(binding, 'length', { + __proto__: null, + configurable: true, + enumerable: false, + writable: false, + value: fn.length + }) + + return binding + } + + /** + * Runs function `fn` in the execution context of this `CoreAsyncResource`. + * @param {function} fn + * @param {object=} [thisArg] + * @param {...any} [args] + * @return {any} + */ + runInAsyncScope (fn, thisArg, ...args) { + dispatch('before', this.asyncId(), this.type, this.triggerAsyncId(), this) + try { + return asyncContextVariable.run(this, fn.bind(thisArg), ...args) + } finally { + dispatch('after', this.asyncId(), this.type, this.triggerAsyncId(), this) + } + } + + /** + * Implements `gc.finalizer` for gc'd resource cleanup. + * @return {gc.Finalizer} + * @ignore + */ + [gc.finalizer] () { + return { + args: [this.asyncId(), this.type, this.triggerAsyncId()], + handle (asyncId, type, triggerAsyncId) { + dispatch('destroy', asyncId, type, triggerAsyncId) + } + } + } +} + +export class TopLevelAsyncResource extends CoreAsyncResource {} + +export function dispatch (hook, asyncId, type, triggerAsyncId, resource) { + if (hook in hooks) { + for (const callback of hooks[hook]) { + callback(asyncId, type, triggerAsyncId, resource) + } + } +} + +export function getNextAsyncResourceId () { + return ++currentAsyncResourceId +} + +export function executionAsyncResource () { + return asyncContextVariable.get() +} + +export function executionAsyncId () { + const resource = executionAsyncResource() + if (resource && typeof resource.asyncId === 'function') { + return resource.asyncId() + } + + return TOP_LEVEL_ASYNC_RESOURCE_ID +} + +export function triggerAsyncId () { + const revert = asyncContextVariable.revert + if (revert?.previousVariable) { + const resource = revert.previousVariable.get() + if (resource && typeof resource.asyncId === 'function') { + return resource.asyncId() + } + } + + return TOP_LEVEL_ASYNC_RESOURCE_ID +} + +export function getDefaultExecutionAsyncId () { + if (state.defaultExecutionAsyncId < 0) { + return executionAsyncId() + } + + return state.defaultExecutionAsyncId +} + +export function wrap ( + callback, + type, + asyncId = getNextAsyncResourceId(), + triggerAsyncId = getDefaultExecutionAsyncId(), + resource = undefined +) { + dispatch('init', asyncId, type, triggerAsyncId, resource) + callback = asyncWrap(callback) + return function (...args) { + dispatch('before', asyncId, type, triggerAsyncId) + try { + return (resource || topLevelAsyncResource).runInAsyncScope(() => { + // eslint-disable-next-line + return callback(...args) + }) + } finally { + dispatch('after', asyncId, type, triggerAsyncId) + } + } +} + +export function getTopLevelAsyncResourceName () { + if (globalThis.__args?.client) { + const { type, frameType } = globalThis.__args.client + return ( + frameType.replace('none', '').split('-').filter(Boolean).map(toProperCase).join('') + + toProperCase(type) + ) + } + + return 'TopLevel' +} + +export const asyncContextVariable = new Variable({ name: 'internal/async/hooks' }) +export const topLevelAsyncResource = new TopLevelAsyncResource( + getTopLevelAsyncResourceName() +) + +asyncContextVariable.defaultValue = topLevelAsyncResource + +export default hooks diff --git a/api/internal/callsite.js b/api/internal/callsite.js new file mode 100644 index 0000000000..d58bdd4b9e --- /dev/null +++ b/api/internal/callsite.js @@ -0,0 +1,919 @@ +import InternalSymbols from './symbols.js' +import { createHook } from '../async/hooks.js' +import { murmur3 } from '../crypto.js' +import { Buffer } from '../buffer.js' +import path from '../path.js' + +let isAsyncContext = false +const asyncContexts = new Set([ + 'Promise', + 'Timeout', + 'Interval', + 'Immediate', + 'Microtask' +]) + +const hook = createHook({ + before (asyncId, type) { + if (asyncContexts.has(type)) { + isAsyncContext = true + } + }, + + after (asyncId, type) { + if (asyncContexts.has(type)) { + isAsyncContext = false + } + } +}) + +hook.enable() + +/** + * @typedef {{ + * sourceURL: string | null, + * symbol: string, + * column: number | undefined, + * line: number | undefined, + * native: boolean + * }} ParsedStackFrame + */ + +/** + * A container for location data related to a `StackFrame` + */ +export class StackFrameLocation { + /** + * Creates a `StackFrameLocation` from JSON input. + * @param {object=} json + * @return {StackFrameLocation} + */ + static from (json) { + const location = new this() + + if (Number.isFinite(json?.lineNumber)) { + location.lineNumber = json.lineNumber + } + + if (Number.isFinite(json?.columnNumber)) { + location.columnNumber = json.columnNumber + } + + if (json?.sourceURL && URL.canParse(json.sourceURL)) { + location.sourceURL = new URL(json.sourceURL).href + } + + if (json?.isNative === true) { + location.isNative = true + } + + return location + } + + /** + * The line number of the location of the stack frame, if available. + * @type {number | undefined} + */ + lineNumber + + /** + * The column number of the location of the stack frame, if available. + * @type {number | undefined} + */ + columnNumber + + /** + * The source URL of the location of the stack frame, if available. This value + * may be `null`. + * @type {string?} + */ + sourceURL = null + + /** + * `true` if the stack frame location is in native location, otherwise + * this value `false` (default). + * @type + */ + isNative = false + + /** + * Converts this `StackFrameLocation` to a JSON object. + * @ignore + * @return {{ + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }} + */ + toJSON () { + return { + lineNumber: this.lineNumber, + columnNumber: this.columnNumber, + sourceURL: this.sourceURL, + isNative: this.isNative + } + } + + /** + * Serializes this `StackFrameLocation`, suitable for `postMessage()` transfers. + * @ignore + * @return {{ + * __type__: 'StackFrameLocation', + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }} + */ + [InternalSymbols.serialize] () { + return { __type__: 'StackFrameLocation', ...this.toJSON() } + } +} + +/** + * A stack frame container related to a `CallSite`. + */ +export class StackFrame { + /** + * Parses a raw stack frame string into structured data. + * @param {string} rawStackFrame + * @return {ParsedStackFrame} + */ + static parse (rawStackFrame) { + const parsed = { + sourceURL: null, + symbol: '', + column: undefined, + line: undefined + } + + const parts = rawStackFrame.split('@') + const symbol = parts.shift() + const location = parts.shift() + + if (symbol) { + parsed.symbol = symbol + } + + if (location === '[native code]') { + parsed.native = true + } else if (location && URL.canParse(location)) { + const url = new URL(location) + const [pathname, lineno, columnno] = url.pathname.split(':') + const line = parseInt(lineno) + const column = parseInt(columnno) + + if (Number.isFinite(line)) { + parsed.line = line + } + + if (Number.isFinite(column)) { + parsed.column = column + } + + parsed.sourceURL = new URL(pathname + url.search, url.origin).href + } + + return parsed + } + + /** + * Creates a new `StackFrame` from an `Error` and raw stack frame + * source `rawStackFrame`. + * @param {Error} error + * @param {string} rawStackFrame + * @return {StackFrame} + */ + static from (error, rawStackFrame) { + const parsed = this.parse(rawStackFrame) + return new this(error, parsed, rawStackFrame) + } + + /** + * The stack frame location data. + * @type {StackFrameLocation} + */ + location = new StackFrameLocation() + + /** + * The `Error` associated with this `StackFrame` instance. + * @type {Error?} + */ + error = null + + /** + * The name of the function where the stack frame is located. + * @type {string?} + */ + symbol = null + + /** + * The raw stack frame source string. + * @type {string?} + */ + source = null + + /** + * `StackFrame` class constructor. + * @param {Error} error + * @param {ParsedStackFrame=} [frame] + * @param {string=} [source] + */ + constructor (error, frame = null, source = null) { + if (error instanceof Error) { + this.error = error + } + + if (typeof source === 'string') { + this.source = source + } + + if (Number.isFinite(frame?.line)) { + this.location.lineNumber = frame.line + } + + if (Number.isFinite(frame?.column)) { + this.location.columnNumber = frame.column + } + + if (typeof frame?.sourceURL === 'string' && URL.canParse(frame.sourceURL)) { + this.location.sourceURL = frame.sourceURL + } + + if (typeof frame?.symbol === 'string') { + this.symbol = frame.symbol + } + + if (frame?.native === true) { + this.location.isNative = true + } + } + + /** + * Converts this `StackFrameLocation` to a JSON object. + * @ignore + * @return {{ + * location: { + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * }} + */ + toJSON () { + return { + location: this.location.toJSON(), + isNative: this.isNative, + symbol: this.symbol, + source: this.source, + error: this.error === null + ? null + : { + message: this.error.message ?? '', + name: this.error.name ?? '', + stack: String(this.error[CallSite.StackSourceSymbol] ?? this.error.stack ?? '') + } + } + } + + /** + * Serializes this `StackFrame`, suitable for `postMessage()` transfers. + * @ignore + * @return {{ + * __type__: 'StackFrame', + * location: { + * __type__: 'StackFrameLocation', + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * }} + */ + [InternalSymbols.serialize] () { + return { + __type__: 'StackFrame', + ...this.toJSON(), + location: this.location[InternalSymbols.serialize]() + } + } +} + +// private symbol for `CallSiteList` previous reference +const $previous = Symbol('previous') + +/** + * A v8 compatible interface and container for call site information. + */ +export class CallSite { + /** + * An internal symbol used to refer to the index of a promise in + * `Promise.all` or `Promise.any` function call site. + * @ignore + * @type {symbol} + */ + static PromiseElementIndexSymbol = Symbol.for('socket.runtime.CallSite.PromiseElementIndex') + + /** + * An internal symbol used to indicate that a call site is in a `Promise.all` + * function call. + * @ignore + * @type {symbol} + */ + static PromiseAllSymbol = Symbol.for('socket.runtime.CallSite.PromiseAll') + + /** + * An internal symbol used to indicate that a call site is in a `Promise.any` + * function call. + * @ignore + * @type {symbol} + */ + static PromiseAnySymbol = Symbol.for('socket.runtime.CallSite.PromiseAny') + + /** + * An internal source symbol used to store the original `Error` stack source. + * @ignore + * @type {symbol} + */ + static StackSourceSymbol = Symbol.for('socket.runtime.CallSite.StackSource') + + #error = null + #frame = null + #previous = null + + /** + * `CallSite` class constructor + * @param {Error} error + * @param {string} rawStackFrame + * @param {CallSite=} previous + */ + constructor (error, rawStackFrame, previous = null) { + this.#error = error + this.#frame = StackFrame.from(error, rawStackFrame) + if (previous !== null && previous instanceof CallSite) { + this.#previous = previous + } + } + + /** + * Private accessor to "friend class" `CallSiteList`. + * @ignore + */ + get [$previous] () { return this.#previous } + set [$previous] (previous) { + if (previous === null || previous instanceof CallSite) { + this.#previous = previous + } + } + + /** + * The `Error` associated with the call site. + * @type {Error} + */ + get error () { + return this.#error + } + + /** + * The previous `CallSite` instance, if available. + * @type {CallSite?} + */ + get previous () { + return this.#previous + } + + /** + * A reference to the `StackFrame` data. + * @type {StackFrame} + */ + get frame () { + return this.#frame + } + + /** + * This function _ALWAYS__ returns `globalThis` as `this` cannot be determined. + * @return {object} + */ + getThis () { + // not supported + return globalThis + } + + /** + * This function _ALWAYS__ returns `null` as the type name of `this` + * cannot be determined. + * @return {null} + */ + getTypeName () { + // not supported + return null + } + + /** + * This function _ALWAYS__ returns `undefined` as the current function + * reference cannot be determined. + * @return {undefined} + */ + getFunction () { + // not supported + return undefined + } + + /** + * Returns the name of the function in at the call site, if available. + * @return {string|undefined} + */ + getFunctionName () { + const symbol = this.#frame.symbol + + if (symbol === 'global code' || symbol === 'module code' || symbol === 'eval code') { + return undefined + } + + return symbol + } + + /** + * An alias to `getFunctionName() + * @return {string} + */ + getMethodName () { + return this.getFunctionName() + } + + /** + * Get the filename of the call site location, if available, otherwise this + * function returns 'unknown location'. + * @return {string} + */ + getFileName () { + if (this.#frame.location.sourceURL) { + const root = new URL('../../', import.meta.url || globalThis.location.href).pathname + + let filename = new URL(this.#frame.location.sourceURL).pathname.replace(root, '') + + if (/\/socket\//.test(filename)) { + filename = filename.replace('socket/', 'socket:').replace(/.js$/, '') + return filename + } + + return path.basename(new URL(this.#frame.location.sourceURL).pathname) + } + + return 'unknown location' + } + + /** + * Returns the location source URL defaulting to the global location. + * @return {string} + */ + getScriptNameOrSourceURL () { + const url = new URL(this.#frame.location.sourceURL ?? globalThis.location.href) + let filename = url.pathname.replace(url.pathname, '') + + if (/\/socket\//.test(filename)) { + filename = filename.replace('socket/', 'socket:').replace(/.js$/, '') + return filename + } + + return url.href + } + + /** + * Returns a hash value of the source URL return by `getScriptNameOrSourceURL()` + * @return {string} + */ + getScriptHash () { + return Buffer.from(String(murmur3(this.getScriptNameOrSourceURL()))).toString('hex') + } + + /** + * Returns the line number of the call site location. + * This value may be `undefined`. + * @return {number|undefined} + */ + getLineNumber () { + return this.#frame.lineNumber + } + + /** + * @ignore + * @return {number} + */ + getPosition () { + return 0 + } + + /** + * Attempts to get an "enclosing" line number, potentially the previous + * line number of the call site + * @param {number|undefined} + */ + getEnclosingLineNumber () { + if (this.#previous) { + const previousSourceURL = this.#previous.getScriptNameOrSourceURL() + if (previousSourceURL && previousSourceURL === this.getScriptNameOrSourceURL()) { + return this.#previous.getLineNumber() + } + } + } + + /** + * Returns the column number of the call site location. + * This value may be `undefined`. + * @return {number|undefined} + */ + getColumnNumber () { + return this.#frame.columnNumber + } + + /** + * Attempts to get an "enclosing" column number, potentially the previous + * line number of the call site + * @param {number|undefined} + */ + getEnclosingColumnNumber () { + if (this.#previous) { + const previousSourceURL = this.#previous.getScriptNameOrSourceURL() + if (previousSourceURL && previousSourceURL === this.getScriptNameOrSourceURL()) { + return this.#previous.getColumnNumber() + } + } + } + + /** + * Gets the origin of where `eval()` was called if this call site function + * originated from a call to `eval()`. This function may return `undefined`. + * @return {string|undefined} + */ + getEvalOrigin () { + let current = this + + while (current) { + if (current.frame.symbol === 'eval' && current.frame.location.isNative) { + const previous = current.previous + if (previous) { + return previous.location.sourceURL + } + } + + current = this.previous + } + } + + /** + * This function _ALWAYS__ returns `false` as `this` cannot be determined so + * "top level" detection is not possible. + * @return {boolean} + */ + isTopLevel () { + return false + } + + /** + * Returns `true` if this call site originated from a call to `eval()`. + * @return {boolean} + */ + isEval () { + let current = this + + while (current) { + if (current.frame.symbol === 'eval' || current.frame.symbol === 'eval code') { + return true + } + + current = this.previous + } + + return false + } + + /** + * Returns `true` if the call site is in a native location, otherwise `false`. + * @return {boolean} + */ + isNative () { + return this.#frame.location.isNative + } + + /** + * This function _ALWAYS_ returns `false` as constructor detection + * is not possible. + * @return {boolean} + */ + isConstructor () { + // not supported + return false + } + + /** + * Returns `true` if the call site is in async context, otherwise `false`. + * @return {boolean} + */ + isAsync () { + return isAsyncContext + } + + /** + * Returns `true` if the call site is in a `Promise.all()` function call, + * otherwise `false. + * @return {boolean} + */ + isPromiseAll () { + return this.#error[CallSite.PromiseAllSymbol] === true + } + + /** + * Gets the index of the promise element that was followed in a + * `Promise.all()` or `Promise.any()` function call. If not available, then + * this function returns `null`. + * @return {number|null} + */ + getPromiseIndex () { + return this.#error[CallSite.PromiseElementIndexSymbol] ?? null + } + + /** + * Converts this call site to a string. + * @return {string} + */ + toString () { + const { symbol, location } = this.#frame + const output = [symbol] + + if (location.sourceURL) { + const pathname = new URL(location.sourceURL).pathname + const root = new URL('../../', import.meta.url || globalThis.location.href).pathname + + let filename = pathname.replace(root, '') + + if (/\/?socket\//.test(filename)) { + filename = filename.replace('socket/', 'socket:').replace(/.js$/, '') + } + + if (location.lineNumber && location.columnNumber) { + output.push(`(${filename}:${location.lineNumber}:${location.columnNumber})`) + } else if (location.lineNumber) { + output.push(`(${filename}:${location.lineNumber})`) + } else { + output.push(`${filename}`) + } + } + + return output.filter(Boolean).join(' ') + } + + /** + * Converts this `CallSite` to a JSON object. + * @ignore + * @return {{ + * frame: { + * location: { + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * } + * }} + */ + toJSON () { + return { + frame: this.#frame.toJSON() + } + } + + /** + * Serializes this `CallSite`, suitable for `postMessage()` transfers. + * @ignore + * @return {{ + * __type__: 'CallSite', + * frame: { + * __type__: 'StackFrame', + * location: { + * __type__: 'StackFrameLocation', + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * } + * }} + */ + [InternalSymbols.serialize] () { + return { + __type__: 'CallSite', + frame: this.#frame[InternalSymbols.serialize]() + } + } +} + +/** + * An array based list container for `CallSite` instances. + */ +export class CallSiteList extends Array { + /** + * Creates a `CallSiteList` instance from `Error` input. + * @param {Error} error + * @param {string} source + * @return {CallSiteList} + */ + static from (error, source) { + const callsites = new this( + error, + source.split('\n').slice(0, Error.stackTraceLimit) + ) + + for (const source of callsites.sources.reverse()) { + callsites.unshift(new CallSite(error, source, callsites[0])) + } + + return callsites + } + + #error = null + #sources = [] + + /** + * `CallSiteList` class constructor. + * @param {Error} error + * @param {string[]=} [sources] + */ + constructor (error, sources = null) { + super() + this.#error = error + + if (Array.isArray(sources)) { + this.#sources = Array.from(sources) // copy + } else if (typeof error.stack === 'string') { + this.#sources = error.stack.split('\n') + } else if (typeof error[CallSiteList.StackSourceSymbol] === 'string') { + this.#sources = error[CallSiteList.StackSourceSymbol].split('\n') + } + } + + /** + * A reference to the `Error` for this `CallSiteList` instance. + * @type {Error} + */ + get error () { + return this.#error + } + + /** + * An array of stack frame source strings. + * @type {string[]} + */ + get sources () { + return this.#sources + } + + /** + * The original stack string derived from the sources. + * @type {string} + */ + get stack () { + return this.#sources.join('\n') + } + + /** + * Adds `CallSite` instances to the top of the list, linking previous + * instances to the next one. + * @param {...CallSite} callsites + * @return {number} + */ + unshift (...callsites) { + for (const callsite of callsites) { + if (callsite instanceof CallSite) { + callsite[$previous] = this[0] + super.unshift(callsite) + } + } + + return this.length + } + + /** + * A no-op function as `CallSite` instances cannot be added to the end + * of the list. + * @return {number} + */ + push () { + // no-op + return this.length + } + + /** + * Pops a `CallSite` off the end of the list. + * @return {CallSite|undefined} + */ + pop () { + const value = super.pop() + + if (this.length >= 1) { + this[this.length - 1][$previous] = null + } + + return value + } + + /** + * Converts the `CallSiteList` to a string combining the error name, message, + * and callsite stack information into a friendly string. + * @return {string} + */ + toString () { + const stack = this.map((callsite) => ` at ${callsite.toString()}`) + if (this.#error.name && this.#error.message) { + return [`${this.#error.name}: ${this.#error.message}`].concat(stack).join('\n') + } else if (this.#error.name) { + return [this.#error.name].concat(stack).join('\n') + } else { + return stack.join('\n') + } + } + + /** + * Converts this `CallSiteList` to a JSON object. + * @return {{ + * frame: { + * location: { + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * } + * }[]} + */ + toJSON () { + return Array.from(this.map((callsite) => callsite.toJSON())) + } + + /** + * Serializes this `CallSiteList`, suitable for `postMessage()` transfers. + * @ignore + * @return {Array<{ + * __type__: 'CallSite', + * frame: { + * __type__: 'StackFrame', + * location: { + * __type__: 'StackFrameLocation', + * lineNumber: number | undefined, + * columnNumber: number | undefined, + * sourceURL: string | null, + * isNative: boolean + * }, + * isNative: boolean, + * symbol: string | null, + * source: string | null, + * error: { message: string, name: string, stack: string } | null + * } + * }>} + */ + [InternalSymbols.serialize] () { + return this.toJSON() + } + + /** + * @ignore + */ + [Symbol.for('socket.runtime.util.inspect.custom')] () { + return this.toString() + } +} + +/** + * Creates an ordered and link array of `CallSite` instances from a + * given `Error`. + * @param {Error} error + * @param {string} source + * @return {CallSite[]} + */ +export function createCallSites (error, source) { + return CallSiteList.from(error, source) +} + +export default CallSite diff --git a/api/internal/conduit.js b/api/internal/conduit.js new file mode 100644 index 0000000000..f9351f88d0 --- /dev/null +++ b/api/internal/conduit.js @@ -0,0 +1,585 @@ +/* global CloseEvent, ErrorEvent, MessageEvent, WebSocket */ +import diagnostics from '../diagnostics.js' +import { sleep } from '../timers.js' +import client from '../application/client.js' +import hooks from '../hooks.js' +import ipc from '../ipc.js' +import gc from '../gc.js' + +/** + * Predicate state to determine if application is paused. + * @type {boolean} + */ +let isApplicationPaused = false + +/** + * The default Conduit port + * @type {number} + */ +let defaultConduitPort = globalThis.__args.conduit || 0 + +/** + * @typedef {{ options: object, payload: Uint8Array }} ReceiveMessage + * @typedef {function(Error?, ReceiveCallback | undefined)} ReceiveCallback + * @typedef {{ id?: string|BigInt|number, reconnect?: {} }} ConduitOptions + * @typedef {{ isActive: boolean, handles: { ids: string[], count: number }}} ConduitDiagnostics + * @typedef {{ isActive: boolean, port: number }} ConduitStatus + */ +export const DEFALUT_MAX_RECONNECT_RETRIES = 32 +export const DEFAULT_MAX_RECONNECT_TIMEOUT = 256 + +/** + * A pool of known `Conduit` instances. + * @type {Set} + */ +export const pool = new Set() + +// reconnect when application resumes +hooks.onApplicationResume(() => { + isApplicationPaused = false + for (const conduit of pool) { + // @ts-ignore + if (conduit?.shouldReconnect) { + // @ts-ignore + conduit.reconnect() + } else { + pool.delete(conduit) + } + } +}) + +hooks.onApplicationPause(() => { + isApplicationPaused = true + for (const conduit of pool) { + if (conduit) { + // @ts-ignore + conduit.isConnecting = false + // @ts-ignore + conduit.isActive = false + // @ts-ignore + if (conduit.socket && conduit.socket.readyState === WebSocket.OPEN) { + // @ts-ignore + conduit.socket?.close() + } + + // @ts-ignore + conduit.socket = null + } + } +}) + +/** + * A container for managing a WebSocket connection to the internal runtime + * Conduit WebSocket server. + */ +export class Conduit extends EventTarget { + /** + * The global `Conduit` port + * @type {number} + */ + static get port () { return defaultConduitPort } + static set port (port) { + if (port && typeof port === 'number') { + defaultConduitPort = port + } + } + + /** + * Returns diagnostics information about the conduit server + * @return {Promise} + */ + static async diagnostics () { + const query = await diagnostics.runtime.query() + // @ts-ignore + return query.conduit + } + + /** + * Returns the current Conduit server status + * @return {Promise} + */ + static async status () { + const result = await ipc.request('internal.conduit.status') + + if (result.err) { + throw result.err + } + + return { + port: result.data.port || 0, + isActive: result.data.isActive || false + } + } + + /** + * Waits for conduit to be active + * @param {{ maxQueriesForStatus?: number }=} [options] + * @return {Promise} + */ + static async waitForActiveState (options = null) { + const maxQueriesForStatus = options?.maxQueriesForStatus ?? Infinity + let queriesForStatus = 0 + while (queriesForStatus++ < maxQueriesForStatus) { + const status = await Conduit.status() + + if (status.isActive) { + break + } + + await sleep(256) + } + } + + /** + * @type {boolean} + */ + shouldReconnect = true + + /** + * @type {boolean} + */ + isConnecting = false + + /** + * @type {boolean} + */ + isActive = false + + /** + * @type {WebSocket?} + */ + socket = null + + /** + * @type {number} + */ + port = 0 + + /** + * @type {number?} + */ + id = null + + /** + * @private + * @type {function(MessageEvent)} + */ + #onmessage = null + + /** + * @private + * @type {function(CloseEvent)} + */ + #onclose = null + + /** + * @private + * @type {function(ErrorEvent)} + */ + #onerror = null + + /** + * @type {function(Event)} + */ + #onopen = null + + /** + * Creates an instance of Conduit. + * + * @param {object} params - The parameters for the Conduit. + * @param {string} params.id - The ID for the connection. + * @param {string} params.method - The method to use for the connection. + */ + constructor ({ id }) { + super() + + this.id = id + // @ts-ignore + this.port = this.constructor.port + this.connect() + + pool.add(this) + gc.ref(this) + } + + /** + * The URL string for the WebSocket server. + * @type {string} + */ + get url () { + return `ws://localhost:${this.port}/${this.id}/${client.top.id}` + } + + /** + * @type {function(MessageEvent)} + */ + get onmessage () { return this.#onmessage } + set onmessage (onmessage) { + if (typeof this.#onmessage === 'function') { + this.removeEventListener('message', this.#onmessage) + } + + this.#onmessage = null + + if (typeof onmessage === 'function') { + this.#onmessage = onmessage + this.addEventListener('message', onmessage) + } + } + + /** + * @type {function(ErrorEvent)} + */ + get onerror () { return this.#onerror } + set onerror (onerror) { + if (typeof this.#onerror === 'function') { + this.removeEventListener('error', this.#onerror) + } + + this.#onerror = null + + if (typeof onerror === 'function') { + this.#onerror = onerror + this.addEventListener('error', onerror) + } + } + + /** + * @type {function(CloseEvent)} + */ + get onclose () { return this.#onclose } + set onclose (onclose) { + if (typeof this.#onclose === 'function') { + this.removeEventListener('close', this.#onclose) + } + + this.#onclose = null + + if (typeof onclose === 'function') { + this.#onclose = onclose + this.addEventListener('close', onclose) + } + } + + /** + * @type {function(Event)} + */ + get onopen () { return this.#onopen } + set onopen (onopen) { + if (typeof this.#onopen === 'function') { + this.removeEventListener('open', this.#onopen) + } + + this.#onopen = null + + if (typeof onopen === 'function') { + this.#onopen = onopen + this.addEventListener('open', onopen) + } + } + + /** + * Connects the underlying conduit `WebSocket`. + * @param {function(Error?)=} [callback] + * @return {Promise} + */ + async connect (callback = null) { + if (this.isConnecting) { + callback(new Error('Application is connecting')) + return this + } + + if (isApplicationPaused) { + callback(new Error('Application is paused')) + return this + } + + if (this.socket) { + this.socket.close() + } + + // reset + this.socket = null + this.isActive = false + this.isConnecting = true + + // @ts-ignore + const resolvers = Promise.withResolvers() + const result = await ipc.request('internal.conduit.start') + + if (result.err) { + if (typeof callback === 'function') { + callback(result.err) + return this + } else { + throw result.err + } + } + + await Conduit.waitForActiveState() + + if (isApplicationPaused) { + callback(new Error('Application is paused')) + return this + } + + this.port = result.data.port + + this.socket = new WebSocket(this.url) + this.socket.binaryType = 'arraybuffer' + this.socket.onerror = (e) => { + this.socket = null + this.isActive = false + this.isConnecting = false + this.dispatchEvent(new ErrorEvent('error', e)) + + if (typeof callback === 'function') { + callback(e.error || new Error('Failed to connect Conduit')) + callback = null + } + + if (!isApplicationPaused) { + resolvers.reject(e.error ?? new Error()) + } + } + + this.socket.onclose = (e) => { + this.socket = null + this.isConnecting = false + this.isActive = false + this.dispatchEvent(new CloseEvent('close', e)) + if (this.shouldReconnect && !isApplicationPaused) { + this.reconnect() + } + } + + this.socket.onopen = (e) => { + this.isActive = true + this.isConnecting = false + this.dispatchEvent(new Event('open', e)) + + if (typeof callback === 'function') { + callback(null) + callback = null + } + + resolvers.resolve() + } + + this.socket.onmessage = (e) => { + this.isActive = true + this.isConnecting = false + this.dispatchEvent(new MessageEvent('message', e)) + } + + await resolvers.promise + return this + } + + /** + * Reconnects a `Conduit` socket. + * @param {{retries?: number, timeout?: number}} [options] + * @return {Promise} + */ + async reconnect (options = null) { + if (this.isConnecting) { + return this + } + + const retries = options?.retries ?? DEFALUT_MAX_RECONNECT_RETRIES + const timeout = options?.timeout ?? DEFAULT_MAX_RECONNECT_TIMEOUT + + return await new Promise((resolve, reject) => { + queueMicrotask(() => { + const promise = this.connect((err) => { + if (err) { + this.isActive = false + if (retries > 0) { + setTimeout(() => this.reconnect({ + retries: retries - 1, + timeout + }), timeout) + } + } + }) + + return promise.then(resolve, reject) + }) + }) + } + + /** + * Encodes a single header into a Uint8Array. + * + * @private + * @param {string} key - The header key. + * @param {string} value - The header value. + * @returns {Uint8Array} The encoded header. + */ + encodeOption (key, value) { + const keyLength = key.length + const keyBuffer = new TextEncoder().encode(key) + + const valueBuffer = new TextEncoder().encode(value) + const valueLength = valueBuffer.length + + const buffer = new ArrayBuffer(1 + keyLength + 2 + valueLength) + const view = new DataView(buffer) + + view.setUint8(0, keyLength) + new Uint8Array(buffer, 1, keyLength).set(keyBuffer) + + view.setUint16(1 + keyLength, valueLength, false) + new Uint8Array(buffer, 3 + keyLength, valueLength).set(valueBuffer) + + return new Uint8Array(buffer) + } + + /** + * Encodes options and payload into a single Uint8Array message. + * + * @private + * @param {object} options - The options to encode. + * @param {Uint8Array} payload - The payload to encode. + * @returns {Uint8Array} The encoded message. + */ + encodeMessage (options, payload) { + const headerBuffers = Object.entries(options) + .map(([key, value]) => this.encodeOption(key, value)) + + const totalOptionLength = headerBuffers.reduce((sum, buf) => sum + buf.length, 0) + const bodyLength = payload.length + const buffer = new ArrayBuffer(1 + totalOptionLength + 2 + bodyLength) + const view = new DataView(buffer) + + view.setUint8(0, headerBuffers.length) + + let offset = 1 + + headerBuffers.forEach(headerBuffer => { + new Uint8Array(buffer, offset, headerBuffer.length).set(headerBuffer) + offset += headerBuffer.length + }) + + view.setUint16(offset, bodyLength, false) + offset += 2 + + new Uint8Array(buffer, offset, bodyLength).set(payload) + + return new Uint8Array(buffer) + } + + /** + * Decodes a Uint8Array message into options and payload. + * @param {Uint8Array} data - The data to decode. + * @return {ReceiveMessage} The decoded message containing options and payload. + * @throws Will throw an error if the data is invalid. + */ + decodeMessage (data) { + const view = new DataView(data.buffer) + const numOpts = view.getUint8(0) + + let offset = 1 + const options = {} + + for (let i = 0; i < numOpts; i++) { + const keyLength = view.getUint8(offset) + offset += 1 + + const key = new TextDecoder().decode(new Uint8Array(data.buffer, offset, keyLength)) + offset += keyLength + + const valueLength = view.getUint16(offset, false) + offset += 2 + + const valueBuffer = new Uint8Array(data.buffer, offset, valueLength) + offset += valueLength + + const value = new TextDecoder().decode(valueBuffer) + options[key] = value + } + + const bodyLength = view.getUint16(offset, false) + offset += 2 + + const payload = new Uint8Array(data.buffer, offset, bodyLength) + return { options, payload } + } + + /** + * Registers a callback to handle incoming messages. + * The callback will receive an error object and an object containing + * decoded options and payload. + * @param {ReceiveCallback} callback - The callback function to handle incoming messages. + */ + receive (callback) { + this.addEventListener('error', (event) => { + // @ts-ignore + callback(event.error || new Error()) + }) + + this.addEventListener('message', (event) => { + // @ts-ignore + const data = new Uint8Array(event.data) + callback(null, this.decodeMessage(data)) + }) + } + + /** + * Sends a message with the specified options and payload over the + * WebSocket connection. + * @param {object} options - The options to send. + * @param {Uint8Array=} [payload] - The payload to send. + * @return {boolean} + */ + send (options, payload = null) { + if (isApplicationPaused || !this.isActive) { + return false + } + + if (!payload) { + payload = new Uint8Array(0) + } + + if ( + this.socket !== null && + this.socket instanceof WebSocket && + this.socket.readyState === WebSocket.OPEN + ) { + this.socket.send(this.encodeMessage(options, payload)) + return true + } + + return false + } + + /** + * Closes the WebSocket connection, preventing reconnects. + */ + close () { + this.shouldReconnect = false + + if (this.socket) { + this.socket.close() + this.socket = null + } + + pool.delete(this) + } + + /** + * Implements `gc.finalizer` for gc'd resource cleanup. + * @ignore + * @return {gc.Finalizer} + */ + [gc.finalizer] () { + return { + args: [this.socket], + handle (socket) { + if (socket?.readyState === WebSocket.OPEN) { + socket.close() + } + } + } + } +} diff --git a/api/internal/database.js b/api/internal/database.js new file mode 100644 index 0000000000..316ceb4c4f --- /dev/null +++ b/api/internal/database.js @@ -0,0 +1,1026 @@ +/* global ErrorEvent, IDBTransaction */ +import gc from '../gc.js' + +/** + * A typed container for optional options given to the `Database` + * class constructor. + * + * @typedef {{ + * version?: string | undefined + * }} DatabaseOptions + */ + +/** + * A typed container for various optional options made to a `get()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined, + * count?: number | undefined + * }} DatabaseGetOptions + */ + +/** + * A typed container for various optional options made to a `put()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined, + * durability?: 'strict' | 'relaxed' | undefined + * }} DatabasePutOptions + */ + +/** + * A typed container for various optional options made to a `delete()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined + * }} DatabaseDeleteOptions + */ + +/** + * A typed container for optional options given to the `Database` + * class constructor. + * + * @typedef {{ + * offset?: number | undefined, + * backlog?: number | undefined + * }} DatabaseRequestQueueWaitOptions + */ + +/** + * A typed container for various optional options made to a `entries()` function + * on a `Database` instance. + * + * @typedef {{ + * store?: string | undefined, + * stores?: string[] | undefined + * }} DatabaseEntriesOptions + */ + +/** + * A `DatabaseRequestQueueRequestConflict` callback function type. + * @typedef {function(Event, DatabaseRequestQueueRequestConflict): any} DatabaseRequestQueueConflictResolutionCallback + */ + +/** + * Waits for an event of `eventType` to be dispatched on a given `EventTarget`. + * @param {EventTarget} target + * @param {string} eventType + * @return {Promise} + */ +export async function waitFor (target, eventType) { + return await new Promise((resolve) => { + target.addEventListener(eventType, resolve, { once: true }) + }) +} + +/** + * A mapping of named `Database` instances that are currently opened + * @type {Map>} + */ +export const opened = new Map() + +/** + * A container for conflict resolution for a `DatabaseRequestQueue` instance + * `IDBRequest` instance. + */ +export class DatabaseRequestQueueRequestConflict { + /** + * Internal conflict resolver callback. + * @type {function(any): void)} + */ + #resolve = null + + /** + * Internal conflict rejection callback. + * @type {function(Error): void)} + */ + #reject = null + + /** + * Internal conflict cleanup callback. + * @type {function(): void)} + */ + #cleanup = null + + /** + * `DatabaseRequestQueueRequestConflict` class constructor + * @param {function(any): void)} resolve + * @param {function(Error): void)} reject + * @param {function(): void)} cleanup + */ + constructor (resolve, reject, cleanup) { + this.#resolve = resolve + this.#reject = reject + this.#cleanup = cleanup + } + + /** + * Called when a conflict is resolved. + * @param {any} argument + */ + resolve (argument = undefined) { + if (typeof this.#resolve === 'function') { + this.#resolve(argument) + } + + if (typeof this.#cleanup === 'function') { + this.#cleanup() + } + + this.#reject = null + this.#resolve = null + this.#cleanup = null + } + + /** + * Called when a conflict is rejected + * @param {Error} error + */ + reject (error) { + if (typeof this.#reject === 'function') { + this.#reject(error) + } + + if (typeof this.#cleanup === 'function') { + this.#cleanup() + } + + this.#reject = null + this.#resolve = null + this.#cleanup = null + } +} + +/** + * An event dispatched on a `DatabaseRequestQueue` + */ +export class DatabaseRequestQueueEvent extends Event { + #request = null + + /** + * `DatabaseRequestQueueEvent` class constructor. + * @param {string} type + * @param {IDBRequest|IDBTransaction} request + */ + constructor (type, request) { + super(type) + this.#request = request + } + + /** + * A reference to the underlying request for this event. + * @type {IDBRequest|IDBTransaction} + */ + get request () { + return this.#request + } +} + +/** + * An event dispatched on a `Database` + */ +export class DatabaseEvent extends Event { + /** + * An internal reference to the underlying database for this event. + * @type {Database} + */ + #database = null + + /** + * `DatabaseEvent` class constructor. + * @param {string} type + * @param {Database} database + */ + constructor (type, database) { + super(type) + this.#database = database + } + + /** + * A reference to the underlying database for this event. + * @type {Database} + */ + get database () { + return this.#database + } +} + +/** + * An error event dispatched on a `DatabaseRequestQueue` + */ +export class DatabaseRequestQueueErrorEvent extends ErrorEvent { + #request = null + + /** + * `DatabaseRequestQueueErrorEvent` class constructor. + * @param {string} type + * @param {IDBRequest|IDBTransaction} request + * @param {{ error: Error, cause?: Error }} options + */ + constructor (type, request, options) { + super(type, options) + this.#request = request + } + + /** + * A reference to the underlying request for this error event. + * @type {IDBRequest|IDBTransaction} + */ + get request () { + return this.#request + } +} + +/** + * A container for various `IDBRequest` and `IDBTransaction` instances + * occurring during the life cycles of a `Database` instance. + */ +export class DatabaseRequestQueue extends EventTarget { + #queue = [] + #promises = [] + + /** + * Computed queue length + * @type {number} + */ + get length () { + return this.#queue.length + } + + /** + * Pushes an `IDBRequest` or `IDBTransaction onto the queue and returns a + * `Promise` that resolves upon a 'success' or 'complete' event and rejects + * upon an error' event. + * @param {IDBRequest|IDBTransaction} + * @param {?DatabaseRequestQueueConflictResolutionCallback} [conflictResolutionCallback] + * @return {Promise} + */ + async push (request, conflictResolutionCallback = null) { + const promises = this.#promises + const queue = this.#queue + + this.#queue.push(request) + + const promise = new Promise((resolve, reject) => { + if (typeof conflictResolutionCallback === 'function') { + request.addEventListener('upgradeneeded', onupgradeneeded, { once: true }) + request.addEventListener('blocked', onblocked, { once: true }) + } + + request.addEventListener('complete', oncomplete, { once: true }) + request.addEventListener('success', onsuccess, { once: true }) + request.addEventListener('error', onerror, { once: true }) + request.addEventListener('abort', onabort, { once: true }) + + function cleanup () { + const queueIndex = queue.indexOf(request) + const promiseIndex = promises.indexOf(promise) + + if (queueIndex >= 0) { + queue.splice(queueIndex, 1) + } + + if (promiseIndex >= 0) { + promises.splice(promiseIndex, 1) + } + + request.removeEventListener('upgradeneeded', onupgradeneeded, { once: true }) + request.removeEventListener('blocked', onblocked, { once: true }) + + request.removeEventListener('complete', oncomplete, { once: true }) + request.removeEventListener('success', onsuccess, { once: true }) + request.removeEventListener('error', onerror, { once: true }) + request.removeEventListener('abort', onabort, { once: true }) + } + + function oncomplete () { + cleanup() + resolve(request.result ?? request) + } + + function onsuccess () { + cleanup() + resolve(request.result ?? null) + } + + function onerror (event) { + const message = 'Database: The request failed. A cause is attached' + const error = new Error(message, { cause: request.error ?? event.target }) + + error.request = request + + if (request.source) { + error.source = request.source + } + + if (request instanceof IDBTransaction) { + error.transaction = request + } else { + error.transaction = request.transaction + } + + cleanup() + reject(error) + } + + function onabort () { + const message = 'Database: The request was aborted' + const error = new Error(message) + + error.request = request + + if (request.source) { + error.source = request.source + } + + if (request instanceof IDBTransaction) { + error.transaction = request + } + + cleanup() + reject(error) + } + + function onupgradeneeded (event) { + const conflict = new DatabaseRequestQueueRequestConflict(resolve, reject, cleanup) + conflictResolutionCallback(event, conflict) + } + + function onblocked (event) { + const conflict = new DatabaseRequestQueueRequestConflict(resolve, reject, cleanup) + conflictResolutionCallback(event, conflict) + } + }) + + this.#promises.push(promise) + promise.then(() => { + const event = new DatabaseRequestQueueEvent('resolve', request) + this.dispatchEvent(event) + }) + + promise.catch((error) => { + const event = new DatabaseRequestQueueErrorEvent('error', request, { + error + }) + + this.dispatchEvent(event) + }) + + // await in tail for call stack inclusion + return await promise + } + + /** + * Waits for all pending requests to complete. This function will throw when + * an `IDBRequest` or `IDBTransaction` instance emits an 'error' event. + * Callers of this function can optionally specify a maximum backlog to wait + * for instead of waiting for all requests to finish. + * @param {?DatabaseRequestQueueWaitOptions | undefined} [options] + */ + async wait (options = null) { + if (this.#queue.length === 0) { + return + } + + const backlog = Number.isFinite(options?.backlog) + ? Math.min(options.backlog, this.#queue.length) + : this.#queue.length + + const offset = Number.isFinite(options?.offset) + ? Math.min(options.offset, this.#queue.length - 1) + : 0 + + const pending = [] + + for (let i = offset; i < backlog; ++i) { + const promise = this.promises[i] + if (promise instanceof Promise) { + pending.push(promise) + } + } + + return await Promise.all(pending) + } +} + +/** + * An interface for reading from named databases backed by IndexedDB. + */ +export class Database extends EventTarget { + /** + * A reference to an opened `IDBDatabase` from an `IDBOpenDBRequest` + * @type {?IDBDatabase} + */ + #storage = null + + /** + * An optional version of the database. + * @type {?string} + */ + #version = null + + /** + * The name of the database + * @param {?string} + */ + #name = null + + /** + * An internal queue of `Database` pending `IDBRequest` operations. + * @type {DatabaseRequestQueue} + */ + #queue = new DatabaseRequestQueue() + + /** + * This value is `true` if a pending `IDBOpenDBRequest` is present in the + * request queue. + * @type {boolean} + */ + #opening = false + + /** + * This value is `true` if the `Database` instance `close()` function was called, + * but the 'close' event has not been dispatched. + * @type {boolean} + */ + #closing = false + + /** + * `Database` class constructor. + * @param {string} name + * @param {?DatabaseOptions | undefined} [options] + */ + constructor (name, options = null) { + if (!name || typeof name !== 'string') { + throw new TypeError( + `Database: Expecting 'name' to be a string. Received: ${name}` + ) + } + + super() + + this.#name = name + + if (options?.version && typeof options?.version === 'string') { + this.#version = options.version + } + } + + /** + * `true` if the `Database` is currently opening, otherwise `false`. + * A `Database` instance should not attempt to be opened if this property value + * is `true`. + * @type {boolean} + */ + get opening () { + return this.#opening + } + + /** + * `true` if the `Database` instance was successfully opened such that the + * internal `IDBDatabase` storage instance was created and can be referenced + * on the `Database` instance, otherwise `false`. + * @type {boolean} + */ + get opened () { + return this.#storage !== null + } + + /** + * `true` if the `Database` instance was closed or has not been opened such + * that the internal `IDBDatabase` storage instance was not created or cannot + * be referenced on the `Database` instance, otherwise `false`. + * @type {boolean} + */ + get closed () { + return !this.closing && this.#storage === null + } + + /** + * `true` if the `Database` is currently closing, otherwise `false`. + * A `Database` instance should not attempt to be closed if this property value + * is `true`. + * @type {boolean} + */ + get closing () { + return this.#closing + } + + /** + * The name of the `IDBDatabase` database. This value cannot be `null`. + * @type {string} + */ + get name () { + return this.#name + } + + /** + * The version of the `IDBDatabase` database. This value may be `null`. + * @type {?string} + */ + get version () { + return this.#version + } + + /** + * A reference to the `IDBDatabase`, if the `Database` instance was opened. + * This value may ba `null`. + * @type {?IDBDatabase} + */ + get storage () { + return this.#storage + } + + /** + * Opens the `IDBDatabase` database optionally at a specific "version" if + * one was given upon construction of the `Database` instance. This function + * is not idempotent and will throw if the underlying `IDBDatabase` instance + * was created successfully or is in the process of opening. + * @return {Promise} + */ + async open () { + const queue = this.#queue + + if (this.opened) { + throw new Error('Database: storage is already opened') + } + + if (this.opening) { + throw new Error('Database: storage is already opening') + } + + this.#opening = true + const request = this.#version !== null + ? globalThis.indexedDB.open(this.name, this.#version) + : globalThis.indexedDB.open(this.name) + + this.#storage = await queue.push(request, async (conflictEvent, conflict) => { + if (conflictEvent.type === 'blocked') { + // TODO(@jwerle): handle a 'blocked' event + conflict.reject(new Error('Database: storage cannot be opened (blocked)')) + } else if (conflictEvent.type === 'upgradeneeded') { + const storage = conflictEvent.target.result + const store = storage.createObjectStore(this.name, { keyPath: 'key' }) + await queue.push(store.transaction) + conflict.resolve(storage) + } else { + conflict.reject(new Error('Database: storage cannot be opened (unknown reason)')) + } + }) + + this.#opening = false + gc.ref(this) + + queueMicrotask(() => { + this.dispatchEvent(new DatabaseEvent('open', this)) + }) + } + + /** + * Closes the `IDBDatabase` database storage, if opened. This function is not + * idempotent and will throw if the underlying `IDBDatabase` instance is + * already closed (not opened) or currently closing. + * @return {Promise} + */ + async close () { + if (!this.opened) { + throw new Error('Database: storage is not opened') + } + + if (this.closing) { + throw new Error('Database: storage is already closing') + } + + const storage = this.#storage + this.#closing = true + this.#storage = null + await storage.close() + gc.unref(this) + this.#closing = false + this.dispatchEvent(new DatabaseEvent('close', this)) + } + + /** + * Deletes entire `Database` instance and closes after successfully + * delete storage. + */ + async drop () { + if (this.opening) { + throw new Error('Database: storage is still opening') + } + + if (this.opened) { + await this.close() + } + + await this.#queue.push(globalThis.indexedDB.deleteDatabase(this.name)) + } + + /** + * Gets a "readonly" value by `key` in the `Database` object storage. + * @param {string} key + * @param {?DatabaseGetOptions|undefined} [options] + * @return {Promise} + */ + async get (key, options = null) { + if (!this.opened) { + throw new Error('Database: storage is not opened') + } + + if (this.closing) { + throw new Error('Database: storage is already closing') + } + + if (this.opening) { + await waitFor(this, 'open') + } + + const stores = [] + const pending = [] + const transaction = this.#storage.transaction( + options?.store ?? options?.stores ?? this.name, + 'readonly' + ) + + if (options?.store) { + stores.push(transaction.objectStore(options.store)) + } else if (options?.stores) { + for (const store of options.stores) { + stores.push(transaction.objectStore(store)) + } + } else { + stores.push(transaction.objectStore(this.name)) + } + + const count = Number.isFinite(options?.count) && options.count > 0 + ? options.count + : 1 + + for (const store of stores) { + if (count > 1) { + pending.push(this.#queue.push(store.getAll(key, count))) + } else { + pending.push(this.#queue.push(store.get(key))) + } + } + + await this.#queue.push(transaction) + const results = await Promise.all(pending) + + if (count === 1 || options?.store) { + const [result] = results + return result?.value ?? null + } + + return results + .map(function map (result) { + return Array.isArray(result) ? result.flatMap(map) : result + }) + .reduce((a, b) => a.concat(b), []) + .filter((value) => value) + .map((entry) => { + return [entry.key, entry.value] + }) + } + + /** + * Put a `value` at `key`, updating if it already exists, otherwise + * "inserting" it into the `Database` instance. + * @param {string} key + * @param {any} value + * @param {?DatabasePutOptions|undefined} [options] + * @return {Promise} + */ + async put (key, value, options = null) { + if (!this.opened) { + throw new Error('Database: storage is not opened') + } + + if (this.closing) { + throw new Error('Database: storage is already closing') + } + + if (this.opening) { + await waitFor(this, 'open') + } + + const stores = [] + const pending = [] + const transaction = this.#storage.transaction( + options?.store ?? options?.stores ?? this.name, + 'readwrite', + { durability: options?.durability ?? 'strict' } + ) + + if (options?.store) { + stores.push(transaction.objectStore(options.store)) + } else if (options?.stores) { + for (const store of options.stores) { + stores.push(transaction.objectStore(store)) + } + } else { + stores.push(transaction.objectStore(this.name)) + } + + for (const store of stores) { + pending.push(this.#queue.push(store.put({ key, value }))) + } + + transaction.commit() + await this.#queue.push(transaction) + await Promise.all(pending) + } + + /** + * Inserts a new `value` at `key`. This function throws if a value at `key` + * already exists. + * @param {string} key + * @param {any} value + * @param {?DatabasePutOptions|undefined} [options] + * @return {Promise} + */ + async insert (key, value, options = null) { + if (!this.opened) { + throw new Error('Database: storage is not opened') + } + + if (this.closing) { + throw new Error('Database: storage is already closing') + } + + if (this.opening) { + await waitFor(this, 'open') + } + + const stores = [] + const pending = [] + const transaction = this.#storage.transaction( + options?.store ?? options?.stores ?? this.name, + 'readwrite', + { durability: options?.durability ?? 'strict' } + ) + + if (options?.store) { + stores.push(transaction.objectStore(options.store)) + } else if (options?.stores) { + for (const store of options.stores) { + stores.push(transaction.objectStore(store)) + } + } else { + stores.push(transaction.objectStore(this.name)) + } + + for (const store of stores) { + pending.push(this.#queue.push(store.add(value, key))) + } + + transaction.commit() + await this.#queue.push(transaction) + await Promise.all(pending) + } + + /** + * Update a `value` at `key`, updating if it already exists, otherwise + * "inserting" it into the `Database` instance. + * @param {string} key + * @param {any} value + * @param {?DatabasePutOptions|undefined} [options] + * @return {Promise} + */ + async update (key, value, options = null) { + if (!this.opened) { + throw new Error('Database: storage is not opened') + } + + if (this.closing) { + throw new Error('Database: storage is already closing') + } + + if (this.opening) { + await waitFor(this, 'open') + } + + const stores = [] + const pending = [] + const transaction = this.#storage.transaction( + options?.store ?? options?.stores ?? this.name, + 'readwrite', + { durability: 'strict' } + ) + + if (options?.store) { + stores.push(transaction.objectStore(options.store)) + } else if (options?.stores) { + for (const store of options.stores) { + stores.push(transaction.objectStore(store)) + } + } else { + stores.push(transaction.objectStore(this.name)) + } + + for (const store of stores) { + let existing = null + try { + existing = await this.#queue.push(store.get(key)) + } catch (err) { + globalThis.reportError(err) + } + + if (existing) { + pending.push(this.#queue.push(store.put({ + key, + value: { ...(existing.value ?? null), ...value } + }))) + } else { + pending.push(this.#queue.push(store.put({ key, value }))) + } + } + + transaction.commit() + await this.#queue.push(transaction) + await Promise.all(pending) + } + + /** + * Delete a value at `key`. + * @param {string} key + * @param {?DatabaseDeleteOptions|undefined} [options] + * @return {Promise} + */ + async delete (key, options = null) { + if (!this.opened) { + throw new Error('Database: storage is not opened') + } + + if (this.closing) { + throw new Error('Database: storage is already closing') + } + + if (this.opening) { + await waitFor(this, 'open') + } + + const stores = [] + const pending = [] + const transaction = this.#storage.transaction( + options?.store ?? options?.stores ?? this.name, + 'readwrite', + { durability: 'strict' } + ) + + if (options?.store) { + stores.push(transaction.objectStore(options.store)) + } else if (options?.stores) { + for (const store of options.stores) { + stores.push(transaction.objectStore(store)) + } + } else { + stores.push(transaction.objectStore(this.name)) + } + + for (const store of stores) { + pending.push(this.#queue.push(store.delete(key))) + } + + transaction.commit() + await this.#queue.push(transaction) + await Promise.all(pending) + } + + /** + * Gets a "readonly" value by `key` in the `Database` object storage. + * @param {string} key + * @param {?DatabaseEntriesOptions|undefined} [options] + * @return {Promise} + */ + async entries (options = null) { + if (!this.opened) { + throw new Error('Database: storage is not opened') + } + + if (this.closing) { + throw new Error('Database: storage is already closing') + } + + if (this.opening) { + await waitFor(this, 'open') + } + + const stores = [] + const pending = [] + const transaction = this.#storage.transaction( + options?.store ?? options?.stores ?? this.name, + 'readonly' + ) + + if (options?.store) { + stores.push(transaction.objectStore(options.store)) + } else if (options?.stores) { + for (const store of options.stores) { + stores.push(transaction.objectStore(store)) + } + } else { + stores.push(transaction.objectStore(this.name)) + } + + for (const store of stores) { + const request = store.openCursor() + let cursor = await this.#queue.push(request) + + while (cursor) { + if (!cursor.value) { + break + } + pending.push(Promise.resolve(cursor.value)) + cursor.continue() + cursor = await this.#queue.push(request) + } + } + + await this.#queue.push(transaction) + const results = await Promise.all(pending) + + return results + .filter((value) => value) + .map((entry) => [entry.key, entry.value]) + } + + /** + * Implements `gc.finalizer` for gc'd resource cleanup. + * @return {gc.Finalizer} + */ + [gc.finalizer] () { + return { + args: [this.#name, this.#storage], + handle (name, storage) { + opened.delete(name) + storage.close() + } + } + } +} + +/** + * Creates an opens a named `Database` instance. + * @param {string} name + * @param {?DatabaseOptions | undefined} [options] + * @return {Promise} + */ +export async function open (name, options) { + const ref = opened.get(name) + + // return already opened instance if still referenced somehow + if (ref && ref.deref()) { + const database = ref.deref() + if (database.opened) { + return database + } + + return new Promise((resolve) => { + database.addEventListener('open', () => { + resolve(database) + }, { once: true }) + }) + } + + const database = new Database(name, options) + + opened.set(name, new WeakRef(database)) + + database.addEventListener('close', () => { + opened.delete(name) + }, { once: true }) + + try { + await database.open() + } catch (err) { + opened.delete(name) + throw err + } + + return database +} + +/** + * Complete deletes a named `Database` instance. + * @param {string} name + * @param {?DatabaseOptions|undefined} [options] + */ +export async function drop (name, options) { + const ref = opened.get(name) + const database = ref?.deref?.() ?? new Database(name, options) + await database.drop() + opened.delete(name) +} + +export default { + Database, + open, + drop +} diff --git a/api/internal/error.js b/api/internal/error.js new file mode 100644 index 0000000000..107b0f6f77 --- /dev/null +++ b/api/internal/error.js @@ -0,0 +1,241 @@ +import { CallSite, createCallSites } from './callsite.js' +import os from '../os.js' + +/** + * The default `Error` class stack trace limit. + * @type {number} + */ +export const DEFAULT_ERROR_STACK_TRACE_LIMIT = 10 + +export const DefaultPlatformError = globalThis.Error + +/** + * @ignore + * @param {typeof globalThis.Error} PlatformError + * @param {Error} target + * @param {...any} args + * @return {Error} + */ +function applyPlatforErrorHook (PlatformError, Constructor, target, ...args) { + const error = PlatformError.call(target, ...args) + const stack = error.stack.split('\n') + + // slice off the `Error()` + `applyPlatforErrorHook()` call frames + if (os.platform() === 'android') { + stack.splice(1, 2) + } else { + stack.splice(0, 2) + } + + const [, callsite] = stack[0].split('@') + + let stackValue = stack.join('\n') + let sourceStack = stackValue + + Object.defineProperties(error, { + [CallSite.StackSourceSymbol]: { + configurable: false, + enumerable: false, + get: () => sourceStack + }, + + column: { + configurable: true, + enumerable: false, + writable: true + }, + + line: { + configurable: true, + enumerable: false, + writable: true + }, + + message: { + configurable: true, + enumerable: false, + writable: true, + value: error.message + }, + + name: { + configurable: true, + enumerable: false, + writable: true, + value: target.constructor.name + }, + + sourceURL: { + configurable: true, + enumerable: false, + writable: true + }, + + stack: { + configurable: true, + enumerable: false, + set: (stack) => { + stackValue = stack + if (stackValue && typeof stackValue === 'string') { + sourceStack = stackValue + } + }, + get: () => { + Object.defineProperty(error, 'stack', { + configurable: true, + enumerable: false, + writable: true, + value: stackValue + }) + + if (Error.stackTraceLimit === 0) { + stackValue = `${error.name}: ${error.message}` + } else { + const prepareStackTrace = Constructor.prepareStackTrace || globalThis.Error.prepareStackTrace + if (typeof prepareStackTrace === 'function') { + const callsites = createCallSites(error, stackValue) + stackValue = prepareStackTrace(error, callsites) + } + } + + Object.defineProperty(error, 'stack', { + configurable: true, + enumerable: false, + writable: true, + value: stackValue + }) + + return stackValue + } + } + }) + + if (URL.canParse(callsite)) { + const url = new URL(callsite) + const parts = url.pathname.split(':') + const line = parseInt(parts[1]) + const column = parseInt(parts[2]) + + error.sourceURL = new URL(parts[0] + url.search, url.origin).href + + if (Number.isFinite(line)) { + error.line = line + } + + if (Number.isFinite(column)) { + error.column = column + } + } else { + delete error.sourceURL + delete error.column + delete error.line + } + + Object.setPrototypeOf(error, target) + + return error +} + +/** + * Creates and install a new runtime `Error` class + * @param {typeof globalThis.Error} PlatformError + * @param {boolean=} [isBaseError] + * @return {typeof globalThis.Error} + */ +function installRuntimeError (PlatformError, isBaseError = false) { + if (!PlatformError) { + PlatformError = DefaultPlatformError + } + + function Error (...args) { + if (!(this instanceof Error)) { + return new Error(...args) + } + + return applyPlatforErrorHook(PlatformError, Error, this, ...args) + } + + // directly inherit platform `Error` prototype + Error.prototype = PlatformError.prototype + + /** + * @ignore + */ + Error.stackTraceLimit = DEFAULT_ERROR_STACK_TRACE_LIMIT + + Object.defineProperty(Error, 'captureStackTrace', { + configurable: true, + enumerable: false, + // eslint-disable-next-line + value (target) { + if (!target || typeof target !== 'object') { + throw new TypeError( + `Invalid target given to ${PlatformError.name}.captureStackTrace. ` + + `Expecting 'target' to be an object. Received: ${target}` + ) + } + + const stack = new PlatformError().stack.split('\n') + if (os.platform() === 'android') { + stack.splice(1, 2) + } else { + stack.splice(0, 2) + } + // prepareStackTrace is already called there + if (target instanceof Error) { + target.stack = stack.join('\n') + } else { + const prepareStackTrace = Error.prepareStackTrace || globalThis.Error.prepareStackTrace + if (typeof prepareStackTrace === 'function') { + const callsites = createCallSites(target, stack.join('\n')) + target.stack = prepareStackTrace(target, callsites) + } else { + target.stack = stack.join('\n') + } + } + } + }) + + // Install + globalThis[PlatformError.name] = Error + + return Error +} + +export const Error = installRuntimeError(globalThis.Error, true) +export const URIError = installRuntimeError(globalThis.URIError) +export const EvalError = installRuntimeError(globalThis.EvalError) +export const TypeError = installRuntimeError(globalThis.TypeError) +export const RangeError = installRuntimeError(globalThis.RangeError) +export const MediaError = installRuntimeError(globalThis.MediaError) +export const SyntaxError = installRuntimeError(globalThis.SyntaxError) +export const ReferenceError = installRuntimeError(globalThis.ReferenceError) +export const AggregateError = installRuntimeError(globalThis.AggregateError) + +// web +export const RTCError = installRuntimeError(globalThis.RTCError) +export const OverconstrainedError = installRuntimeError(globalThis.OverconstrainedError) +export const GeolocationPositionError = installRuntimeError(globalThis.GeolocationPositionError) + +// not-standard +export const ApplePayError = installRuntimeError(globalThis.ApplePayError) + +export default { + Error, + URIError, + EvalError, + TypeError, + RangeError, + MediaError, + SyntaxError, + ReferenceError, + AggregateError, + + // web + RTCError, + OverconstrainedError, + GeolocationPositionError, + + // non-standard + ApplePayError +} diff --git a/api/internal/events.js b/api/internal/events.js index 1666106bf9..33e6a1af3d 100644 --- a/api/internal/events.js +++ b/api/internal/events.js @@ -1,4 +1,4 @@ -/* global MessageEvent */ +/* global Event, MessageEvent */ /** * An event dispatched when an application URL is opening the application. @@ -188,8 +188,54 @@ export class MenuItemEvent extends MessageEvent { } } +/** + * An event dispacted when the application receives an OS signal + */ +export class SignalEvent extends MessageEvent { + #code = 0 + #name = '' + #message = '' + + /** + * `SignalEvent` class constructor + * @ignore + * @param {string=} [type] + * @param {object=} [options] + */ + constructor (type, options) { + super(type, options) + this.#code = options?.code ?? 0 + this.#name = type + this.#message = options?.message ?? '' + } + + /** + * The code of the signal. + * @type {import('../process/signal.js').signal} + */ + get code () { + return this.#code ?? 0 + } + + /** + * The name of the signal. + * @type {string} + */ + get name () { + return this.#name + } + + /** + * An optional message describing the signal + * @type {string} + */ + get message () { + return this.#message ?? '' + } +} export default { ApplicationURLEvent, MenuItemEvent, + SignalEvent, HotKeyEvent } diff --git a/api/internal/geolocation.js b/api/internal/geolocation.js index 9826985248..7c247cba8c 100644 --- a/api/internal/geolocation.js +++ b/api/internal/geolocation.js @@ -140,7 +140,7 @@ export async function getCurrentPosition ( if (isAndroid) { await new Promise((resolve) => hooks.onReady(resolve)) - const result = await ipc.send('permissions.request', { name: 'geolocation' }) + const result = await ipc.request('permissions.request', { name: 'geolocation' }) if (result.err) { if (typeof onError === 'function') { @@ -187,7 +187,7 @@ export async function getCurrentPosition ( }, options.timeout) } - const result = await ipc.send('geolocation.getCurrentPosition') + const result = await ipc.request('geolocation.getCurrentPosition') if (didTimeout) { return @@ -276,7 +276,7 @@ export function watchPosition ( }, options.timeout) } - ipc.send('geolocation.watchPosition', { id: identifier }).then((result) => { + ipc.request('geolocation.watchPosition', { id: identifier }).then((result) => { if (result.err) { if (typeof onError === 'function') { onError(result.err) @@ -317,7 +317,7 @@ export function clearWatch (id) { watchers[id]?.stop() delete watchers[id] - ipc.send('geolocation.clearWatch', { id }) + ipc.request('geolocation.clearWatch', { id }) } export default { diff --git a/api/internal/globals.js b/api/internal/globals.js index 570184fb07..ccaecd8a1f 100644 --- a/api/internal/globals.js +++ b/api/internal/globals.js @@ -1,3 +1,5 @@ +import Promise from './promise.js' + /** * Symbolic global registry * @ignore @@ -22,11 +24,15 @@ export class GlobalsRegistry { } const registry = ( - globalThis.__globals ?? globalThis.top?.__globals ?? + globalThis.__globals ?? new GlobalsRegistry() ) +const RuntimeReadyPromiseResolvers = Promise.withResolvers() +registry.register('RuntimeReadyPromiseResolvers', RuntimeReadyPromiseResolvers) +registry.register('RuntimeReadyPromise', RuntimeReadyPromiseResolvers.promise) + /** * Gets a runtime global value by name. * @ignore diff --git a/api/internal/init.js b/api/internal/init.js index d6358d031a..ff8ca8e5e2 100644 --- a/api/internal/init.js +++ b/api/internal/init.js @@ -1,4 +1,4 @@ -/* global ArrayBuffer, Blob, DataTransfer, DragEvent, FileList */ +/* global XMLHttpRequest, requestAnimationFrame, Blob, DataTransfer, DragEvent, FileList, MessageEvent, reportError */ /* eslint-disable import/first */ // mark when runtime did init console.assert( @@ -12,13 +12,21 @@ console.assert( 'This could lead to undefined behavior.' ) -import './monkeypatch.js' +import './primitives.js' +import ipc from '../ipc.js' +ipc.sendSync('platform.event', 'beforeruntimeinit') -import { IllegalConstructor, InvertedPromise } from '../util.js' import { CustomEvent, ErrorEvent } from '../events.js' +import { IllegalConstructor } from '../util.js' +import { URL, protocols } from '../url.js' +import * as asyncHooks from './async/hooks.js' +import { Deferred } from '../async.js' import { rand64 } from '../crypto.js' import location from '../location.js' -import { URL } from '../url.js' +import * as sw from '../shared-worker/index.js' +import mime from '../mime.js' +import path from '../path.js' +import os from '../os.js' import fs from '../fs/promises.js' import { createFileSystemDirectoryHandle, @@ -27,6 +35,9 @@ import { const GlobalWorker = globalThis.Worker || class Worker extends EventTarget {} +// init `SharedWorker` window context +sw.getContextWindow() + // only patch a webview or worker context if ((globalThis.window || globalThis.self) === globalThis) { if (typeof globalThis.queueMicrotask === 'function') { @@ -52,11 +63,11 @@ if ((globalThis.window || globalThis.self) === globalThis) { ) } - return originalQueueMicrotask(task) + originalQueueMicrotask(task) function task () { try { - return callback.call(globalThis) + return asyncHooks.wrap(callback, 'Microtask').call(globalThis) } catch (error) { // XXX(@jwerle): `queueMicrotask()` is broken in WebKit WebViews // If an error is thrown, it does not bubble to the `globalThis` @@ -91,11 +102,33 @@ if ((globalThis.window) === globalThis) { if (Array.isArray(event.detail?.files)) { for (const file of event.detail.files) { if (typeof file === 'string') { - const stats = await fs.stat(file) - if (stats.isDirectory()) { - handles.push(await createFileSystemDirectoryHandle(file, { writable: false })) - } else { - handles.push(await createFileSystemFileHandle(file, { writable: false })) + try { + const stats = await fs.stat(file) + if (stats.isDirectory()) { + handles.push(await createFileSystemDirectoryHandle(file, { writable: false })) + } else { + handles.push(await createFileSystemFileHandle(file, { writable: false })) + } + } catch (err) { + try { + // try to read from navigator + const response = await fetch(file) + if (response.ok) { + const lastModified = Date.now() + const buffer = new Uint8Array(await response.arrayBuffer()) + const types = await mime.lookup(path.extname(file).slice(1)) + const type = types[0]?.mime ?? '' + + handles.push(await createFileSystemFileHandle( + new File(buffer, { lastModified, type }), + { writable: false } + )) + } else { + console.warn('platformdrop: ', err) + } + } catch (err) { + console.warn('platformdrop: ', err) + } } } } @@ -181,43 +214,200 @@ if ((globalThis.window) === globalThis) { })) } }) + + // TODO: move this somewhere more appropriate + if (os.platform() === 'ios') { + const timing = { + duration: 346 // TODO: document this + } + + const keyboard = { + opened: false, + offset: 0, + height: 0 + } + + const bezier = { + show (t) { + const p1 = 0.9 + const p2 = 0.95 + return 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t + }, + + hide (t) { + const p1 = 0.86 + const p2 = 0.95 + return 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t + } + } + + globalThis.addEventListener('keyboard', function (event) { + const { detail } = event + + keyboard.height = detail.value.height + + if (keyboard.offset === 0) { + keyboard.offset = document.body.offsetHeight + } + + if (detail.value.event === 'will-show' && !keyboard.opened) { + let start = null + + requestAnimationFrame(function animate (timestamp) { + if (!start) start = timestamp + const elapsed = timestamp - start + const progress = Math.min(elapsed / timing.duration, 1) + const easeProgress = bezier.show(progress) + const currentHeight = keyboard.offset - (easeProgress * keyboard.height) + + globalThis.document.body.style.height = `${currentHeight}px` + + if (progress < 1) { + keyboard.opened = true + requestAnimationFrame(animate) + } + }) + } + + if (detail.value.event === 'will-hide' && keyboard.opened) { + keyboard.opened = false + const { offsetHeight } = globalThis.document.body + + let start = null + + requestAnimationFrame(function animate (timestamp) { + if (!start) start = timestamp + const elapsed = timestamp - start + let progress = Math.min(elapsed / timing.duration, 1) + const easeProgress = bezier.hide(progress) + const currentHeight = offsetHeight + (easeProgress * keyboard.height) + if (currentHeight <= 0) progress = 1 + + globalThis.document.body.style.height = `${currentHeight}px` + + if (progress < 1) { + requestAnimationFrame(animate) + } else { + keyboard.opened = false + } + }) + } + }) + } } class RuntimeWorker extends GlobalWorker { - #onglobaldata = null - #id = rand64() - - static pool = new Map() + /** + * Internal worker pool + * @ignore + */ + static pool = globalThis.top?.Worker?.pool ?? new Map() + /** + * Handles `Symbol.species` + * @ignore + */ static get [Symbol.species] () { return GlobalWorker } + #id = null + #objectURL = null + #onglobaldata = null + /** * `RuntimeWorker` class worker. * @ignore + * @param {string|URL} filename + * @param {object=} [options] */ constructor (filename, options, ...args) { - const url = encodeURIComponent(new URL(filename, globalThis.location.href || '/').toString()) - const id = rand64() + options = { ...options } + + if (typeof filename === 'string' && !URL.canParse(filename, location.href)) { + const blob = new Blob([filename], { type: 'text/javascript' }) + filename = URL.createObjectURL(blob).toString() + } else if (String(filename).startsWith('blob')) { + const request = new XMLHttpRequest() + request.open('GET', String(filename), false) + request.send() + + const blob = new Blob([request.responseText || request.response], { + type: 'text/javascript' + }) + + filename = URL + .createObjectURL(blob) + .toString() + } + + const workerType = options[Symbol.for('socket.runtime.internal.worker.type')] ?? 'worker' + const url = encodeURIComponent(new URL(filename, location.href).toString()) + const id = String(rand64()) + + const topClient = globalThis.__args.client.top || globalThis.__args.client + const __args = { ...globalThis.__args, client: {} } const preload = ` Object.defineProperty(globalThis, '__args', { configurable: false, enumerable: false, - value: ${JSON.stringify(globalThis.__args)} + value: ${JSON.stringify(__args)} + }) + + globalThis.__args.client.id = '${id}' + globalThis.__args.client.type = 'worker' + globalThis.__args.client.frameType = 'none' + globalThis.__args.client.parent = ${JSON.stringify({ + id: globalThis.__args?.client?.id, + top: null, + type: globalThis.__args?.client?.type, + parent: null, + frameType: globalThis.__args?.client?.frameType + })} + globalThis.__args.client.top = ${JSON.stringify({ + id: topClient?.id, + top: null, + type: topClient?.type, + parent: null, + frameType: topClient?.frameType + })} + + globalThis.__args.client.parent.top = globalThis.__args.client.top + + Object.defineProperty(globalThis, 'isWorkerScope', { + configurable: false, + enumerable: false, + writable: false, + value: true + }) + + Object.defineProperty(globalThis, 'isSocketRuntime', { + configurable: false, + enumerable: false, + writable: false, + value: true }) Object.defineProperty(globalThis, 'RUNTIME_WORKER_ID', { configurable: false, enumerable: false, + writable: false, value: '${id}' }) + Object.defineProperty(globalThis, 'RUNTIME_WORKER_TYPE', { + configurable: false, + enumerable: false, + writable: false, + value: '${workerType}' + }) + Object.defineProperty(globalThis, 'RUNTIME_WORKER_LOCATION', { configurable: false, enumerable: false, - value: '${url}' + writable: true, + value: decodeURIComponent('${url}') }) Object.defineProperty(globalThis, 'RUNTIME_WORKER_MESSAGE_EVENT_BACKLOG', { @@ -232,12 +422,18 @@ class RuntimeWorker extends GlobalWorker { RUNTIME_WORKER_MESSAGE_EVENT_BACKLOG.push(event) } - import '${globalThis.location.protocol}//${globalThis.location.hostname}/socket/internal/worker.js?source=${url}' - import hooks from 'socket:hooks' + try { + await import('${location.origin}/socket/internal/init.js') + const hooks = await import('${location.origin}/socket/hooks.js') - hooks.onReady(() => { - globalThis.removeEventListener('message', onInitialWorkerMessages) - }) + hooks.onReady(() => { + globalThis.removeEventListener('message', onInitialWorkerMessages) + }) + + await import('${location.origin}/socket/internal/worker.js?source=${url}') + } catch (err) { + globalThis.reportError(err) + } `.trim() const objectURL = URL.createObjectURL( @@ -255,6 +451,7 @@ class RuntimeWorker extends GlobalWorker { RuntimeWorker.pool.set(id, new WeakRef(this)) this.#id = id + this.#objectURL = objectURL this.#onglobaldata = (event) => { const data = new Uint8Array(event.detail.data).buffer @@ -271,9 +468,21 @@ class RuntimeWorker extends GlobalWorker { globalThis.addEventListener('data', this.#onglobaldata) + const postMessage = this.postMessage.bind(this) const addEventListener = this.addEventListener.bind(this) const removeEventListener = this.removeEventListener.bind(this) + const postMessageQueue = [] + let isReady = false + + this.postMessage = (...args) => { + if (!isReady) { + postMessageQueue.push(args) + } else { + return postMessage(...args) + } + } + this.addEventListener = (eventName, ...args) => { if (eventName === 'message') { return eventTarget.addEventListener(eventName, ...args) @@ -305,41 +514,54 @@ class RuntimeWorker extends GlobalWorker { } }) - this.addEventListener('message', (event) => { - const { data } = event - if (data?.__runtime_worker_ipc_request) { - const request = data.__runtime_worker_ipc_request - if ( - typeof request?.message === 'string' && - request.message.startsWith('ipc://') - ) { - queueMicrotask(async () => { - try { - // eslint-disable-next-line no-use-before-define - const message = ipc.Message.from(request.message, request.bytes) - const options = { bytes: message.bytes } - // eslint-disable-next-line no-use-before-define - const result = await ipc.request(message.name, message.rawParams, options) - const transfer = [] - - if (ArrayBuffer.isView(result.data) || result.data instanceof ArrayBuffer) { - transfer.push(result.data) - } + queueMicrotask(() => { + addEventListener('message', (event) => { + const { data } = event + if (data?.__runtime_worker_init === true) { + isReady = true + + for (const args of postMessageQueue) { + postMessage(...args) + } - this.postMessage({ - __runtime_worker_ipc_result: { - message: message.toJSON(), - result: result.toJSON() + postMessageQueue.splice(0, postMessageQueue.length) + } else if (data?.__runtime_worker_ipc_request) { + const request = data.__runtime_worker_ipc_request + if ( + typeof request?.message === 'string' && + request.message.startsWith('ipc://') + ) { + queueMicrotask(async () => { + try { + // eslint-disable-next-line no-use-before-define + const transfer = [] + const message = ipc.Message.from(request.message, request.bytes) + const options = { bytes: message.bytes } + const promise = ipc.send(message.name, message.rawParams, options) + + if (message.get('resolve') === false) { + return } - }, transfer) - } catch (err) { - console.warn('RuntimeWorker:', err) - } - }) + + const result = await promise + + ipc.findMessageTransfers(transfer, result) + + this.postMessage({ + __runtime_worker_ipc_result: { + message: message.toJSON(), + result: result.toJSON() + } + }, { transfer }) + } catch (err) { + globalThis.reportError(err) + } + }) + } + } else { + return eventTarget.dispatchEvent(new MessageEvent(event.type, event)) } - } else { - return eventTarget.dispatchEvent(event) - } + }) }) } @@ -347,6 +569,10 @@ class RuntimeWorker extends GlobalWorker { return this.#id } + get objectURL () { + return this.#objectURL + } + terminate () { globalThis.removeEventListener('data', this.#onglobaldata) return super.terminate() @@ -360,10 +586,54 @@ if (globalThis.Worker === GlobalWorker) { // patch `globalThis.XMLHttpRequest` if (typeof globalThis.XMLHttpRequest === 'function') { - const { open, send } = globalThis.XMLHttpRequest.prototype + const { open, send, setRequestHeader } = globalThis.XMLHttpRequest.prototype + const additionalHeaders = {} + const headerFilters = ['user-agent'] const isAsync = Symbol('isAsync') let queue = null + if ( + typeof globalThis.__args.config.webview_fetch_headers_filter === 'string' && + globalThis.__args.config.webview_fetch_headers_filter.length > 0 + ) { + const filters = globalThis.__args.config.webview_fetch_headers_filter.split(' ') + for (const filter of filters) { + headerFilters.push(new RegExp(filter.replace(/\*/g, '(.*)').replace(/\.\.\*/g, '.*'), 'i')) + } + } + + for (const key in globalThis.__args.config) { + if (key.startsWith('webview_fetch_headers_')) { + const name = key.replace('webview_fetch_headers_', '') + additionalHeaders[name] = globalThis.__args.config[name] + } + } + + globalThis.XMLHttpRequest.prototype.setRequestHeader = function (name, value) { + if (testHeaderFilters(name)) { + try { + setRequestHeader.call(this, name, value) + } catch {} + } + + function testHeaderFilters (header) { + if (header.toLowerCase().startsWith('sec-')) { + return false + } + + for (const filter of headerFilters) { + if (typeof filter === 'string') { + if (filter === name.toLowerCase()) { + return false + } + } else if (filter.test(header)) { + return false + } + } + return true + } + } + globalThis.XMLHttpRequest.prototype.open = function (method, url, isAsyncRequest, ...args) { Object.defineProperty(this, isAsync, { configurable: false, @@ -373,24 +643,48 @@ if (typeof globalThis.XMLHttpRequest === 'function') { }) if (typeof url === 'string') { - if (url.startsWith('/') || url.startsWith('.')) { - try { - url = new URL(url, globalThis.location.origin).toString() - } catch {} - } + try { + url = new URL(url, location.origin) + } catch {} } - const value = open.call(this, method, url, isAsyncRequest !== false, ...args) + const value = open.call(this, method, url.toString(), isAsyncRequest !== false, ...args) - if (typeof globalThis.RUNTIME_WORKER_ID === 'string') { - this.setRequestHeader('Runtime-Worker-ID', globalThis.RUNTIME_WORKER_ID) - } + if ( + method !== 'OPTIONS' && ( + globalThis.__args?.config?.webview_fetch_allow_runtime_headers === true || + (url.protocol && /(socket|ipc|node|npm):/.test(url.protocol)) || + (url.protocol && protocols.handlers.has(url.protocol.slice(0, -1))) || + url.hostname === globalThis.__args.config.meta_bundle_identifier + ) + ) { + for (const key in additionalHeaders) { + this.setRequestHeader(key, additionalHeaders[key]) + } - if (typeof globalThis.RUNTIME_WORKER_LOCATION === 'string') { - this.setRequestHeader('Runtime-Worker-Location', globalThis.RUNTIME_WORKER_LOCATION) - } + if (globalThis.__args?.client) { + this.setRequestHeader('Runtime-Client-ID', globalThis.__args.client.id) + } + + if (typeof globalThis.RUNTIME_WORKER_LOCATION === 'string') { + this.setRequestHeader('Runtime-Worker-Location', globalThis.RUNTIME_WORKER_LOCATION) + } - return value + if (globalThis.top && globalThis.top !== globalThis) { + this.setRequestHeader('Runtime-Frame-Type', 'nested') + } else if (!globalThis.window && globalThis.self === globalThis) { + this.setRequestHeader('Runtime-Frame-Type', 'worker') + if (globalThis.clients && globalThis.FetchEvent) { + this.setRequestHeader('Runtime-Worker-Type', 'serviceworker') + } else { + this.setRequestHeader('Runtime-Worker-Type', 'worker') + } + } else { + this.setRequestHeader('Runtime-Frame-Type', 'top-level') + } + + return value + } } globalThis.XMLHttpRequest.prototype.send = async function (...args) { @@ -425,11 +719,52 @@ if (typeof globalThis.XMLHttpRequest === 'function') { import hooks, { RuntimeInitEvent } from '../hooks.js' import { config } from '../application.js' import globals from './globals.js' -import ipc from '../ipc.js' - import '../console.js' -const isWorkerLike = !globalThis.window && globalThis.self && globalThis.postMessage +hooks.onApplicationResume((e) => { + for (const ref of RuntimeWorker.pool.values()) { + const worker = ref.deref() + if (worker) { + worker.postMessage({ + __runtime_worker_event: { + type: 'applicationresume' + } + }) + } + } +}) + +hooks.onApplicationPause((e) => { + for (const ref of RuntimeWorker.pool.values()) { + const worker = ref.deref() + if (worker) { + worker.postMessage({ + __runtime_worker_event: { + type: 'applicationpause' + } + }) + } + } +}) + +hooks.onApplicationURL((e) => { + for (const ref of RuntimeWorker.pool.values()) { + const worker = ref.deref() + if (worker) { + worker.postMessage({ + __runtime_worker_event: { + type: 'applicationurl', + detail: { data: e.data, url: String(e.url ?? '') } + } + }) + } + } +}) + +ipc.sendSync('platform.event', { + value: 'load', + 'location.href': globalThis.location.href +}) class ConcurrentQueue extends EventTarget { concurrency = Infinity @@ -501,7 +836,7 @@ class RuntimeXHRPostQueue extends ConcurrentQueue { } } - const promise = new InvertedPromise() + const promise = new Deferred() await this.push(promise, 8) if (typeof params !== 'object') { @@ -524,24 +859,122 @@ class RuntimeXHRPostQueue extends ConcurrentQueue { } } -hooks.onLoad(() => { +hooks.onLoad(async () => { + const registeredServiceWorkers = new Set() + const serviceWorkerScripts = config['webview_service-workers'] + const pending = [] + + if ( + globalThis.window && + !globalThis.__RUNTIME_SERVICE_WORKER_CONTEXT__ && + globalThis.location.pathname !== '/socket/service-worker/index.html' && + String(config.permissions_allow_service_worker) !== 'false' && + String(config.webview_auto_register_service_workers) !== 'false' + ) { + const pendingServiceRegistrations = [] + + if (typeof config['webview_service-workers'] === 'string') { + for (const scriptURL of serviceWorkerScripts.trim().split(' ')) { + pendingServiceRegistrations.push({ + scriptURL: scriptURL.trim(), + options: {} + }) + } + } + + for (const key in config) { + if (key.startsWith('webview_service-workers_')) { + const scope = key.replace('webview_service-workers_', '') + const scriptURL = config[key].trim() + pendingServiceRegistrations.push({ + scriptURL, + options: { scope } + }) + } + } + + for (const registration of pendingServiceRegistrations) { + const { options } = registration + let { scriptURL } = registration + + if (!scriptURL.startsWith('/') && scriptURL.startsWith('.')) { + if (!URL.canParse(scriptURL, globalThis.location.href)) { + scriptURL = `./${scriptURL}` + } + } + + const url = new URL(scriptURL, globalThis.location.origin) + const scope = options.scope ?? new URL('.', url).pathname + + if (!globalThis.location.pathname.startsWith(scope)) { + continue + } + + if (registeredServiceWorkers.has(scriptURL)) { + continue + } + + const promise = globalThis.navigator.serviceWorker.register(scriptURL, options) + registeredServiceWorkers.add(scriptURL) + + pending.push(promise) + + promise + .then((registration) => { + if (!registration) { + console.warn( + 'ServiceWorker failed to register in preload: %s', + scriptURL + ) + } + }) + .catch((err) => { + console.error( + 'ServiceWorker registration error occurred in preload: %s:', + scriptURL, + err + ) + }) + } + } + + await Promise.all(pending) if (typeof globalThis.dispatchEvent === 'function') { globalThis.__RUNTIME_INIT_NOW__ = performance.now() globalThis.dispatchEvent(new RuntimeInitEvent()) } + + if (globalThis.document) { + const beginRuntimePreload = globalThis.document.querySelector('meta[name=begin-runtime-preload]') + if (beginRuntimePreload) { + let current = beginRuntimePreload + while (current) { + const next = current.nextElementSibling + current.remove() + if (current.tagName === 'META' && current.name === 'end-runtime-preload') { + current = null + } else { + current = next + } + } + } + } }) // async preload modules hooks.onReady(async () => { try { - if (!isWorkerLike) { - // precache fs.constants - await ipc.request('fs.constants', {}, { cache: true }) - } - + // precache 'fs.constants' and 'os.constants' + await ipc.request('fs.constants', {}, { cache: true }) + await ipc.request('os.constants', {}, { cache: true }) await import('../diagnostics.js') + await import('../process/signal.js') await import('../fs/fds.js') - await import('../fs/constants.js') + await import('../constants.js') + const errors = await import('../errors.js') + // lazily install this + const errno = await import('../errno.js') + errors.ErrnoError.errno = errno } catch (err) { console.error(err.stack || err) } @@ -549,6 +982,8 @@ hooks.onReady(async () => { // symbolic globals globals.register('RuntimeXHRPostQueue', new RuntimeXHRPostQueue()) +globals.register('RuntimeExecution', new asyncHooks.CoreAsyncResource('RuntimeExecution')) + // prevent further construction if this class is indirectly referenced RuntimeXHRPostQueue.prototype.constructor = IllegalConstructor Object.defineProperty(globalThis, '__globals', { @@ -557,6 +992,11 @@ Object.defineProperty(globalThis, '__globals', { value: globals }) +ipc.send('platform.event', 'runtimeinit') + .then(() => { + globals.get('RuntimeReadyPromiseResolvers')?.resolve() + }, reportError) + export default { location } diff --git a/api/internal/iterator.js b/api/internal/iterator.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/internal/monkeypatch.js b/api/internal/monkeypatch.js deleted file mode 100644 index c8eb17b9f4..0000000000 --- a/api/internal/monkeypatch.js +++ /dev/null @@ -1,232 +0,0 @@ -/* global MutationObserver */ -import { fetch, Headers, Request, Response } from '../fetch.js' -import { URL, URLPattern, URLSearchParams } from '../url.js' -import SharedWorker from './shared-worker.js' -import Notification from '../notification.js' -import geolocation from './geolocation.js' -import permissions from './permissions.js' -import WebAssembly from './webassembly.js' -import { Buffer } from '../buffer.js' - -import { - ApplicationURLEvent, - HotKeyEvent -} from './events.js' - -import { - File, - FileSystemHandle, - FileSystemFileHandle, - FileSystemDirectoryHandle, - FileSystemWritableFileStream -} from '../fs/web.js' - -import { - showDirectoryPicker, - showOpenFilePicker, - showSaveFilePicker -} from './pickers.js' - -import ipc from '../ipc.js' - -let applied = false -const natives = {} -const patches = {} - -export function init () { - if (applied || !globalThis.window) { - return { natives, patches } - } - - function install (implementations, target = globalThis, prefix) { - for (let name in implementations) { - const implementation = implementations[name] - - if (typeof prefix === 'string') { - name = `${prefix}.${name}` - } - - const actualName = name.split('.').slice(-1)[0] - - if (typeof target[actualName] === 'object' && target[actualName] !== null) { - for (const key in implementation) { - const nativeImplementation = target[actualName][key] || null - if (nativeImplementation === implementation[key]) { - continue - } - // let this fail, the environment implementation may not be writable - try { - target[actualName][key] = implementation[key] - } catch {} - - patches[actualName] ??= {} - patches[actualName][key] = implementation[key] - if (nativeImplementation !== null) { - const nativeName = ['_', 'native', ...name.split('.'), key].join('_') - natives[name] = nativeImplementation - Object.defineProperty(globalThis, nativeName, { - enumerable: false, - configurable: false, - value: nativeImplementation - }) - } - } - } else { - const nativeImplementation = target[actualName] || null - if (nativeImplementation !== implementation) { - // let this fail, the environment implementation may not be writable - try { - target[actualName] = implementation - } catch {} - - patches[actualName] = implementation - if ( - (typeof nativeImplementation === 'function' && nativeImplementation.prototype) && - (typeof implementation === 'function' && implementation.prototype) - ) { - const nativeDescriptors = Object.getOwnPropertyDescriptors(nativeImplementation.prototype) - const descriptors = Object.getOwnPropertyDescriptors(implementation.prototype) - implementation[Symbol.species] = nativeImplementation - for (const key in nativeDescriptors) { - const nativeDescriptor = nativeDescriptors[key] - const descriptor = descriptors[key] - - if (key === 'constructor') { - continue - } - - if (descriptor) { - if (nativeDescriptor.set && nativeDescriptor.get) { - descriptors[key] = { ...nativeDescriptor, ...descriptor } - } else { - descriptors[key] = { writable: true, configurable: true, ...descriptor } - } - } else { - descriptors[key] = { writable: true, configurable: true } - } - - if (descriptors[key] && typeof descriptors[key] === 'object') { - if ('get' in descriptors[key] && typeof descriptors[key].get !== 'function') { - delete descriptors[key].get - } - - if ('set' in descriptors[key] && typeof descriptors[key].set !== 'function') { - delete descriptors[key].set - } - - if (descriptors[key].get || descriptors[key].set) { - delete descriptors[key].writable - delete descriptors[key].value - } - } - } - - Object.defineProperties(implementation.prototype, descriptors) - Object.setPrototypeOf(implementation.prototype, nativeImplementation.prototype) - } - - if (nativeImplementation !== null) { - const nativeName = ['_', 'native', ...name.split('.')].join('_') - natives[name] = nativeImplementation - Object.defineProperty(globalThis, nativeName, { - enumerable: false, - configurable: false, - value: nativeImplementation - }) - } - } - } - } - } - - if ( - typeof globalThis.webkitSpeechRecognition === 'function' && - typeof globalThis.SpeechRecognition !== 'function' - ) { - globalThis.SpeechRecognition = globalThis.webkitSpeechRecognition - } - - // globals - install({ - // url - URL, - URLPattern, - URLSearchParams, - - // fetch - fetch, - Headers, - Request, - Response, - - // notifications - Notification, - - // pickers - showDirectoryPicker, - showOpenFilePicker, - showSaveFilePicker, - - // events - ApplicationURLEvent, - HotKeyEvent, - - // file - File, - FileSystemHandle, - FileSystemFileHandle, - FileSystemDirectoryHandle, - FileSystemWritableFileStream, - - // buffer - Buffer, - - // workers - SharedWorker, - - // platform detection - isSocketRuntime: true - }) - - // navigator - install({ geolocation, permissions }, globalThis.navigator, 'navigator') - - // WebAssembly - install(WebAssembly, globalThis.WebAssembly, 'WebAssembly') - - applied = true - // create tag in document if it doesn't exist - globalThis.document.title ||= '' - // initial value - globalThis.document.addEventListener('DOMContentLoaded', () => { - const title = globalThis.document.title - if (title.length !== 0) { - const index = globalThis.__args.index - const o = new URLSearchParams({ value: title, index }).toString() - ipc.postMessage(`ipc://window.setTitle?${o}`) - } - }) - - // - // globalThis.document is uncofigurable property so we need to use MutationObserver here - // - const observer = new MutationObserver((mutationList) => { - for (const mutation of mutationList) { - if (mutation.type === 'childList') { - const index = globalThis.__args.index - const title = mutation.addedNodes[0].textContent - const o = new URLSearchParams({ value: title, index }).toString() - ipc.postMessage(`ipc://window.setTitle?${o}`) - } - } - }) - - const titleElement = document.querySelector('head > title') - if (titleElement) { - observer.observe(titleElement, { childList: true }) - } - - return { natives, patches } -} - -export default init() diff --git a/api/internal/permissions.js b/api/internal/permissions.js index 746dd42ebc..c67ad072e9 100644 --- a/api/internal/permissions.js +++ b/api/internal/permissions.js @@ -1,6 +1,6 @@ /* global EventTarget, CustomEvent, Event */ /** - * @module Permissions + * @module permissions * This module provides an API for querying and requesting permissions. */ import { IllegalConstructorError } from '../errors.js' @@ -63,7 +63,9 @@ export const types = Enumeration.from([ 'push', 'persistent-storage', 'midi', - 'storage-access' + 'storage-access', + 'camera', + 'microphone' ]) /** @@ -226,6 +228,20 @@ export async function query (descriptor, options) { ) } + if (name === 'camera' || name === 'microphone') { + if (!globalThis.navigator.mediaDevices) { + if (globalThis.isServiceWorkerScope) { + throw new TypeError('MediaDevices are not supported in ServiceWorkerGlobalScope.') + } else if (globalThis.isSharedWorkerScope) { + throw new TypeError('MediaDevices are not supported in SharedWorkerGlobalScope.') + } else if (globalThis.isWorkerScope) { + throw new TypeError('MediaDevices are not supported in WorkerGlobalScope.') + } else { + throw new TypeError('MediaDevices are not supported.') + } + } + } + if (!isAndroid && !isApple) { if (isLinux) { if (name === 'notifications' || name === 'push') { @@ -238,13 +254,31 @@ export async function query (descriptor, options) { } } - const result = await ipc.request('permissions.query', { name, signal: options?.signal }) + const { signal } = options || {} - if (result.err) { - throw result.err + if (options?.signal) { + delete options.signal } - return new PermissionStatus(name, result.data.state, options) + if ( + name === 'notifications' || + name === 'geolocation' || + (isAndroid && (name === 'camera' || name === 'microphone')) + ) { + const result = await ipc.request('permissions.query', { name }, { signal }) + + if (result.err) { + throw result.err + } + + return new PermissionStatus(name, result.data?.state, options) + } + + if (typeof platform.query === 'function') { + return platform.query(descriptor) + } + + throw new TypeError('Not supported') } /** @@ -287,6 +321,41 @@ export async function request (descriptor, options) { ) } + if (name === 'camera' || name === 'microphone') { + // will throw if `MediaDevices` are not supported + const status = await query({ name }, options) + + if (status.state === 'granted') { + return new PermissionStatus(name, 'granted', options) + } + + if (!isAndroid) { + const constraints = { video: false, audio: false } + if (name === 'camera') { + constraints.video = true + delete constraints.audio + } else if (name === 'microphone') { + constraints.audio = true + delete constraints.video + } + + try { + const stream = await globalThis.navigator.mediaDevices.getUserMedia(constraints) + const tracks = await stream.getTracks() + for (const track of tracks) { + await track.stop() + } + return new PermissionStatus(name, 'granted', options) + } catch (err) { + if (err.name === 'NotAllowedError') { + return new PermissionStatus(name, 'denied', options) + } else { + throw err + } + } + } + } + if (isLinux) { if (name === 'notifications' || name === 'push') { const currentState = Notification.permission @@ -316,7 +385,11 @@ export async function request (descriptor, options) { delete options.signal } - const result = await ipc.request('permissions.request', { ...options, name, signal }) + const result = await ipc.request( + 'permissions.request', + { ...options, name }, + { signal } + ) if (result.err) { throw result.err diff --git a/api/internal/pickers.js b/api/internal/pickers.js index 7839441350..4d07e09950 100644 --- a/api/internal/pickers.js +++ b/api/internal/pickers.js @@ -8,6 +8,8 @@ import { FileSystemHandle } from '../fs/web.js' +import bookmarks from '../fs/bookmarks.js' + /** * Key-value store for general usage by the file pickers" * @ignore @@ -125,13 +127,12 @@ function normalizeShowFileSystemPickerOptions (options) { return { contentTypeSpecs: contentTypeSpecs.join('|'), + prefersDarkMode: globalThis?.matchMedia?.('(prefers-color-scheme: dark)')?.matches || false, allowMultiple: options?.multiple === true, defaultName: options?.suggestedName ?? '', defaultPath: resolveStartInPath(options?.startIn, options?.id) ?? '', - // eslint-disable-next-line - allowFiles: options?.files === true ? true : false, - // eslint-disable-next-line - allowDirs: options?.directories === true ? true : false + allowFiles: options?.files === true, + allowDirs: options?.directories === true } } @@ -141,6 +142,7 @@ function normalizeShowFileSystemPickerOptions (options) { * mode?: 'read' | 'readwrite' * startIn?: FileSystemHandle | 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos', * }} ShowDirectoryPickerOptions + * */ /** @@ -168,7 +170,13 @@ export async function showDirectoryPicker (options = null) { db.set(id, dirname) } - return await createFileSystemDirectoryHandle(dirname) + bookmarks.temporary.set(dirname, null) // placehold `null` + + const result = await createFileSystemDirectoryHandle(dirname, { + open: true + }) + + return result } /** @@ -215,7 +223,15 @@ export async function showOpenFilePicker (options = null) { const handles = [] for (const filename of filenames) { - handles.push(await createFileSystemFileHandle(filename, { writable: false })) + bookmarks.temporary.set(filename, null) + } + + for (const filename of filenames) { + const handle = await createFileSystemFileHandle(filename, { + writable: false + }) + + handles.push(handle) } return handles @@ -263,6 +279,8 @@ export async function showSaveFilePicker (options = null) { db.set(id, filename) } + bookmarks.temporary.set(filename, null) + return await createFileSystemFileHandle(filename) } diff --git a/api/internal/post-message.js b/api/internal/post-message.js new file mode 100644 index 0000000000..d4a5f5a176 --- /dev/null +++ b/api/internal/post-message.js @@ -0,0 +1,43 @@ +import serialize from './serialize.js' + +const { + BroadcastChannel, + MessagePort, + postMessage +} = globalThis + +const platform = { + BroadcastChannelPostMessage: BroadcastChannel.prototype.postMessage, + MessagePortPostMessage: MessagePort.prototype.postMessage, + GlobalPostMessage: postMessage +} + +BroadcastChannel.prototype.postMessage = function (message, ...args) { + return platform.BroadcastChannelPostMessage.call( + this, + handlePostMessage(message), + ...args + ) +} + +MessagePort.prototype.postMessage = function (message, ...args) { + return platform.MessagePortPostMessage.call( + this, + handlePostMessage(message), + ...args + ) +} + +globalThis.postMessage = function (message, ...args) { + return platform.GlobalPostMessage.call( + this, + handlePostMessage(message), + ...args + ) +} + +function handlePostMessage (message) { + return serialize(message) +} + +export default null diff --git a/api/internal/primitives.js b/api/internal/primitives.js new file mode 100644 index 0000000000..13579ca09c --- /dev/null +++ b/api/internal/primitives.js @@ -0,0 +1,425 @@ +/* global MutationObserver */ +import './post-message.js' +import './error.js' + +import { fetch, Headers, Request, Response } from '../fetch.js' +import { URL, URLPattern, URLSearchParams } from '../url.js' +import { ServiceWorker } from '../service-worker/instance.js' +import { SharedWorker } from '../shared-worker/index.js' +import serviceWorker from './service-worker.js' +import Notification from '../notification.js' +import geolocation from './geolocation.js' +import permissions from './permissions.js' +import WebAssembly from './webassembly.js' +import { Buffer } from '../buffer.js' +import scheduler from './scheduler.js' +import Promise from './promise.js' +import symbols from './symbols.js' + +import { + ReadableStream, + ReadableStreamBYOBReader, + ReadableByteStreamController, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + + TransformStream, + TransformStreamDefaultController, + + ByteLengthQueuingStrategy, + CountQueuingStrategy +} from './streams.js' + +import { + AsyncContext, + AsyncResource, + AsyncHook, + AsyncLocalStorage, + Deferred +} from '../async.js' + +import { + setTimeout, + setInterval, + setImmediate, + clearTimeout, + clearInterval, + clearImmediate +} from './timers.js' + +import { + ApplicationURLEvent, + MenuItemEvent, + SignalEvent, + HotKeyEvent +} from './events.js' + +import { + File, + FileSystemHandle, + FileSystemFileHandle, + FileSystemDirectoryHandle, + FileSystemWritableFileStream +} from '../fs/web.js' + +import { + showDirectoryPicker, + showOpenFilePicker, + showSaveFilePicker +} from './pickers.js' + +import ipc from '../ipc.js' + +let applied = false +const natives = {} +const patches = {} + +export function init () { + if (applied || globalThis.self !== globalThis) { + return { natives, patches } + } + + function install (implementations, target = globalThis, prefix) { + for (let name in implementations) { + const implementation = implementations[name] + + if (typeof prefix === 'string') { + name = `${prefix}.${name}` + } + + const actualName = name.split('.').slice(-1)[0] + + if (typeof target[actualName] === 'object' && target[actualName] !== null) { + for (const key in implementation) { + const nativeImplementation = target[actualName][key] || null + if (nativeImplementation === implementation[key]) { + continue + } + // let this fail, the environment implementation may not be writable + try { + target[actualName][key] = implementation[key] + } catch {} + + patches[actualName] ??= {} + patches[actualName][key] = implementation[key] + if (nativeImplementation !== null) { + const nativeName = ['_', 'native', ...name.split('.'), key].join('_') + natives[name] = nativeImplementation + Object.defineProperty(globalThis, nativeName, { + enumerable: false, + configurable: false, + value: nativeImplementation + }) + } + } + } else { + const nativeImplementation = target[actualName] || null + if (nativeImplementation !== implementation) { + // let this fail, the environment implementation may not be writable + try { + Object.defineProperty( + target, + actualName, + Object.getOwnPropertyDescriptor(implementations, actualName) + ) + } catch {} + + patches[actualName] = implementation + if ( + (typeof nativeImplementation === 'function' && nativeImplementation.prototype) && + (typeof implementation === 'function' && implementation.prototype) + ) { + const nativeDescriptors = Object.getOwnPropertyDescriptors(nativeImplementation.prototype) + const descriptors = Object.getOwnPropertyDescriptors(implementation.prototype) + + try { + implementation[Symbol.species] = nativeImplementation + } catch {} + + for (const key in nativeDescriptors) { + const nativeDescriptor = nativeDescriptors[key] + const descriptor = descriptors[key] + + if (key === 'constructor') { + continue + } + + if (descriptor) { + if (nativeDescriptor.set && nativeDescriptor.get) { + descriptors[key] = { ...nativeDescriptor, ...descriptor } + } else if (!nativeDescriptor.get && !nativeDescriptor.set) { + descriptors[key] = { ...nativeDescriptor, writable: true, configurable: true, ...descriptor } + } else { + descriptors[key] = { writable: true, configurable: true, ...descriptor } + } + } else { + descriptors[key] = { ...nativeDescriptor, writable: true, configurable: true } + if (typeof implementation.prototype[key] === 'function') { + descriptors[key].value = implementation.prototype[key] + delete descriptors[key].get + delete descriptors[key].set + } + } + + if (descriptors[key] && typeof descriptors[key] === 'object') { + if ('get' in descriptors[key] && typeof descriptors[key].get !== 'function') { + delete descriptors[key].get + } + + if ('set' in descriptors[key] && typeof descriptors[key].set !== 'function') { + delete descriptors[key].set + } + + if (descriptors[key].get || descriptors[key].set) { + delete descriptors[key].writable + delete descriptors[key].value + } + } + } + + Object.defineProperties(implementation.prototype, descriptors) + Object.setPrototypeOf(implementation.prototype, nativeImplementation.prototype) + } + + if (nativeImplementation !== null) { + const nativeName = ['_', 'native', ...name.split('.')].join('_') + natives[name] = nativeImplementation + Object.defineProperty(globalThis, nativeName, { + enumerable: false, + configurable: false, + value: nativeImplementation + }) + } + } + } + } + } + + try { + globalThis.Symbol.dispose = symbols.dispose + } catch {} + + try { + globalThis.Symbol.serialize = symbols.serialize + } catch {} + + if ( + typeof globalThis.webkitSpeechRecognition === 'function' && + typeof globalThis.SpeechRecognition !== 'function' + ) { + globalThis.SpeechRecognition = globalThis.webkitSpeechRecognition + } + + if (globalThis.RUNTIME_WORKER_TYPE !== 'sharedWorker') { + // globals + install({ + // url + URL, + URLPattern, + URLSearchParams, + + // Promise + Promise, + + // fetch + fetch, + Headers, + Request, + Response, + + // notifications + Notification, + + // pickers + showDirectoryPicker, + showOpenFilePicker, + showSaveFilePicker, + + // events + ApplicationURLEvent, + MenuItemEvent, + SignalEvent, + HotKeyEvent, + + // file + File, + FileSystemHandle, + FileSystemFileHandle, + FileSystemDirectoryHandle, + FileSystemWritableFileStream, + + // buffer + Buffer, + + // workers + SharedWorker, + ServiceWorker, + + // timers + setTimeout, + setInterval, + setImmediate, + clearTimeout, + clearInterval, + clearImmediate, + + // streams + ReadableStream, + ReadableStreamBYOBReader, + ReadableByteStreamController, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + TransformStream, + TransformStreamDefaultController, + ByteLengthQueuingStrategy, + CountQueuingStrategy, + + // async + AsyncContext, + AsyncResource, + AsyncHook, + AsyncLocalStorage, + Deferred, + + // platform detection + isSocketRuntime: true + }) + } + + if (globalThis.scheduler) { + install(scheduler, globalThis.scheduler) + } + + if (globalThis.navigator) { + if (globalThis.window) { + install({ geolocation }, globalThis.navigator, 'geolocation') + } + + install({ permissions, serviceWorker }, globalThis.navigator, 'navigator') + + // manually install 'navigator.serviceWorker' accessors from prototype + Object.defineProperties( + globalThis.navigator.serviceWorker, + Object.getOwnPropertyDescriptors(Object.getPrototypeOf(serviceWorker)) + ) + + // manually initialize `ServiceWorkerContainer` instance with the + // runtime implementations + if (typeof serviceWorker.init === 'function') { + serviceWorker.init.call(globalThis.navigator.serviceWorker) + delete serviceWorker.init + } + + // TODO(@jwerle): handle 'popstate' for service workers + // globalThis.addEventListener('popstate', (event) => { }) + } + + // WebAssembly + install(WebAssembly, globalThis.WebAssembly, 'WebAssembly') + + // quirks + if (typeof globalThis.FormData === 'function') { + const { append, set } = FormData.prototype + Object.defineProperties(FormData.prototype, { + append: { + configurable: true, + enumerable: true, + value (name, value, filename) { + if ( // check for 'File' + typeof value === 'object' && + value instanceof Blob && + typeof value.name === 'string' + ) { + if (!filename) { + filename = value.name + } + + value = value.slice() + } + + return append.call(this, name, value, filename) + } + }, + + set: { + configurable: true, + enumerable: true, + value (name, value, filename) { + if ( // check for 'File' + typeof value === 'object' && + value instanceof Blob && + typeof value.name === 'string' + ) { + if (!filename) { + filename = value.name + } + + value = value.slice() + } + + return set.call(this, name, value, filename) + } + } + }) + } + + applied = true + + if (!Error.captureStackTrace) { + Error.captureStackTrace = function () {} + } + + if (globalThis.document && globalThis.top === globalThis) { + // create <title> tag in document if it doesn't exist + globalThis.document.title ||= '' + // initial value + globalThis.document.addEventListener('DOMContentLoaded', async () => { + const { title } = globalThis.document + if (title) { + const result = await ipc.request('window.setTitle', { + targetWindowIndex: globalThis.__args.index, + value: title + }) + + if (result.err) { + console.warn(result.err) + } + } + }) + + // globalThis.document is unconfigurable property so we need to use MutationObserver here + const observer = new MutationObserver(async (mutationList) => { + for (const mutation of mutationList) { + if (mutation.type === 'childList') { + const title = mutation.addedNodes[0].textContent + const result = await ipc.request('window.setTitle', { + targetWindowIndex: globalThis.__args.index, + value: title + }) + + if (result.err) { + console.warn(result.err) + } + } + } + }) + + const titleElement = globalThis.document.querySelector('head > title') + if (titleElement) { + observer.observe(titleElement, { childList: true }) + } + } + + return { natives, patches } +} + +export default init() diff --git a/api/internal/promise.js b/api/internal/promise.js new file mode 100644 index 0000000000..20b8d24e5e --- /dev/null +++ b/api/internal/promise.js @@ -0,0 +1,169 @@ +// @ts-nocheck +import * as asyncHooks from './async/hooks.js' + +const resourceSymbol = Symbol('PromiseResource') + +export const NativePromise = globalThis.Promise +export const NativePromisePrototype = { + then: globalThis.Promise.prototype.then, + catch: globalThis.Promise.prototype.catch, + finally: globalThis.Promise.prototype.finally +} + +export const NativePromiseAll = globalThis.Promise.all.bind(globalThis.Promise) +export const NativePromiseAny = globalThis.Promise.any.bind(globalThis.Promise) + +/** + * @typedef {function(any): void} ResolveFunction + */ + +/** + * @typedef {function(Error|string|null): void} RejectFunction + */ + +/** + * @typedef {function(ResolveFunction, RejectFunction): void} ResolverFunction + */ + +/** + * @typedef {{ + * promise: Promise, + * resolve: ResolveFunction, + * reject: RejectFunction + * }} PromiseResolvers + */ + +// @ts-ignore +export class Promise extends NativePromise { + /** + * Creates a new `Promise` with resolver functions. + * @see {https://github.com/tc39/proposal-promise-with-resolvers} + * @return {PromiseResolvers} + */ + static withResolvers () { + if (typeof super.withResolvers === 'function') { + return super.withResolvers() + } + + const resolvers = { promise: null, resolve: null, reject: null } + resolvers.promise = new Promise((resolve, reject) => { + resolvers.resolve = resolve + resolvers.reject = reject + }) + return resolvers + } + + /** + * `Promise` class constructor. + * @ignore + * @param {ResolverFunction} resolver + */ + constructor (resolver) { + super(resolver) + // eslint-disable-next-line + this[resourceSymbol] = new class Promise extends asyncHooks.CoreAsyncResource { + constructor () { + super('Promise') + } + } + } +} + +Promise.all = function (iterable) { + return NativePromiseAll.call(NativePromise, Array.from(iterable).map((promise, index) => { + if (!promise || typeof promise.catch !== 'function') { + return promise + } + + return promise.catch((err) => { + return Promise.reject(Object.defineProperties(err, { + [Symbol.for('socket.runtime.CallSite.PromiseElementIndex')]: { + configurable: false, + enumerable: false, + writable: false, + value: index + }, + + [Symbol.for('socket.runtime.CallSite.PromiseAll')]: { + configurable: false, + enumerable: false, + writable: false, + value: true + } + })) + }) + })) +} + +Promise.any = function (iterable) { + return NativePromiseAny.call(NativePromise, Array.from(iterable).map((promise, index) => { + if (!promise || typeof promise.catch !== 'function') { + return promise + } + + return promise.catch((err) => { + return Promise.reject(Object.defineProperties(err, { + [Symbol.for('socket.runtime.CallSite.PromiseElementIndex')]: { + configurable: false, + enumerable: false, + writable: false, + value: index + }, + + [Symbol.for('socket.runtime.CallSite.PromiseAny')]: { + configurable: false, + enumerable: false, + writable: false, + value: true + } + })) + }) + })) +} + +function wrapNativePromiseFunction (name) { + const prototype = Promise.prototype + if (prototype[name].__async_wrapped__) { + return + } + + const nativeFunction = prototype[name] + + prototype[name] = function (...args) { + if (asyncHooks.executionAsyncResource().type === 'RuntimeExecution') { + return nativeFunction.call(this, ...args) + } + + const resource = this[resourceSymbol] + + return nativeFunction.call( + this, + ...args.map((arg) => { + if (typeof arg === 'function') { + return asyncHooks.wrap( + arg, + 'Promise', + resource?.asyncId?.() ?? asyncHooks.getNextAsyncResourceId(), + resource?.triggerAsyncId?.() ?? asyncHooks.getDefaultExecutionAsyncId(), + resource ?? undefined + ) + } + + return arg + }) + ) + } + + Object.defineProperty(prototype[name], '__async_wrapped__', { + configurable: false, + enumerable: false, + writable: false, + value: true + }) +} + +wrapNativePromiseFunction('then') +wrapNativePromiseFunction('catch') +wrapNativePromiseFunction('finally') + +export default Promise diff --git a/api/internal/scheduler.js b/api/internal/scheduler.js new file mode 100644 index 0000000000..9bc20ea425 --- /dev/null +++ b/api/internal/scheduler.js @@ -0,0 +1,3 @@ +import scheduler from '../timers/scheduler.js' +export * from '../timers/scheduler.js' +export default scheduler diff --git a/api/internal/serialize.js b/api/internal/serialize.js new file mode 100644 index 0000000000..ddec16e158 --- /dev/null +++ b/api/internal/serialize.js @@ -0,0 +1,48 @@ +import Buffer from '../buffer.js' + +export default function serialize (value) { + if (!value || typeof value !== 'object') { + return value + } + + return map(value, (value) => { + if (Buffer.isBuffer(value)) return value + + if (typeof value[Symbol.serialize] === 'function') { + return value[Symbol.serialize]() + } + + if (typeof value.toJSON === 'function') { + return value.toJSON() + } + + return value + }) +} + +function map (object, callback, seen = new Set()) { + if (seen.has(object)) { + return object + } + + seen.add(object) + if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + object[i] = map(object[i], callback, seen) + } + } else if (object && typeof object === 'object') { + object = callback(object) + for (const key in object) { + const descriptor = Object.getOwnPropertyDescriptor(object, key) + if (descriptor && descriptor.writable) { + object[key] = map(object[key], callback, seen) + } + } + } + + if (object && typeof object === 'object') { + return callback(object) + } else { + return object + } +} diff --git a/api/internal/service-worker.js b/api/internal/service-worker.js new file mode 100644 index 0000000000..ebe3806f5d --- /dev/null +++ b/api/internal/service-worker.js @@ -0,0 +1,4 @@ +import { ServiceWorkerContainer } from '../service-worker/container.js' + +export const serviceWorker = new ServiceWorkerContainer() +export default serviceWorker diff --git a/api/internal/shared-worker.js b/api/internal/shared-worker.js deleted file mode 100644 index 481d17fe48..0000000000 --- a/api/internal/shared-worker.js +++ /dev/null @@ -1,226 +0,0 @@ -/* global MessageChannel, MessagePort, EventTarget, Worker */ -import { murmur3, randomBytes } from '../crypto.js' -import process from '../process.js' -import globals from './globals.js' -import os from '../os.js' -import gc from '../gc.js' - -const workers = new Map() - -export class SharedHybridWorkerProxy extends EventTarget { - #eventListeners = [] - #started = false - #channel = null - #queue = [] - #port = null - #url = null - #id = null - - constructor (url, options) { - super() - - this.#id = randomBytes(8).toString('base64') - this.#url = new URL(url, globalThis.location.href) - this.#channel = globals.get('internal.sharedWorker.channel') - - let onmessage = null - - // temporary port until one becomes acquired - this.#port = Object.create(MessagePort.prototype, { - onmessage: { - configurable: true, - enumerable: true, - get: () => onmessage, - set: (value) => { - onmessage = value - this.#port.start() - } - }, - - onmessageerror: { - configurable: true, - enumerable: true - } - }) - - this.#port.start = () => { this.#started = true } - this.#port.close = () => {} - this.#port.postMessage = (...args) => { - this.#queue.push(args) - } - - this.#port.addEventListener = (...args) => { - this.#eventListeners.push(args) - } - - this.#port.removeEventListener = (...args) => { - this.#eventListeners = this.#eventListeners.filter((eventListener) => { - if ( - eventListener[0] === args[0] && - eventListener[1] === args[1] - ) { - return false - } - - return true - }) - } - - if (!this.#channel) { - throw new TypeError('Unable to acquire global SharedWorker BroadcastChannel') - } - - this.onChannelMessage = this.onChannelMessage.bind(this) - this.#channel.port2.addEventListener('message', this.onChannelMessage) - this.#channel.port2.postMessage({ - __runtime_shared_worker_proxy_create: { - id: this.#id, - url: this.#url.toString(), - options - } - }) - } - - get id () { - return this.#id - } - - get port () { - return this.#port - } - - onChannelMessage (event) { - const eventListeners = this.#eventListeners - const queue = this.#queue - - if (event.data?.__runtime_shared_worker_proxy_init) { - const { id, port } = event.data.__runtime_shared_worker_proxy_init - if (id === this.#id) { - const { start } = port - port.onmessage = this.#port.onmessage - this.#port = port - this.#port.start = () => { - start.call(port) - - for (const entry of queue) { - port.postMessage(...entry) - } - - for (const entry of eventListeners) { - port.addEventListener(...entry) - } - - eventListeners.splice(0, eventListeners.length) - queue.splice(0, queue.length) - } - - if (this.#started) { - this.#port.start() - } - - gc.ref(this) - } - } - } - - [gc.finalizer] () { - return { - args: [this.port, this.#channel, this.onChannelMessage], - handler (port, channel, onChannelMessage) { - try { - port.close() - } catch {} - - channel.removeEventListener('message', onChannelMessage) - } - } - } -} - -export class SharedHybridWorker extends EventTarget { - #id = null - #url = null - #name = null - #worker = null - #channel = null - #onmessage = null - - constructor (url, nameOrOptions) { - super() - if (typeof nameOrOptions === 'string') { - this.#name = nameOrOptions - } - - const options = nameOrOptions && typeof nameOrOptions === 'object' - ? { ...nameOrOptions } - : {} - - this.#url = new URL(url, globalThis.location.href) - // id is based on current origin and worker path name - this.#id = murmur3(globalThis.origin + this.#url.pathname) - - this.#worker = workers.get(this.#id) ?? new Worker(this.#url.toString()) - this.#channel = new MessageChannel() - - workers.set(this.#id, this.#worker) - - this.#worker.addEventListener('error', (event) => { - this.dispatchEvent(new Event(event.type, event)) - }) - - // notify worker of new message channel, transfering `port2` - // to be owned by the worker - this.#worker.postMessage({ - __runtime_shared_worker: { - ports: [this.#channel.port2], - origin: globalThis.origin, - options - } - }, { transfer: [this.#channel.port2] }) - } - - get port () { - return this.#channel.port1 - } -} - -const isInFrame = globalThis.window && globalThis.top !== globalThis.window - -export function getSharedWorkerImplementationForPlatform () { - if ( - os.platform() === 'android' || - (os.platform() === 'win32' && !process.env.COREWEBVIEW2_22_AVAILABLE) - ) { - if (isInFrame) { - return SharedHybridWorkerProxy - } - - return SharedHybridWorker - } - - return globalThis.SharedWorker ?? SharedHybridWorker -} - -export const SharedWorker = getSharedWorkerImplementationForPlatform() - -if (!isInFrame) { - const channel = new MessageChannel() - globals.register('internal.sharedWorker.channel', channel) - channel.port1.start() - channel.port2.start() - - channel.port1.addEventListener('message', (event) => { - if (event.data?.__runtime_shared_worker_proxy_create) { - const { id, url, options } = event.data?.__runtime_shared_worker_proxy_create - const worker = new SharedHybridWorker(url, options) - channel.port1.postMessage({ - __runtime_shared_worker_proxy_init: { - port: worker.port, - id - } - }, { transfer: [worker.port] }) - } - }) -} - -export default SharedWorker diff --git a/api/internal/streams.js b/api/internal/streams.js new file mode 100644 index 0000000000..75cec77ad7 --- /dev/null +++ b/api/internal/streams.js @@ -0,0 +1,39 @@ +import { + ReadableStream, + ReadableStreamBYOBReader, + ReadableByteStreamController, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + + TransformStream, + TransformStreamDefaultController, + + ByteLengthQueuingStrategy, + CountQueuingStrategy +} from './streams/web.js' + +export { + ReadableStream, + ReadableStreamBYOBReader, + ReadableByteStreamController, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + + TransformStream, + TransformStreamDefaultController, + + ByteLengthQueuingStrategy, + CountQueuingStrategy +} + +export default null diff --git a/api/internal/streams/web.js b/api/internal/streams/web.js new file mode 100644 index 0000000000..946c0eefce --- /dev/null +++ b/api/internal/streams/web.js @@ -0,0 +1,8 @@ +/** + * @license + * web-streams-polyfill v4.0.0 + * Copyright 2024 Mattias Buelens, Diwank Singh Tomer and other contributors. + * This code is released under the MIT license. + * SPDX-License-Identifier: MIT + */ +function e(){}function t(e){return"object"==typeof e&&null!==e||"function"==typeof e}const r=e;function o(e,t){try{Object.defineProperty(e,"name",{value:t,configurable:!0})}catch(e){}}const n=Promise,a=Promise.resolve.bind(n),i=Promise.prototype.then,l=Promise.reject.bind(n),s=a;function u(e){return new n(e)}function c(e){return u((t=>t(e)))}function d(e){return l(e)}function f(e,t,r){return i.call(e,t,r)}function b(e,t,o){f(f(e,t,o),void 0,r)}function h(e,t){b(e,t)}function m(e,t){b(e,void 0,t)}function _(e,t,r){return f(e,t,r)}function p(e){f(e,void 0,r)}let y=e=>{if("function"==typeof queueMicrotask)y=queueMicrotask;else{const e=c(void 0);y=t=>f(e,t)}return y(e)};function S(e,t,r){if("function"!=typeof e)throw new TypeError("Argument is not a function");return Function.prototype.apply.call(e,t,r)}function g(e,t,r){try{return c(S(e,t,r))}catch(e){return d(e)}}class v{constructor(){this._cursor=0,this._size=0,this._front={_elements:[],_next:void 0},this._back=this._front,this._cursor=0,this._size=0}get length(){return this._size}push(e){const t=this._back;let r=t;16383===t._elements.length&&(r={_elements:[],_next:void 0}),t._elements.push(e),r!==t&&(this._back=r,t._next=r),++this._size}shift(){const e=this._front;let t=e;const r=this._cursor;let o=r+1;const n=e._elements,a=n[r];return 16384===o&&(t=e._next,o=0),--this._size,this._cursor=o,e!==t&&(this._front=t),n[r]=void 0,a}forEach(e){let t=this._cursor,r=this._front,o=r._elements;for(;!(t===o.length&&void 0===r._next||t===o.length&&(r=r._next,o=r._elements,t=0,0===o.length));)e(o[t]),++t}peek(){const e=this._front,t=this._cursor;return e._elements[t]}}const w=Symbol("[[AbortSteps]]"),R=Symbol("[[ErrorSteps]]"),T=Symbol("[[CancelSteps]]"),C=Symbol("[[PullSteps]]"),P=Symbol("[[ReleaseSteps]]");function q(e,t){e._ownerReadableStream=t,t._reader=e,"readable"===t._state?B(e):"closed"===t._state?function(e){B(e),A(e)}(e):k(e,t._storedError)}function E(e,t){return Or(e._ownerReadableStream,t)}function W(e){const t=e._ownerReadableStream;"readable"===t._state?j(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")):function(e,t){k(e,t)}(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")),t._readableStreamController[P](),t._reader=void 0,e._ownerReadableStream=void 0}function O(e){return new TypeError("Cannot "+e+" a stream using a released reader")}function B(e){e._closedPromise=u(((t,r)=>{e._closedPromise_resolve=t,e._closedPromise_reject=r}))}function k(e,t){B(e),j(e,t)}function j(e,t){void 0!==e._closedPromise_reject&&(p(e._closedPromise),e._closedPromise_reject(t),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}function A(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}const z=Number.isFinite||function(e){return"number"==typeof e&&isFinite(e)},D=Math.trunc||function(e){return e<0?Math.ceil(e):Math.floor(e)};function L(e,t){if(void 0!==e&&("object"!=typeof(r=e)&&"function"!=typeof r))throw new TypeError(`${t} is not an object.`);var r}function F(e,t){if("function"!=typeof e)throw new TypeError(`${t} is not a function.`)}function I(e,t){if(!function(e){return"object"==typeof e&&null!==e||"function"==typeof e}(e))throw new TypeError(`${t} is not an object.`)}function $(e,t,r){if(void 0===e)throw new TypeError(`Parameter ${t} is required in '${r}'.`)}function M(e,t,r){if(void 0===e)throw new TypeError(`${t} is required in '${r}'.`)}function Y(e){return Number(e)}function x(e){return 0===e?0:e}function Q(e,t){const r=Number.MAX_SAFE_INTEGER;let o=Number(e);if(o=x(o),!z(o))throw new TypeError(`${t} is not a finite number`);if(o=function(e){return x(D(e))}(o),o<0||o>r)throw new TypeError(`${t} is outside the accepted range of 0 to ${r}, inclusive`);return z(o)&&0!==o?o:0}function N(e,t){if(!Er(e))throw new TypeError(`${t} is not a ReadableStream.`)}function H(e){return new ReadableStreamDefaultReader(e)}function V(e,t){e._reader._readRequests.push(t)}function U(e,t,r){const o=e._reader._readRequests.shift();r?o._closeSteps():o._chunkSteps(t)}function G(e){return e._reader._readRequests.length}function X(e){const t=e._reader;return void 0!==t&&!!J(t)}class ReadableStreamDefaultReader{constructor(e){if($(e,1,"ReadableStreamDefaultReader"),N(e,"First parameter"),Wr(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");q(this,e),this._readRequests=new v}get closed(){return J(this)?this._closedPromise:d(ee("closed"))}cancel(e=void 0){return J(this)?void 0===this._ownerReadableStream?d(O("cancel")):E(this,e):d(ee("cancel"))}read(){if(!J(this))return d(ee("read"));if(void 0===this._ownerReadableStream)return d(O("read from"));let e,t;const r=u(((r,o)=>{e=r,t=o}));return K(this,{_chunkSteps:t=>e({value:t,done:!1}),_closeSteps:()=>e({value:void 0,done:!0}),_errorSteps:e=>t(e)}),r}releaseLock(){if(!J(this))throw ee("releaseLock");void 0!==this._ownerReadableStream&&function(e){W(e);const t=new TypeError("Reader was released");Z(e,t)}(this)}}function J(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readRequests")&&e instanceof ReadableStreamDefaultReader)}function K(e,t){const r=e._ownerReadableStream;r._disturbed=!0,"closed"===r._state?t._closeSteps():"errored"===r._state?t._errorSteps(r._storedError):r._readableStreamController[C](t)}function Z(e,t){const r=e._readRequests;e._readRequests=new v,r.forEach((e=>{e._errorSteps(t)}))}function ee(e){return new TypeError(`ReadableStreamDefaultReader.prototype.${e} can only be used on a ReadableStreamDefaultReader`)}var te,re,oe;function ne(e){return e.slice()}function ae(e,t,r,o,n){new Uint8Array(e).set(new Uint8Array(r,o,n),t)}Object.defineProperties(ReadableStreamDefaultReader.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),o(ReadableStreamDefaultReader.prototype.cancel,"cancel"),o(ReadableStreamDefaultReader.prototype.read,"read"),o(ReadableStreamDefaultReader.prototype.releaseLock,"releaseLock"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(ReadableStreamDefaultReader.prototype,Symbol.toStringTag,{value:"ReadableStreamDefaultReader",configurable:!0});let ie=e=>(ie="function"==typeof e.transfer?e=>e.transfer():"function"==typeof structuredClone?e=>structuredClone(e,{transfer:[e]}):e=>e,ie(e)),le=e=>(le="boolean"==typeof e.detached?e=>e.detached:e=>0===e.byteLength,le(e));function se(e,t,r){if(e.slice)return e.slice(t,r);const o=r-t,n=new ArrayBuffer(o);return ae(n,0,e,t,o),n}function ue(e,t){const r=e[t];if(null!=r){if("function"!=typeof r)throw new TypeError(`${String(t)} is not a function`);return r}}function ce(e){try{const t=e.done,r=e.value;return f(s(r),(e=>({done:t,value:e})))}catch(e){return d(e)}}const de=null!==(oe=null!==(te=Symbol.asyncIterator)&&void 0!==te?te:null===(re=Symbol.for)||void 0===re?void 0:re.call(Symbol,"Symbol.asyncIterator"))&&void 0!==oe?oe:"@@asyncIterator";function fe(e,r="sync",o){if(void 0===o)if("async"===r){if(void 0===(o=ue(e,de))){return function(e){const r={next(){let t;try{t=be(e)}catch(e){return d(e)}return ce(t)},return(r){let o;try{const t=ue(e.iterator,"return");if(void 0===t)return c({done:!0,value:r});o=S(t,e.iterator,[r])}catch(e){return d(e)}return t(o)?ce(o):d(new TypeError("The iterator.return() method must return an object"))}};return{iterator:r,nextMethod:r.next,done:!1}}(fe(e,"sync",ue(e,Symbol.iterator)))}}else o=ue(e,Symbol.iterator);if(void 0===o)throw new TypeError("The object is not iterable");const n=S(o,e,[]);if(!t(n))throw new TypeError("The iterator method must return an object");return{iterator:n,nextMethod:n.next,done:!1}}function be(e){const r=S(e.nextMethod,e.iterator,[]);if(!t(r))throw new TypeError("The iterator.next() method must return an object");return r}class he{constructor(e,t){this._ongoingPromise=void 0,this._isFinished=!1,this._reader=e,this._preventCancel=t}next(){const e=()=>this._nextSteps();return this._ongoingPromise=this._ongoingPromise?_(this._ongoingPromise,e,e):e(),this._ongoingPromise}return(e){const t=()=>this._returnSteps(e);return this._ongoingPromise?_(this._ongoingPromise,t,t):t()}_nextSteps(){if(this._isFinished)return Promise.resolve({value:void 0,done:!0});const e=this._reader;let t,r;const o=u(((e,o)=>{t=e,r=o}));return K(e,{_chunkSteps:e=>{this._ongoingPromise=void 0,y((()=>t({value:e,done:!1})))},_closeSteps:()=>{this._ongoingPromise=void 0,this._isFinished=!0,W(e),t({value:void 0,done:!0})},_errorSteps:t=>{this._ongoingPromise=void 0,this._isFinished=!0,W(e),r(t)}}),o}_returnSteps(e){if(this._isFinished)return Promise.resolve({value:e,done:!0});this._isFinished=!0;const t=this._reader;if(!this._preventCancel){const r=E(t,e);return W(t),_(r,(()=>({value:e,done:!0})))}return W(t),c({value:e,done:!0})}}const me={next(){return _e(this)?this._asyncIteratorImpl.next():d(pe("next"))},return(e){return _e(this)?this._asyncIteratorImpl.return(e):d(pe("return"))},[de](){return this}};function _e(e){if(!t(e))return!1;if(!Object.prototype.hasOwnProperty.call(e,"_asyncIteratorImpl"))return!1;try{return e._asyncIteratorImpl instanceof he}catch(e){return!1}}function pe(e){return new TypeError(`ReadableStreamAsyncIterator.${e} can only be used on a ReadableSteamAsyncIterator`)}Object.defineProperty(me,de,{enumerable:!1});const ye=Number.isNaN||function(e){return e!=e};function Se(e){const t=se(e.buffer,e.byteOffset,e.byteOffset+e.byteLength);return new Uint8Array(t)}function ge(e){const t=e._queue.shift();return e._queueTotalSize-=t.size,e._queueTotalSize<0&&(e._queueTotalSize=0),t.value}function ve(e,t,r){if("number"!=typeof(o=r)||ye(o)||o<0||r===1/0)throw new RangeError("Size must be a finite, non-NaN, non-negative number.");var o;e._queue.push({value:t,size:r}),e._queueTotalSize+=r}function we(e){e._queue=new v,e._queueTotalSize=0}function Re(e){return e===DataView}class ReadableStreamBYOBRequest{constructor(){throw new TypeError("Illegal constructor")}get view(){if(!Ce(this))throw Je("view");return this._view}respond(e){if(!Ce(this))throw Je("respond");if($(e,1,"respond"),e=Q(e,"First parameter"),void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");if(le(this._view.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be used as a response");Ue(this._associatedReadableByteStreamController,e)}respondWithNewView(e){if(!Ce(this))throw Je("respondWithNewView");if($(e,1,"respondWithNewView"),!ArrayBuffer.isView(e))throw new TypeError("You can only respond with array buffer views");if(void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");if(le(e.buffer))throw new TypeError("The given view's buffer has been detached and so cannot be used as a response");Ge(this._associatedReadableByteStreamController,e)}}Object.defineProperties(ReadableStreamBYOBRequest.prototype,{respond:{enumerable:!0},respondWithNewView:{enumerable:!0},view:{enumerable:!0}}),o(ReadableStreamBYOBRequest.prototype.respond,"respond"),o(ReadableStreamBYOBRequest.prototype.respondWithNewView,"respondWithNewView"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(ReadableStreamBYOBRequest.prototype,Symbol.toStringTag,{value:"ReadableStreamBYOBRequest",configurable:!0});class ReadableByteStreamController{constructor(){throw new TypeError("Illegal constructor")}get byobRequest(){if(!Te(this))throw Ke("byobRequest");return He(this)}get desiredSize(){if(!Te(this))throw Ke("desiredSize");return Ve(this)}close(){if(!Te(this))throw Ke("close");if(this._closeRequested)throw new TypeError("The stream has already been closed; do not close it again!");const e=this._controlledReadableByteStream._state;if("readable"!==e)throw new TypeError(`The stream (in ${e} state) is not in the readable state and cannot be closed`);Ye(this)}enqueue(e){if(!Te(this))throw Ke("enqueue");if($(e,1,"enqueue"),!ArrayBuffer.isView(e))throw new TypeError("chunk must be an array buffer view");if(0===e.byteLength)throw new TypeError("chunk must have non-zero byteLength");if(0===e.buffer.byteLength)throw new TypeError("chunk's buffer must have non-zero byteLength");if(this._closeRequested)throw new TypeError("stream is closed or draining");const t=this._controlledReadableByteStream._state;if("readable"!==t)throw new TypeError(`The stream (in ${t} state) is not in the readable state and cannot be enqueued to`);xe(this,e)}error(e=void 0){if(!Te(this))throw Ke("error");Qe(this,e)}[T](e){qe(this),we(this);const t=this._cancelAlgorithm(e);return Me(this),t}[C](e){const t=this._controlledReadableByteStream;if(this._queueTotalSize>0)return void Ne(this,e);const r=this._autoAllocateChunkSize;if(void 0!==r){let t;try{t=new ArrayBuffer(r)}catch(t){return void e._errorSteps(t)}const o={buffer:t,bufferByteLength:r,byteOffset:0,byteLength:r,bytesFilled:0,minimumFill:1,elementSize:1,viewConstructor:Uint8Array,readerType:"default"};this._pendingPullIntos.push(o)}V(t,e),Pe(this)}[P](){if(this._pendingPullIntos.length>0){const e=this._pendingPullIntos.peek();e.readerType="none",this._pendingPullIntos=new v,this._pendingPullIntos.push(e)}}}function Te(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableByteStream")&&e instanceof ReadableByteStreamController)}function Ce(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_associatedReadableByteStreamController")&&e instanceof ReadableStreamBYOBRequest)}function Pe(e){const t=function(e){const t=e._controlledReadableByteStream;if("readable"!==t._state)return!1;if(e._closeRequested)return!1;if(!e._started)return!1;if(X(t)&&G(t)>0)return!0;if(ot(t)&&rt(t)>0)return!0;const r=Ve(e);if(r>0)return!0;return!1}(e);if(!t)return;if(e._pulling)return void(e._pullAgain=!0);e._pulling=!0;b(e._pullAlgorithm(),(()=>(e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,Pe(e)),null)),(t=>(Qe(e,t),null)))}function qe(e){De(e),e._pendingPullIntos=new v}function Ee(e,t){let r=!1;"closed"===e._state&&(r=!0);const o=We(t);"default"===t.readerType?U(e,o,r):function(e,t,r){const o=e._reader,n=o._readIntoRequests.shift();r?n._closeSteps(t):n._chunkSteps(t)}(e,o,r)}function We(e){const t=e.bytesFilled,r=e.elementSize;return new e.viewConstructor(e.buffer,e.byteOffset,t/r)}function Oe(e,t,r,o){e._queue.push({buffer:t,byteOffset:r,byteLength:o}),e._queueTotalSize+=o}function Be(e,t,r,o){let n;try{n=se(t,r,r+o)}catch(t){throw Qe(e,t),t}Oe(e,n,0,o)}function ke(e,t){t.bytesFilled>0&&Be(e,t.buffer,t.byteOffset,t.bytesFilled),$e(e)}function je(e,t){const r=Math.min(e._queueTotalSize,t.byteLength-t.bytesFilled),o=t.bytesFilled+r;let n=r,a=!1;const i=o-o%t.elementSize;i>=t.minimumFill&&(n=i-t.bytesFilled,a=!0);const l=e._queue;for(;n>0;){const r=l.peek(),o=Math.min(n,r.byteLength),a=t.byteOffset+t.bytesFilled;ae(t.buffer,a,r.buffer,r.byteOffset,o),r.byteLength===o?l.shift():(r.byteOffset+=o,r.byteLength-=o),e._queueTotalSize-=o,Ae(e,o,t),n-=o}return a}function Ae(e,t,r){r.bytesFilled+=t}function ze(e){0===e._queueTotalSize&&e._closeRequested?(Me(e),Br(e._controlledReadableByteStream)):Pe(e)}function De(e){null!==e._byobRequest&&(e._byobRequest._associatedReadableByteStreamController=void 0,e._byobRequest._view=null,e._byobRequest=null)}function Le(e){for(;e._pendingPullIntos.length>0;){if(0===e._queueTotalSize)return;const t=e._pendingPullIntos.peek();je(e,t)&&($e(e),Ee(e._controlledReadableByteStream,t))}}function Fe(e,t,r,o){const n=e._controlledReadableByteStream,a=t.constructor,i=function(e){return Re(e)?1:e.BYTES_PER_ELEMENT}(a),{byteOffset:l,byteLength:s}=t,u=r*i;let c;try{c=ie(t.buffer)}catch(e){return void o._errorSteps(e)}const d={buffer:c,bufferByteLength:c.byteLength,byteOffset:l,byteLength:s,bytesFilled:0,minimumFill:u,elementSize:i,viewConstructor:a,readerType:"byob"};if(e._pendingPullIntos.length>0)return e._pendingPullIntos.push(d),void tt(n,o);if("closed"!==n._state){if(e._queueTotalSize>0){if(je(e,d)){const t=We(d);return ze(e),void o._chunkSteps(t)}if(e._closeRequested){const t=new TypeError("Insufficient bytes to fill elements in the given buffer");return Qe(e,t),void o._errorSteps(t)}}e._pendingPullIntos.push(d),tt(n,o),Pe(e)}else{const e=new a(d.buffer,d.byteOffset,0);o._closeSteps(e)}}function Ie(e,t){const r=e._pendingPullIntos.peek();De(e);"closed"===e._controlledReadableByteStream._state?function(e,t){"none"===t.readerType&&$e(e);const r=e._controlledReadableByteStream;if(ot(r))for(;rt(r)>0;)Ee(r,$e(e))}(e,r):function(e,t,r){if(Ae(0,t,r),"none"===r.readerType)return ke(e,r),void Le(e);if(r.bytesFilled<r.minimumFill)return;$e(e);const o=r.bytesFilled%r.elementSize;if(o>0){const t=r.byteOffset+r.bytesFilled;Be(e,r.buffer,t-o,o)}r.bytesFilled-=o,Ee(e._controlledReadableByteStream,r),Le(e)}(e,t,r),Pe(e)}function $e(e){return e._pendingPullIntos.shift()}function Me(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0}function Ye(e){const t=e._controlledReadableByteStream;if(!e._closeRequested&&"readable"===t._state)if(e._queueTotalSize>0)e._closeRequested=!0;else{if(e._pendingPullIntos.length>0){const t=e._pendingPullIntos.peek();if(t.bytesFilled%t.elementSize!=0){const t=new TypeError("Insufficient bytes to fill elements in the given buffer");throw Qe(e,t),t}}Me(e),Br(t)}}function xe(e,t){const r=e._controlledReadableByteStream;if(e._closeRequested||"readable"!==r._state)return;const{buffer:o,byteOffset:n,byteLength:a}=t;if(le(o))throw new TypeError("chunk's buffer is detached and so cannot be enqueued");const i=ie(o);if(e._pendingPullIntos.length>0){const t=e._pendingPullIntos.peek();if(le(t.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be filled with an enqueued chunk");De(e),t.buffer=ie(t.buffer),"none"===t.readerType&&ke(e,t)}if(X(r))if(function(e){const t=e._controlledReadableByteStream._reader;for(;t._readRequests.length>0;){if(0===e._queueTotalSize)return;Ne(e,t._readRequests.shift())}}(e),0===G(r))Oe(e,i,n,a);else{e._pendingPullIntos.length>0&&$e(e);U(r,new Uint8Array(i,n,a),!1)}else ot(r)?(Oe(e,i,n,a),Le(e)):Oe(e,i,n,a);Pe(e)}function Qe(e,t){const r=e._controlledReadableByteStream;"readable"===r._state&&(qe(e),we(e),Me(e),kr(r,t))}function Ne(e,t){const r=e._queue.shift();e._queueTotalSize-=r.byteLength,ze(e);const o=new Uint8Array(r.buffer,r.byteOffset,r.byteLength);t._chunkSteps(o)}function He(e){if(null===e._byobRequest&&e._pendingPullIntos.length>0){const t=e._pendingPullIntos.peek(),r=new Uint8Array(t.buffer,t.byteOffset+t.bytesFilled,t.byteLength-t.bytesFilled),o=Object.create(ReadableStreamBYOBRequest.prototype);!function(e,t,r){e._associatedReadableByteStreamController=t,e._view=r}(o,e,r),e._byobRequest=o}return e._byobRequest}function Ve(e){const t=e._controlledReadableByteStream._state;return"errored"===t?null:"closed"===t?0:e._strategyHWM-e._queueTotalSize}function Ue(e,t){const r=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==t)throw new TypeError("bytesWritten must be 0 when calling respond() on a closed stream")}else{if(0===t)throw new TypeError("bytesWritten must be greater than 0 when calling respond() on a readable stream");if(r.bytesFilled+t>r.byteLength)throw new RangeError("bytesWritten out of range")}r.buffer=ie(r.buffer),Ie(e,t)}function Ge(e,t){const r=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==t.byteLength)throw new TypeError("The view's length must be 0 when calling respondWithNewView() on a closed stream")}else if(0===t.byteLength)throw new TypeError("The view's length must be greater than 0 when calling respondWithNewView() on a readable stream");if(r.byteOffset+r.bytesFilled!==t.byteOffset)throw new RangeError("The region specified by view does not match byobRequest");if(r.bufferByteLength!==t.buffer.byteLength)throw new RangeError("The buffer of view has different capacity than byobRequest");if(r.bytesFilled+t.byteLength>r.byteLength)throw new RangeError("The region specified by view is larger than byobRequest");const o=t.byteLength;r.buffer=ie(t.buffer),Ie(e,o)}function Xe(e,t,r,o,n,a,i){t._controlledReadableByteStream=e,t._pullAgain=!1,t._pulling=!1,t._byobRequest=null,t._queue=t._queueTotalSize=void 0,we(t),t._closeRequested=!1,t._started=!1,t._strategyHWM=a,t._pullAlgorithm=o,t._cancelAlgorithm=n,t._autoAllocateChunkSize=i,t._pendingPullIntos=new v,e._readableStreamController=t;b(c(r()),(()=>(t._started=!0,Pe(t),null)),(e=>(Qe(t,e),null)))}function Je(e){return new TypeError(`ReadableStreamBYOBRequest.prototype.${e} can only be used on a ReadableStreamBYOBRequest`)}function Ke(e){return new TypeError(`ReadableByteStreamController.prototype.${e} can only be used on a ReadableByteStreamController`)}function Ze(e,t){if("byob"!==(e=`${e}`))throw new TypeError(`${t} '${e}' is not a valid enumeration value for ReadableStreamReaderMode`);return e}function et(e){return new ReadableStreamBYOBReader(e)}function tt(e,t){e._reader._readIntoRequests.push(t)}function rt(e){return e._reader._readIntoRequests.length}function ot(e){const t=e._reader;return void 0!==t&&!!nt(t)}Object.defineProperties(ReadableByteStreamController.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},byobRequest:{enumerable:!0},desiredSize:{enumerable:!0}}),o(ReadableByteStreamController.prototype.close,"close"),o(ReadableByteStreamController.prototype.enqueue,"enqueue"),o(ReadableByteStreamController.prototype.error,"error"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(ReadableByteStreamController.prototype,Symbol.toStringTag,{value:"ReadableByteStreamController",configurable:!0});class ReadableStreamBYOBReader{constructor(e){if($(e,1,"ReadableStreamBYOBReader"),N(e,"First parameter"),Wr(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");if(!Te(e._readableStreamController))throw new TypeError("Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source");q(this,e),this._readIntoRequests=new v}get closed(){return nt(this)?this._closedPromise:d(lt("closed"))}cancel(e=void 0){return nt(this)?void 0===this._ownerReadableStream?d(O("cancel")):E(this,e):d(lt("cancel"))}read(e,t={}){if(!nt(this))return d(lt("read"));if(!ArrayBuffer.isView(e))return d(new TypeError("view must be an array buffer view"));if(0===e.byteLength)return d(new TypeError("view must have non-zero byteLength"));if(0===e.buffer.byteLength)return d(new TypeError("view's buffer must have non-zero byteLength"));if(le(e.buffer))return d(new TypeError("view's buffer has been detached"));let r;try{r=function(e,t){var r;return L(e,t),{min:Q(null!==(r=null==e?void 0:e.min)&&void 0!==r?r:1,`${t} has member 'min' that`)}}(t,"options")}catch(e){return d(e)}const o=r.min;if(0===o)return d(new TypeError("options.min must be greater than 0"));if(function(e){return Re(e.constructor)}(e)){if(o>e.byteLength)return d(new RangeError("options.min must be less than or equal to view's byteLength"))}else if(o>e.length)return d(new RangeError("options.min must be less than or equal to view's length"));if(void 0===this._ownerReadableStream)return d(O("read from"));let n,a;const i=u(((e,t)=>{n=e,a=t}));return at(this,e,o,{_chunkSteps:e=>n({value:e,done:!1}),_closeSteps:e=>n({value:e,done:!0}),_errorSteps:e=>a(e)}),i}releaseLock(){if(!nt(this))throw lt("releaseLock");void 0!==this._ownerReadableStream&&function(e){W(e);const t=new TypeError("Reader was released");it(e,t)}(this)}}function nt(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readIntoRequests")&&e instanceof ReadableStreamBYOBReader)}function at(e,t,r,o){const n=e._ownerReadableStream;n._disturbed=!0,"errored"===n._state?o._errorSteps(n._storedError):Fe(n._readableStreamController,t,r,o)}function it(e,t){const r=e._readIntoRequests;e._readIntoRequests=new v,r.forEach((e=>{e._errorSteps(t)}))}function lt(e){return new TypeError(`ReadableStreamBYOBReader.prototype.${e} can only be used on a ReadableStreamBYOBReader`)}function st(e,t){const{highWaterMark:r}=e;if(void 0===r)return t;if(ye(r)||r<0)throw new RangeError("Invalid highWaterMark");return r}function ut(e){const{size:t}=e;return t||(()=>1)}function ct(e,t){L(e,t);const r=null==e?void 0:e.highWaterMark,o=null==e?void 0:e.size;return{highWaterMark:void 0===r?void 0:Y(r),size:void 0===o?void 0:dt(o,`${t} has member 'size' that`)}}function dt(e,t){return F(e,t),t=>Y(e(t))}function ft(e,t,r){return F(e,r),r=>g(e,t,[r])}function bt(e,t,r){return F(e,r),()=>g(e,t,[])}function ht(e,t,r){return F(e,r),r=>S(e,t,[r])}function mt(e,t,r){return F(e,r),(r,o)=>g(e,t,[r,o])}function _t(e,t){if(!gt(e))throw new TypeError(`${t} is not a WritableStream.`)}Object.defineProperties(ReadableStreamBYOBReader.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),o(ReadableStreamBYOBReader.prototype.cancel,"cancel"),o(ReadableStreamBYOBReader.prototype.read,"read"),o(ReadableStreamBYOBReader.prototype.releaseLock,"releaseLock"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(ReadableStreamBYOBReader.prototype,Symbol.toStringTag,{value:"ReadableStreamBYOBReader",configurable:!0});const pt="function"==typeof AbortController;class WritableStream{constructor(e={},t={}){void 0===e?e=null:I(e,"First parameter");const r=ct(t,"Second parameter"),o=function(e,t){L(e,t);const r=null==e?void 0:e.abort,o=null==e?void 0:e.close,n=null==e?void 0:e.start,a=null==e?void 0:e.type,i=null==e?void 0:e.write;return{abort:void 0===r?void 0:ft(r,e,`${t} has member 'abort' that`),close:void 0===o?void 0:bt(o,e,`${t} has member 'close' that`),start:void 0===n?void 0:ht(n,e,`${t} has member 'start' that`),write:void 0===i?void 0:mt(i,e,`${t} has member 'write' that`),type:a}}(e,"First parameter");St(this);if(void 0!==o.type)throw new RangeError("Invalid type is specified");const n=ut(r);!function(e,t,r,o){const n=Object.create(WritableStreamDefaultController.prototype);let a,i,l,s;a=void 0!==t.start?()=>t.start(n):()=>{};i=void 0!==t.write?e=>t.write(e,n):()=>c(void 0);l=void 0!==t.close?()=>t.close():()=>c(void 0);s=void 0!==t.abort?e=>t.abort(e):()=>c(void 0);Ft(e,n,a,i,l,s,r,o)}(this,o,st(r,1),n)}get locked(){if(!gt(this))throw Nt("locked");return vt(this)}abort(e=void 0){return gt(this)?vt(this)?d(new TypeError("Cannot abort a stream that already has a writer")):wt(this,e):d(Nt("abort"))}close(){return gt(this)?vt(this)?d(new TypeError("Cannot close a stream that already has a writer")):qt(this)?d(new TypeError("Cannot close an already-closing stream")):Rt(this):d(Nt("close"))}getWriter(){if(!gt(this))throw Nt("getWriter");return yt(this)}}function yt(e){return new WritableStreamDefaultWriter(e)}function St(e){e._state="writable",e._storedError=void 0,e._writer=void 0,e._writableStreamController=void 0,e._writeRequests=new v,e._inFlightWriteRequest=void 0,e._closeRequest=void 0,e._inFlightCloseRequest=void 0,e._pendingAbortRequest=void 0,e._backpressure=!1}function gt(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_writableStreamController")&&e instanceof WritableStream)}function vt(e){return void 0!==e._writer}function wt(e,t){var r;if("closed"===e._state||"errored"===e._state)return c(void 0);e._writableStreamController._abortReason=t,null===(r=e._writableStreamController._abortController)||void 0===r||r.abort(t);const o=e._state;if("closed"===o||"errored"===o)return c(void 0);if(void 0!==e._pendingAbortRequest)return e._pendingAbortRequest._promise;let n=!1;"erroring"===o&&(n=!0,t=void 0);const a=u(((r,o)=>{e._pendingAbortRequest={_promise:void 0,_resolve:r,_reject:o,_reason:t,_wasAlreadyErroring:n}}));return e._pendingAbortRequest._promise=a,n||Ct(e,t),a}function Rt(e){const t=e._state;if("closed"===t||"errored"===t)return d(new TypeError(`The stream (in ${t} state) is not in the writable state and cannot be closed`));const r=u(((t,r)=>{const o={_resolve:t,_reject:r};e._closeRequest=o})),o=e._writer;var n;return void 0!==o&&e._backpressure&&"writable"===t&&or(o),ve(n=e._writableStreamController,Dt,0),Mt(n),r}function Tt(e,t){"writable"!==e._state?Pt(e):Ct(e,t)}function Ct(e,t){const r=e._writableStreamController;e._state="erroring",e._storedError=t;const o=e._writer;void 0!==o&&jt(o,t),!function(e){if(void 0===e._inFlightWriteRequest&&void 0===e._inFlightCloseRequest)return!1;return!0}(e)&&r._started&&Pt(e)}function Pt(e){e._state="errored",e._writableStreamController[R]();const t=e._storedError;if(e._writeRequests.forEach((e=>{e._reject(t)})),e._writeRequests=new v,void 0===e._pendingAbortRequest)return void Et(e);const r=e._pendingAbortRequest;if(e._pendingAbortRequest=void 0,r._wasAlreadyErroring)return r._reject(t),void Et(e);b(e._writableStreamController[w](r._reason),(()=>(r._resolve(),Et(e),null)),(t=>(r._reject(t),Et(e),null)))}function qt(e){return void 0!==e._closeRequest||void 0!==e._inFlightCloseRequest}function Et(e){void 0!==e._closeRequest&&(e._closeRequest._reject(e._storedError),e._closeRequest=void 0);const t=e._writer;void 0!==t&&Jt(t,e._storedError)}function Wt(e,t){const r=e._writer;void 0!==r&&t!==e._backpressure&&(t?function(e){Zt(e)}(r):or(r)),e._backpressure=t}Object.defineProperties(WritableStream.prototype,{abort:{enumerable:!0},close:{enumerable:!0},getWriter:{enumerable:!0},locked:{enumerable:!0}}),o(WritableStream.prototype.abort,"abort"),o(WritableStream.prototype.close,"close"),o(WritableStream.prototype.getWriter,"getWriter"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(WritableStream.prototype,Symbol.toStringTag,{value:"WritableStream",configurable:!0});class WritableStreamDefaultWriter{constructor(e){if($(e,1,"WritableStreamDefaultWriter"),_t(e,"First parameter"),vt(e))throw new TypeError("This stream has already been locked for exclusive writing by another writer");this._ownerWritableStream=e,e._writer=this;const t=e._state;if("writable"===t)!qt(e)&&e._backpressure?Zt(this):tr(this),Gt(this);else if("erroring"===t)er(this,e._storedError),Gt(this);else if("closed"===t)tr(this),Gt(r=this),Kt(r);else{const t=e._storedError;er(this,t),Xt(this,t)}var r}get closed(){return Ot(this)?this._closedPromise:d(Vt("closed"))}get desiredSize(){if(!Ot(this))throw Vt("desiredSize");if(void 0===this._ownerWritableStream)throw Ut("desiredSize");return function(e){const t=e._ownerWritableStream,r=t._state;if("errored"===r||"erroring"===r)return null;if("closed"===r)return 0;return $t(t._writableStreamController)}(this)}get ready(){return Ot(this)?this._readyPromise:d(Vt("ready"))}abort(e=void 0){return Ot(this)?void 0===this._ownerWritableStream?d(Ut("abort")):function(e,t){return wt(e._ownerWritableStream,t)}(this,e):d(Vt("abort"))}close(){if(!Ot(this))return d(Vt("close"));const e=this._ownerWritableStream;return void 0===e?d(Ut("close")):qt(e)?d(new TypeError("Cannot close an already-closing stream")):Bt(this)}releaseLock(){if(!Ot(this))throw Vt("releaseLock");void 0!==this._ownerWritableStream&&At(this)}write(e=void 0){return Ot(this)?void 0===this._ownerWritableStream?d(Ut("write to")):zt(this,e):d(Vt("write"))}}function Ot(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_ownerWritableStream")&&e instanceof WritableStreamDefaultWriter)}function Bt(e){return Rt(e._ownerWritableStream)}function kt(e,t){"pending"===e._closedPromiseState?Jt(e,t):function(e,t){Xt(e,t)}(e,t)}function jt(e,t){"pending"===e._readyPromiseState?rr(e,t):function(e,t){er(e,t)}(e,t)}function At(e){const t=e._ownerWritableStream,r=new TypeError("Writer was released and can no longer be used to monitor the stream's closedness");jt(e,r),kt(e,r),t._writer=void 0,e._ownerWritableStream=void 0}function zt(e,t){const r=e._ownerWritableStream,o=r._writableStreamController,n=function(e,t){try{return e._strategySizeAlgorithm(t)}catch(t){return Yt(e,t),1}}(o,t);if(r!==e._ownerWritableStream)return d(Ut("write to"));const a=r._state;if("errored"===a)return d(r._storedError);if(qt(r)||"closed"===a)return d(new TypeError("The stream is closing or closed and cannot be written to"));if("erroring"===a)return d(r._storedError);const i=function(e){return u(((t,r)=>{const o={_resolve:t,_reject:r};e._writeRequests.push(o)}))}(r);return function(e,t,r){try{ve(e,t,r)}catch(t){return void Yt(e,t)}const o=e._controlledWritableStream;if(!qt(o)&&"writable"===o._state){Wt(o,xt(e))}Mt(e)}(o,t,n),i}Object.defineProperties(WritableStreamDefaultWriter.prototype,{abort:{enumerable:!0},close:{enumerable:!0},releaseLock:{enumerable:!0},write:{enumerable:!0},closed:{enumerable:!0},desiredSize:{enumerable:!0},ready:{enumerable:!0}}),o(WritableStreamDefaultWriter.prototype.abort,"abort"),o(WritableStreamDefaultWriter.prototype.close,"close"),o(WritableStreamDefaultWriter.prototype.releaseLock,"releaseLock"),o(WritableStreamDefaultWriter.prototype.write,"write"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(WritableStreamDefaultWriter.prototype,Symbol.toStringTag,{value:"WritableStreamDefaultWriter",configurable:!0});const Dt={};class WritableStreamDefaultController{constructor(){throw new TypeError("Illegal constructor")}get abortReason(){if(!Lt(this))throw Ht("abortReason");return this._abortReason}get signal(){if(!Lt(this))throw Ht("signal");if(void 0===this._abortController)throw new TypeError("WritableStreamDefaultController.prototype.signal is not supported");return this._abortController.signal}error(e=void 0){if(!Lt(this))throw Ht("error");"writable"===this._controlledWritableStream._state&&Qt(this,e)}[w](e){const t=this._abortAlgorithm(e);return It(this),t}[R](){we(this)}}function Lt(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledWritableStream")&&e instanceof WritableStreamDefaultController)}function Ft(e,t,r,o,n,a,i,l){t._controlledWritableStream=e,e._writableStreamController=t,t._queue=void 0,t._queueTotalSize=void 0,we(t),t._abortReason=void 0,t._abortController=function(){if(pt)return new AbortController}(),t._started=!1,t._strategySizeAlgorithm=l,t._strategyHWM=i,t._writeAlgorithm=o,t._closeAlgorithm=n,t._abortAlgorithm=a;const s=xt(t);Wt(e,s);b(c(r()),(()=>(t._started=!0,Mt(t),null)),(r=>(t._started=!0,Tt(e,r),null)))}function It(e){e._writeAlgorithm=void 0,e._closeAlgorithm=void 0,e._abortAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function $t(e){return e._strategyHWM-e._queueTotalSize}function Mt(e){const t=e._controlledWritableStream;if(!e._started)return;if(void 0!==t._inFlightWriteRequest)return;if("erroring"===t._state)return void Pt(t);if(0===e._queue.length)return;const r=e._queue.peek().value;r===Dt?function(e){const t=e._controlledWritableStream;(function(e){e._inFlightCloseRequest=e._closeRequest,e._closeRequest=void 0})(t),ge(e);const r=e._closeAlgorithm();It(e),b(r,(()=>(function(e){e._inFlightCloseRequest._resolve(void 0),e._inFlightCloseRequest=void 0,"erroring"===e._state&&(e._storedError=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._resolve(),e._pendingAbortRequest=void 0)),e._state="closed";const t=e._writer;void 0!==t&&Kt(t)}(t),null)),(e=>(function(e,t){e._inFlightCloseRequest._reject(t),e._inFlightCloseRequest=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._reject(t),e._pendingAbortRequest=void 0),Tt(e,t)}(t,e),null)))}(e):function(e,t){const r=e._controlledWritableStream;!function(e){e._inFlightWriteRequest=e._writeRequests.shift()}(r);const o=e._writeAlgorithm(t);b(o,(()=>{!function(e){e._inFlightWriteRequest._resolve(void 0),e._inFlightWriteRequest=void 0}(r);const t=r._state;if(ge(e),!qt(r)&&"writable"===t){const t=xt(e);Wt(r,t)}return Mt(e),null}),(t=>("writable"===r._state&&It(e),function(e,t){e._inFlightWriteRequest._reject(t),e._inFlightWriteRequest=void 0,Tt(e,t)}(r,t),null)))}(e,r)}function Yt(e,t){"writable"===e._controlledWritableStream._state&&Qt(e,t)}function xt(e){return $t(e)<=0}function Qt(e,t){const r=e._controlledWritableStream;It(e),Ct(r,t)}function Nt(e){return new TypeError(`WritableStream.prototype.${e} can only be used on a WritableStream`)}function Ht(e){return new TypeError(`WritableStreamDefaultController.prototype.${e} can only be used on a WritableStreamDefaultController`)}function Vt(e){return new TypeError(`WritableStreamDefaultWriter.prototype.${e} can only be used on a WritableStreamDefaultWriter`)}function Ut(e){return new TypeError("Cannot "+e+" a stream using a released writer")}function Gt(e){e._closedPromise=u(((t,r)=>{e._closedPromise_resolve=t,e._closedPromise_reject=r,e._closedPromiseState="pending"}))}function Xt(e,t){Gt(e),Jt(e,t)}function Jt(e,t){void 0!==e._closedPromise_reject&&(p(e._closedPromise),e._closedPromise_reject(t),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="rejected")}function Kt(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="resolved")}function Zt(e){e._readyPromise=u(((t,r)=>{e._readyPromise_resolve=t,e._readyPromise_reject=r})),e._readyPromiseState="pending"}function er(e,t){Zt(e),rr(e,t)}function tr(e){Zt(e),or(e)}function rr(e,t){void 0!==e._readyPromise_reject&&(p(e._readyPromise),e._readyPromise_reject(t),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="rejected")}function or(e){void 0!==e._readyPromise_resolve&&(e._readyPromise_resolve(void 0),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="fulfilled")}Object.defineProperties(WritableStreamDefaultController.prototype,{abortReason:{enumerable:!0},signal:{enumerable:!0},error:{enumerable:!0}}),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(WritableStreamDefaultController.prototype,Symbol.toStringTag,{value:"WritableStreamDefaultController",configurable:!0});const nr="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof global?global:void 0;const ar=function(){const e=null==nr?void 0:nr.DOMException;return function(e){if("function"!=typeof e&&"object"!=typeof e)return!1;if("DOMException"!==e.name)return!1;try{return new e,!0}catch(e){return!1}}(e)?e:void 0}()||function(){const e=function(e,t){this.message=e||"",this.name=t||"Error",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)};return o(e,"DOMException"),e.prototype=Object.create(Error.prototype),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,configurable:!0}),e}();function ir(t,r,o,n,a,i){const l=H(t),s=yt(r);t._disturbed=!0;let _=!1,y=c(void 0);return u(((S,g)=>{let v;if(void 0!==i){if(v=()=>{const e=void 0!==i.reason?i.reason:new ar("Aborted","AbortError"),o=[];n||o.push((()=>"writable"===r._state?wt(r,e):c(void 0))),a||o.push((()=>"readable"===t._state?Or(t,e):c(void 0))),q((()=>Promise.all(o.map((e=>e())))),!0,e)},i.aborted)return void v();i.addEventListener("abort",v)}var w,R,T;if(P(t,l._closedPromise,(e=>(n?E(!0,e):q((()=>wt(r,e)),!0,e),null))),P(r,s._closedPromise,(e=>(a?E(!0,e):q((()=>Or(t,e)),!0,e),null))),w=t,R=l._closedPromise,T=()=>(o?E():q((()=>function(e){const t=e._ownerWritableStream,r=t._state;return qt(t)||"closed"===r?c(void 0):"errored"===r?d(t._storedError):Bt(e)}(s))),null),"closed"===w._state?T():h(R,T),qt(r)||"closed"===r._state){const e=new TypeError("the destination writable stream closed before all data could be piped to it");a?E(!0,e):q((()=>Or(t,e)),!0,e)}function C(){const e=y;return f(y,(()=>e!==y?C():void 0))}function P(e,t,r){"errored"===e._state?r(e._storedError):m(t,r)}function q(e,t,o){function n(){return b(e(),(()=>O(t,o)),(e=>O(!0,e))),null}_||(_=!0,"writable"!==r._state||qt(r)?n():h(C(),n))}function E(e,t){_||(_=!0,"writable"!==r._state||qt(r)?O(e,t):h(C(),(()=>O(e,t))))}function O(e,t){return At(s),W(l),void 0!==i&&i.removeEventListener("abort",v),e?g(t):S(void 0),null}p(u(((t,r)=>{!function o(n){n?t():f(_?c(!0):f(s._readyPromise,(()=>u(((t,r)=>{K(l,{_chunkSteps:r=>{y=f(zt(s,r),void 0,e),t(!1)},_closeSteps:()=>t(!0),_errorSteps:r})})))),o,r)}(!1)})))}))}class ReadableStreamDefaultController{constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!lr(this))throw pr("desiredSize");return hr(this)}close(){if(!lr(this))throw pr("close");if(!mr(this))throw new TypeError("The stream is not in a state that permits close");dr(this)}enqueue(e=void 0){if(!lr(this))throw pr("enqueue");if(!mr(this))throw new TypeError("The stream is not in a state that permits enqueue");return fr(this,e)}error(e=void 0){if(!lr(this))throw pr("error");br(this,e)}[T](e){we(this);const t=this._cancelAlgorithm(e);return cr(this),t}[C](e){const t=this._controlledReadableStream;if(this._queue.length>0){const r=ge(this);this._closeRequested&&0===this._queue.length?(cr(this),Br(t)):sr(this),e._chunkSteps(r)}else V(t,e),sr(this)}[P](){}}function lr(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableStream")&&e instanceof ReadableStreamDefaultController)}function sr(e){if(!ur(e))return;if(e._pulling)return void(e._pullAgain=!0);e._pulling=!0;b(e._pullAlgorithm(),(()=>(e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,sr(e)),null)),(t=>(br(e,t),null)))}function ur(e){const t=e._controlledReadableStream;if(!mr(e))return!1;if(!e._started)return!1;if(Wr(t)&&G(t)>0)return!0;return hr(e)>0}function cr(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function dr(e){if(!mr(e))return;const t=e._controlledReadableStream;e._closeRequested=!0,0===e._queue.length&&(cr(e),Br(t))}function fr(e,t){if(!mr(e))return;const r=e._controlledReadableStream;if(Wr(r)&&G(r)>0)U(r,t,!1);else{let r;try{r=e._strategySizeAlgorithm(t)}catch(t){throw br(e,t),t}try{ve(e,t,r)}catch(t){throw br(e,t),t}}sr(e)}function br(e,t){const r=e._controlledReadableStream;"readable"===r._state&&(we(e),cr(e),kr(r,t))}function hr(e){const t=e._controlledReadableStream._state;return"errored"===t?null:"closed"===t?0:e._strategyHWM-e._queueTotalSize}function mr(e){const t=e._controlledReadableStream._state;return!e._closeRequested&&"readable"===t}function _r(e,t,r,o,n,a,i){t._controlledReadableStream=e,t._queue=void 0,t._queueTotalSize=void 0,we(t),t._started=!1,t._closeRequested=!1,t._pullAgain=!1,t._pulling=!1,t._strategySizeAlgorithm=i,t._strategyHWM=a,t._pullAlgorithm=o,t._cancelAlgorithm=n,e._readableStreamController=t;b(c(r()),(()=>(t._started=!0,sr(t),null)),(e=>(br(t,e),null)))}function pr(e){return new TypeError(`ReadableStreamDefaultController.prototype.${e} can only be used on a ReadableStreamDefaultController`)}function yr(e,t){return Te(e._readableStreamController)?function(e){let t,r,o,n,a,i=H(e),l=!1,s=!1,d=!1,f=!1,b=!1;const h=u((e=>{a=e}));function _(e){m(e._closedPromise,(t=>(e!==i||(Qe(o._readableStreamController,t),Qe(n._readableStreamController,t),f&&b||a(void 0)),null)))}function p(){nt(i)&&(W(i),i=H(e),_(i));K(i,{_chunkSteps:t=>{y((()=>{s=!1,d=!1;const r=t;let i=t;if(!f&&!b)try{i=Se(t)}catch(t){return Qe(o._readableStreamController,t),Qe(n._readableStreamController,t),void a(Or(e,t))}f||xe(o._readableStreamController,r),b||xe(n._readableStreamController,i),l=!1,s?g():d&&v()}))},_closeSteps:()=>{l=!1,f||Ye(o._readableStreamController),b||Ye(n._readableStreamController),o._readableStreamController._pendingPullIntos.length>0&&Ue(o._readableStreamController,0),n._readableStreamController._pendingPullIntos.length>0&&Ue(n._readableStreamController,0),f&&b||a(void 0)},_errorSteps:()=>{l=!1}})}function S(t,r){J(i)&&(W(i),i=et(e),_(i));const u=r?n:o,c=r?o:n;at(i,t,1,{_chunkSteps:t=>{y((()=>{s=!1,d=!1;const o=r?b:f;if(r?f:b)o||Ge(u._readableStreamController,t);else{let r;try{r=Se(t)}catch(t){return Qe(u._readableStreamController,t),Qe(c._readableStreamController,t),void a(Or(e,t))}o||Ge(u._readableStreamController,t),xe(c._readableStreamController,r)}l=!1,s?g():d&&v()}))},_closeSteps:e=>{l=!1;const t=r?b:f,o=r?f:b;t||Ye(u._readableStreamController),o||Ye(c._readableStreamController),void 0!==e&&(t||Ge(u._readableStreamController,e),!o&&c._readableStreamController._pendingPullIntos.length>0&&Ue(c._readableStreamController,0)),t&&o||a(void 0)},_errorSteps:()=>{l=!1}})}function g(){if(l)return s=!0,c(void 0);l=!0;const e=He(o._readableStreamController);return null===e?p():S(e._view,!1),c(void 0)}function v(){if(l)return d=!0,c(void 0);l=!0;const e=He(n._readableStreamController);return null===e?p():S(e._view,!0),c(void 0)}function w(o){if(f=!0,t=o,b){const o=ne([t,r]),n=Or(e,o);a(n)}return h}function R(o){if(b=!0,r=o,f){const o=ne([t,r]),n=Or(e,o);a(n)}return h}function T(){}return o=Pr(T,g,w),n=Pr(T,v,R),_(i),[o,n]}(e):function(e,t){const r=H(e);let o,n,a,i,l,s=!1,d=!1,f=!1,b=!1;const h=u((e=>{l=e}));function _(){if(s)return d=!0,c(void 0);s=!0;return K(r,{_chunkSteps:e=>{y((()=>{d=!1;const t=e,r=e;f||fr(a._readableStreamController,t),b||fr(i._readableStreamController,r),s=!1,d&&_()}))},_closeSteps:()=>{s=!1,f||dr(a._readableStreamController),b||dr(i._readableStreamController),f&&b||l(void 0)},_errorSteps:()=>{s=!1}}),c(void 0)}function p(t){if(f=!0,o=t,b){const t=ne([o,n]),r=Or(e,t);l(r)}return h}function S(t){if(b=!0,n=t,f){const t=ne([o,n]),r=Or(e,t);l(r)}return h}function g(){}return a=Cr(g,_,p),i=Cr(g,_,S),m(r._closedPromise,(e=>(br(a._readableStreamController,e),br(i._readableStreamController,e),f&&b||l(void 0),null))),[a,i]}(e)}function Sr(r){return t(o=r)&&void 0!==o.getReader?function(r){let o;function n(){let e;try{e=r.read()}catch(e){return d(e)}return _(e,(e=>{if(!t(e))throw new TypeError("The promise returned by the reader.read() method must fulfill with an object");if(e.done)dr(o._readableStreamController);else{const t=e.value;fr(o._readableStreamController,t)}}))}function a(e){try{return c(r.cancel(e))}catch(e){return d(e)}}return o=Cr(e,n,a,0),o}(r.getReader()):function(r){let o;const n=fe(r,"async");function a(){let e;try{e=be(n)}catch(e){return d(e)}return _(c(e),(e=>{if(!t(e))throw new TypeError("The promise returned by the iterator.next() method must fulfill with an object");if(e.done)dr(o._readableStreamController);else{const t=e.value;fr(o._readableStreamController,t)}}))}function i(e){const r=n.iterator;let o;try{o=ue(r,"return")}catch(e){return d(e)}if(void 0===o)return c(void 0);return _(g(o,r,[e]),(e=>{if(!t(e))throw new TypeError("The promise returned by the iterator.return() method must fulfill with an object")}))}return o=Cr(e,a,i,0),o}(r);var o}function gr(e,t,r){return F(e,r),r=>g(e,t,[r])}function vr(e,t,r){return F(e,r),r=>g(e,t,[r])}function wr(e,t,r){return F(e,r),r=>S(e,t,[r])}function Rr(e,t){if("bytes"!==(e=`${e}`))throw new TypeError(`${t} '${e}' is not a valid enumeration value for ReadableStreamType`);return e}function Tr(e,t){L(e,t);const r=null==e?void 0:e.preventAbort,o=null==e?void 0:e.preventCancel,n=null==e?void 0:e.preventClose,a=null==e?void 0:e.signal;return void 0!==a&&function(e,t){if(!function(e){if("object"!=typeof e||null===e)return!1;try{return"boolean"==typeof e.aborted}catch(e){return!1}}(e))throw new TypeError(`${t} is not an AbortSignal.`)}(a,`${t} has member 'signal' that`),{preventAbort:Boolean(r),preventCancel:Boolean(o),preventClose:Boolean(n),signal:a}}Object.defineProperties(ReadableStreamDefaultController.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},desiredSize:{enumerable:!0}}),o(ReadableStreamDefaultController.prototype.close,"close"),o(ReadableStreamDefaultController.prototype.enqueue,"enqueue"),o(ReadableStreamDefaultController.prototype.error,"error"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(ReadableStreamDefaultController.prototype,Symbol.toStringTag,{value:"ReadableStreamDefaultController",configurable:!0});class ReadableStream{constructor(e={},t={}){void 0===e?e=null:I(e,"First parameter");const r=ct(t,"Second parameter"),o=function(e,t){L(e,t);const r=e,o=null==r?void 0:r.autoAllocateChunkSize,n=null==r?void 0:r.cancel,a=null==r?void 0:r.pull,i=null==r?void 0:r.start,l=null==r?void 0:r.type;return{autoAllocateChunkSize:void 0===o?void 0:Q(o,`${t} has member 'autoAllocateChunkSize' that`),cancel:void 0===n?void 0:gr(n,r,`${t} has member 'cancel' that`),pull:void 0===a?void 0:vr(a,r,`${t} has member 'pull' that`),start:void 0===i?void 0:wr(i,r,`${t} has member 'start' that`),type:void 0===l?void 0:Rr(l,`${t} has member 'type' that`)}}(e,"First parameter");if(qr(this),"bytes"===o.type){if(void 0!==r.size)throw new RangeError("The strategy for a byte stream cannot have a size function");!function(e,t,r){const o=Object.create(ReadableByteStreamController.prototype);let n,a,i;n=void 0!==t.start?()=>t.start(o):()=>{},a=void 0!==t.pull?()=>t.pull(o):()=>c(void 0),i=void 0!==t.cancel?e=>t.cancel(e):()=>c(void 0);const l=t.autoAllocateChunkSize;if(0===l)throw new TypeError("autoAllocateChunkSize must be greater than 0");Xe(e,o,n,a,i,r,l)}(this,o,st(r,0))}else{const e=ut(r);!function(e,t,r,o){const n=Object.create(ReadableStreamDefaultController.prototype);let a,i,l;a=void 0!==t.start?()=>t.start(n):()=>{},i=void 0!==t.pull?()=>t.pull(n):()=>c(void 0),l=void 0!==t.cancel?e=>t.cancel(e):()=>c(void 0),_r(e,n,a,i,l,r,o)}(this,o,st(r,1),e)}}get locked(){if(!Er(this))throw jr("locked");return Wr(this)}cancel(e=void 0){return Er(this)?Wr(this)?d(new TypeError("Cannot cancel a stream that already has a reader")):Or(this,e):d(jr("cancel"))}getReader(e=void 0){if(!Er(this))throw jr("getReader");return void 0===function(e,t){L(e,t);const r=null==e?void 0:e.mode;return{mode:void 0===r?void 0:Ze(r,`${t} has member 'mode' that`)}}(e,"First parameter").mode?H(this):et(this)}pipeThrough(e,t={}){if(!Er(this))throw jr("pipeThrough");$(e,1,"pipeThrough");const r=function(e,t){L(e,t);const r=null==e?void 0:e.readable;M(r,"readable","ReadableWritablePair"),N(r,`${t} has member 'readable' that`);const o=null==e?void 0:e.writable;return M(o,"writable","ReadableWritablePair"),_t(o,`${t} has member 'writable' that`),{readable:r,writable:o}}(e,"First parameter"),o=Tr(t,"Second parameter");if(Wr(this))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream");if(vt(r.writable))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream");return p(ir(this,r.writable,o.preventClose,o.preventAbort,o.preventCancel,o.signal)),r.readable}pipeTo(e,t={}){if(!Er(this))return d(jr("pipeTo"));if(void 0===e)return d("Parameter 1 is required in 'pipeTo'.");if(!gt(e))return d(new TypeError("ReadableStream.prototype.pipeTo's first argument must be a WritableStream"));let r;try{r=Tr(t,"Second parameter")}catch(e){return d(e)}return Wr(this)?d(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream")):vt(e)?d(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream")):ir(this,e,r.preventClose,r.preventAbort,r.preventCancel,r.signal)}tee(){if(!Er(this))throw jr("tee");return ne(yr(this))}values(e=void 0){if(!Er(this))throw jr("values");return function(e,t){const r=H(e),o=new he(r,t),n=Object.create(me);return n._asyncIteratorImpl=o,n}(this,function(e,t){L(e,t);const r=null==e?void 0:e.preventCancel;return{preventCancel:Boolean(r)}}(e,"First parameter").preventCancel)}[de](e){return this.values(e)}static from(e){return Sr(e)}}function Cr(e,t,r,o=1,n=(()=>1)){const a=Object.create(ReadableStream.prototype);qr(a);return _r(a,Object.create(ReadableStreamDefaultController.prototype),e,t,r,o,n),a}function Pr(e,t,r){const o=Object.create(ReadableStream.prototype);qr(o);return Xe(o,Object.create(ReadableByteStreamController.prototype),e,t,r,0,void 0),o}function qr(e){e._state="readable",e._reader=void 0,e._storedError=void 0,e._disturbed=!1}function Er(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readableStreamController")&&e instanceof ReadableStream)}function Wr(e){return void 0!==e._reader}function Or(t,r){if(t._disturbed=!0,"closed"===t._state)return c(void 0);if("errored"===t._state)return d(t._storedError);Br(t);const o=t._reader;if(void 0!==o&&nt(o)){const e=o._readIntoRequests;o._readIntoRequests=new v,e.forEach((e=>{e._closeSteps(void 0)}))}return _(t._readableStreamController[T](r),e)}function Br(e){e._state="closed";const t=e._reader;if(void 0!==t&&(A(t),J(t))){const e=t._readRequests;t._readRequests=new v,e.forEach((e=>{e._closeSteps()}))}}function kr(e,t){e._state="errored",e._storedError=t;const r=e._reader;void 0!==r&&(j(r,t),J(r)?Z(r,t):it(r,t))}function jr(e){return new TypeError(`ReadableStream.prototype.${e} can only be used on a ReadableStream`)}function Ar(e,t){L(e,t);const r=null==e?void 0:e.highWaterMark;return M(r,"highWaterMark","QueuingStrategyInit"),{highWaterMark:Y(r)}}Object.defineProperties(ReadableStream,{from:{enumerable:!0}}),Object.defineProperties(ReadableStream.prototype,{cancel:{enumerable:!0},getReader:{enumerable:!0},pipeThrough:{enumerable:!0},pipeTo:{enumerable:!0},tee:{enumerable:!0},values:{enumerable:!0},locked:{enumerable:!0}}),o(ReadableStream.from,"from"),o(ReadableStream.prototype.cancel,"cancel"),o(ReadableStream.prototype.getReader,"getReader"),o(ReadableStream.prototype.pipeThrough,"pipeThrough"),o(ReadableStream.prototype.pipeTo,"pipeTo"),o(ReadableStream.prototype.tee,"tee"),o(ReadableStream.prototype.values,"values"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(ReadableStream.prototype,Symbol.toStringTag,{value:"ReadableStream",configurable:!0}),Object.defineProperty(ReadableStream.prototype,de,{value:ReadableStream.prototype.values,writable:!0,configurable:!0});const zr=e=>e.byteLength;o(zr,"size");class ByteLengthQueuingStrategy{constructor(e){$(e,1,"ByteLengthQueuingStrategy"),e=Ar(e,"First parameter"),this._byteLengthQueuingStrategyHighWaterMark=e.highWaterMark}get highWaterMark(){if(!Lr(this))throw Dr("highWaterMark");return this._byteLengthQueuingStrategyHighWaterMark}get size(){if(!Lr(this))throw Dr("size");return zr}}function Dr(e){return new TypeError(`ByteLengthQueuingStrategy.prototype.${e} can only be used on a ByteLengthQueuingStrategy`)}function Lr(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_byteLengthQueuingStrategyHighWaterMark")&&e instanceof ByteLengthQueuingStrategy)}Object.defineProperties(ByteLengthQueuingStrategy.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(ByteLengthQueuingStrategy.prototype,Symbol.toStringTag,{value:"ByteLengthQueuingStrategy",configurable:!0});const Fr=()=>1;o(Fr,"size");class CountQueuingStrategy{constructor(e){$(e,1,"CountQueuingStrategy"),e=Ar(e,"First parameter"),this._countQueuingStrategyHighWaterMark=e.highWaterMark}get highWaterMark(){if(!$r(this))throw Ir("highWaterMark");return this._countQueuingStrategyHighWaterMark}get size(){if(!$r(this))throw Ir("size");return Fr}}function Ir(e){return new TypeError(`CountQueuingStrategy.prototype.${e} can only be used on a CountQueuingStrategy`)}function $r(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_countQueuingStrategyHighWaterMark")&&e instanceof CountQueuingStrategy)}function Mr(e,t,r){return F(e,r),r=>g(e,t,[r])}function Yr(e,t,r){return F(e,r),r=>S(e,t,[r])}function xr(e,t,r){return F(e,r),(r,o)=>g(e,t,[r,o])}function Qr(e,t,r){return F(e,r),r=>g(e,t,[r])}Object.defineProperties(CountQueuingStrategy.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(CountQueuingStrategy.prototype,Symbol.toStringTag,{value:"CountQueuingStrategy",configurable:!0});class TransformStream{constructor(e={},t={},r={}){void 0===e&&(e=null);const o=ct(t,"Second parameter"),n=ct(r,"Third parameter"),a=function(e,t){L(e,t);const r=null==e?void 0:e.cancel,o=null==e?void 0:e.flush,n=null==e?void 0:e.readableType,a=null==e?void 0:e.start,i=null==e?void 0:e.transform,l=null==e?void 0:e.writableType;return{cancel:void 0===r?void 0:Qr(r,e,`${t} has member 'cancel' that`),flush:void 0===o?void 0:Mr(o,e,`${t} has member 'flush' that`),readableType:n,start:void 0===a?void 0:Yr(a,e,`${t} has member 'start' that`),transform:void 0===i?void 0:xr(i,e,`${t} has member 'transform' that`),writableType:l}}(e,"First parameter");if(void 0!==a.readableType)throw new RangeError("Invalid readableType specified");if(void 0!==a.writableType)throw new RangeError("Invalid writableType specified");const i=st(n,0),l=ut(n),s=st(o,1),f=ut(o);let h;!function(e,t,r,o,n,a){function i(){return t}function l(t){return function(e,t){const r=e._transformStreamController;if(e._backpressure){return _(e._backpressureChangePromise,(()=>{const o=e._writable;if("erroring"===o._state)throw o._storedError;return Zr(r,t)}))}return Zr(r,t)}(e,t)}function s(t){return function(e,t){const r=e._transformStreamController;if(void 0!==r._finishPromise)return r._finishPromise;const o=e._readable;r._finishPromise=u(((e,t)=>{r._finishPromise_resolve=e,r._finishPromise_reject=t}));const n=r._cancelAlgorithm(t);return Jr(r),b(n,(()=>("errored"===o._state?ro(r,o._storedError):(br(o._readableStreamController,t),to(r)),null)),(e=>(br(o._readableStreamController,e),ro(r,e),null))),r._finishPromise}(e,t)}function c(){return function(e){const t=e._transformStreamController;if(void 0!==t._finishPromise)return t._finishPromise;const r=e._readable;t._finishPromise=u(((e,r)=>{t._finishPromise_resolve=e,t._finishPromise_reject=r}));const o=t._flushAlgorithm();return Jr(t),b(o,(()=>("errored"===r._state?ro(t,r._storedError):(dr(r._readableStreamController),to(t)),null)),(e=>(br(r._readableStreamController,e),ro(t,e),null))),t._finishPromise}(e)}function d(){return function(e){return Gr(e,!1),e._backpressureChangePromise}(e)}function f(t){return function(e,t){const r=e._transformStreamController;if(void 0!==r._finishPromise)return r._finishPromise;const o=e._writable;r._finishPromise=u(((e,t)=>{r._finishPromise_resolve=e,r._finishPromise_reject=t}));const n=r._cancelAlgorithm(t);return Jr(r),b(n,(()=>("errored"===o._state?ro(r,o._storedError):(Yt(o._writableStreamController,t),Ur(e),to(r)),null)),(t=>(Yt(o._writableStreamController,t),Ur(e),ro(r,t),null))),r._finishPromise}(e,t)}e._writable=function(e,t,r,o,n=1,a=(()=>1)){const i=Object.create(WritableStream.prototype);return St(i),Ft(i,Object.create(WritableStreamDefaultController.prototype),e,t,r,o,n,a),i}(i,l,c,s,r,o),e._readable=Cr(i,d,f,n,a),e._backpressure=void 0,e._backpressureChangePromise=void 0,e._backpressureChangePromise_resolve=void 0,Gr(e,!0),e._transformStreamController=void 0}(this,u((e=>{h=e})),s,f,i,l),function(e,t){const r=Object.create(TransformStreamDefaultController.prototype);let o,n,a;o=void 0!==t.transform?e=>t.transform(e,r):e=>{try{return Kr(r,e),c(void 0)}catch(e){return d(e)}};n=void 0!==t.flush?()=>t.flush(r):()=>c(void 0);a=void 0!==t.cancel?e=>t.cancel(e):()=>c(void 0);!function(e,t,r,o,n){t._controlledTransformStream=e,e._transformStreamController=t,t._transformAlgorithm=r,t._flushAlgorithm=o,t._cancelAlgorithm=n,t._finishPromise=void 0,t._finishPromise_resolve=void 0,t._finishPromise_reject=void 0}(e,r,o,n,a)}(this,a),void 0!==a.start?h(a.start(this._transformStreamController)):h(void 0)}get readable(){if(!Nr(this))throw oo("readable");return this._readable}get writable(){if(!Nr(this))throw oo("writable");return this._writable}}function Nr(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_transformStreamController")&&e instanceof TransformStream)}function Hr(e,t){br(e._readable._readableStreamController,t),Vr(e,t)}function Vr(e,t){Jr(e._transformStreamController),Yt(e._writable._writableStreamController,t),Ur(e)}function Ur(e){e._backpressure&&Gr(e,!1)}function Gr(e,t){void 0!==e._backpressureChangePromise&&e._backpressureChangePromise_resolve(),e._backpressureChangePromise=u((t=>{e._backpressureChangePromise_resolve=t})),e._backpressure=t}Object.defineProperties(TransformStream.prototype,{readable:{enumerable:!0},writable:{enumerable:!0}}),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(TransformStream.prototype,Symbol.toStringTag,{value:"TransformStream",configurable:!0});class TransformStreamDefaultController{constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!Xr(this))throw eo("desiredSize");return hr(this._controlledTransformStream._readable._readableStreamController)}enqueue(e=void 0){if(!Xr(this))throw eo("enqueue");Kr(this,e)}error(e=void 0){if(!Xr(this))throw eo("error");var t;t=e,Hr(this._controlledTransformStream,t)}terminate(){if(!Xr(this))throw eo("terminate");!function(e){const t=e._controlledTransformStream;dr(t._readable._readableStreamController);const r=new TypeError("TransformStream terminated");Vr(t,r)}(this)}}function Xr(e){return!!t(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledTransformStream")&&e instanceof TransformStreamDefaultController)}function Jr(e){e._transformAlgorithm=void 0,e._flushAlgorithm=void 0,e._cancelAlgorithm=void 0}function Kr(e,t){const r=e._controlledTransformStream,o=r._readable._readableStreamController;if(!mr(o))throw new TypeError("Readable side is not in a state that permits enqueue");try{fr(o,t)}catch(e){throw Vr(r,e),r._readable._storedError}const n=function(e){return!ur(e)}(o);n!==r._backpressure&&Gr(r,!0)}function Zr(e,t){return _(e._transformAlgorithm(t),void 0,(t=>{throw Hr(e._controlledTransformStream,t),t}))}function eo(e){return new TypeError(`TransformStreamDefaultController.prototype.${e} can only be used on a TransformStreamDefaultController`)}function to(e){void 0!==e._finishPromise_resolve&&(e._finishPromise_resolve(),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}function ro(e,t){void 0!==e._finishPromise_reject&&(p(e._finishPromise),e._finishPromise_reject(t),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}function oo(e){return new TypeError(`TransformStream.prototype.${e} can only be used on a TransformStream`)}Object.defineProperties(TransformStreamDefaultController.prototype,{enqueue:{enumerable:!0},error:{enumerable:!0},terminate:{enumerable:!0},desiredSize:{enumerable:!0}}),o(TransformStreamDefaultController.prototype.enqueue,"enqueue"),o(TransformStreamDefaultController.prototype.error,"error"),o(TransformStreamDefaultController.prototype.terminate,"terminate"),"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(TransformStreamDefaultController.prototype,Symbol.toStringTag,{value:"TransformStreamDefaultController",configurable:!0});export{ByteLengthQueuingStrategy,CountQueuingStrategy,ReadableByteStreamController,ReadableStream,ReadableStreamBYOBReader,ReadableStreamBYOBRequest,ReadableStreamDefaultController,ReadableStreamDefaultReader,TransformStream,TransformStreamDefaultController,WritableStream,WritableStreamDefaultController,WritableStreamDefaultWriter}; diff --git a/api/internal/symbols.js b/api/internal/symbols.js new file mode 100644 index 0000000000..9b82e21acc --- /dev/null +++ b/api/internal/symbols.js @@ -0,0 +1,7 @@ +export const dispose = Symbol.dispose ?? Symbol.for('socket.runtime.gc.finalizer') +export const serialize = Symbol.serialize ?? Symbol.for('socket.runtime.serialize') + +export default { + dispose, + serialize +} diff --git a/api/internal/timers.js b/api/internal/timers.js new file mode 100644 index 0000000000..0c8ea1929a --- /dev/null +++ b/api/internal/timers.js @@ -0,0 +1,35 @@ +import { Timeout, Interval, Immediate } from '../timers/timer.js' +import { platform } from '../timers.js' + +export function setTimeout (callback, ...args) { + return Timeout.from(callback, ...args).id +} + +export function clearTimeout (timeout) { + return platform.clearTimeout(timeout) +} + +export function setInterval (callback, ...args) { + return Interval.from(callback, ...args).id +} + +export function clearInterval (interval) { + return platform.clearInterval(interval) +} + +export function setImmediate (callback, ...args) { + return Immediate.from(callback, ...args).id +} + +export function clearImmediate (immediate) { + return platform.clearTimeout(immediate) +} + +export default { + setTimeout, + setInterval, + setImmediate, + clearTimeout, + clearInterval, + clearImmediate +} diff --git a/api/internal/webassembly.js b/api/internal/webassembly.js index 82a0ef3781..871794f1e2 100644 --- a/api/internal/webassembly.js +++ b/api/internal/webassembly.js @@ -1,4 +1,4 @@ -/* global __native_Response */ +/* global __native_Response, __native_ReadableStream */ /** * The native platform `WebAssembly.instantiateStreaming()` function. * @ignore @@ -36,10 +36,19 @@ export async function instantiateStreaming (response, importObject = undefined) } const stream = await (result.body || result._bodyInit) - const nativeResponse = new __native_Response(stream, { - statusText: response.statsText, - headers: response.headers, - status: response.status + const nativeStream = new __native_ReadableStream({ + async start (controller) { + for await (const chunk of stream) { + controller.enqueue(new Uint8Array(chunk)) + } + controller.close() + } + }) + + const nativeResponse = new __native_Response(nativeStream, { + statusText: result.statusText, + headers: result.headers, + status: result.status }) return nativeInstantiateStreaming(nativeResponse, importObject) diff --git a/api/internal/worker.js b/api/internal/worker.js index 224644c590..dcada6ccf7 100644 --- a/api/internal/worker.js +++ b/api/internal/worker.js @@ -1,7 +1,6 @@ -/* global reportError, EventTarget, CustomEvent */ -import './init.js' - +/* global reportError, EventTarget, CustomEvent, MessageEvent, ApplicationURLEvent */ import { rand64 } from '../crypto.js' +import { Loader } from '../commonjs/loader.js' import globals from './globals.js' import hooks from '../hooks.js' import ipc from '../ipc.js' @@ -17,9 +16,6 @@ const ports = [] // level 1 Worker `EventTarget` 'message' listener let onmessage = null -// level 1 Worker `EventTarget` 'connect' listener -let onconnect = null - // 'close' state for a polyfilled `SharedWorker` let isClosed = false @@ -95,8 +91,8 @@ if (source && typeof source === 'string') { // @ts-ignore await import(source) if (Array.isArray(globalThis.RUNTIME_WORKER_MESSAGE_EVENT_BACKLOG)) { - for (const message of globalThis.RUNTIME_WORKER_MESSAGE_EVENT_BACKLOG) { - globalThis.dispatchEvent(message) + for (const event of globalThis.RUNTIME_WORKER_MESSAGE_EVENT_BACKLOG) { + globalThis.dispatchEvent(new MessageEvent(event.type, event)) } globalThis.RUNTIME_WORKER_MESSAGE_EVENT_BACKLOG.splice( @@ -104,6 +100,7 @@ if (source && typeof source === 'string') { globalThis.RUNTIME_WORKER_MESSAGE_EVENT_BACKLOG.length ) } + globalThis.postMessage({ __runtime_worker_init: true }) } catch (err) { reportError(err) } @@ -130,21 +127,6 @@ Object.defineProperties(WorkerGlobalScopePrototype, { } }, - onconnect: { - configurable: false, - get: () => onconnect, - set: (value) => { - if (typeof onconnect === 'function') { - workerGlobalScopeEventTarget.removeEventListener('connect', onconnect) - } - - if (value === null || typeof value === 'function') { - onconnect = value - workerGlobalScopeEventTarget.addEventListener('connect', onconnect) - } - } - }, - close: { configurable: false, enumerable: false, @@ -173,6 +155,12 @@ Object.defineProperties(WorkerGlobalScopePrototype, { configurable: false, enumerable: false, value: postMessage + }, + + importScripts: { + configurable: false, + enumerable: false, + value: importScripts } }) @@ -196,8 +184,12 @@ export async function onWorkerMessage (event) { return false } else if (typeof data?.__runtime_worker_event === 'object') { const { type, detail } = data?.__runtime_worker_event || {} - if (type) { + if (type === 'applicationurl') { + globalThis.dispatchEvent(new ApplicationURLEvent(type, detail)) + } else if (type && detail) { globalThis.dispatchEvent(new CustomEvent(type, { detail })) + } else if (type) { + globalThis.dispatchEvent(new Event(type)) } event.stopImmediatePropagation() return false @@ -208,22 +200,16 @@ export async function onWorkerMessage (event) { } } - workerGlobalScopeEventTarget.dispatchEvent(new MessageEvent('connect', { - ...data?.__runtime_shared_worker - })) - event.stopImmediatePropagation() return false } - return dispatchEvent(event) + return dispatchEvent(new MessageEvent(event.type, event)) } export function addEventListener (eventName, callback, ...args) { if (eventName === 'message') { return workerGlobalScopeEventTarget.addEventListener(eventName, callback, ...args) - } else if (eventName === 'connect') { - return workerGlobalScopeEventTarget.addEventListener(eventName, callback, ...args) } else { return worker.addEventListener(eventName, callback, ...args) } @@ -232,18 +218,17 @@ export function addEventListener (eventName, callback, ...args) { export function removeEventListener (eventName, callback, ...args) { if (eventName === 'message') { return workerGlobalScopeEventTarget.removeEventListener(eventName, callback, ...args) - } else if (eventName === 'connect') { - return workerGlobalScopeEventTarget.removeEventListener(eventName, callback, ...args) } else { return worker.removeEventListener(eventName, callback, ...args) } } export function dispatchEvent (event) { - if (hooks.globalEvents.includes(event.type)) { - return worker.dispatchEvent(event) + if (event.type === 'message') { + return workerGlobalScopeEventTarget.dispatchEvent(event) } - return workerGlobalScopeEventTarget.dispatchEvent(event) + + return worker.dispatchEvent(event) } export function postMessage (message, ...args) { @@ -266,10 +251,22 @@ export function close () { return worker.close() } +export function importScripts (...scripts) { + const loader = new Loader(source) + for (const script of scripts) { + const { text, ok } = loader.load(script) + if (ok && text) { + // eslint-disable-next-line + eval(text) + } + } +} + export default { RUNTIME_WORKER_ID, removeEventListener, addEventListener, + importScripts, dispatchEvent, postMessage, source, diff --git a/api/ip.js b/api/ip.js index 596a94c6c3..04ea587193 100644 --- a/api/ip.js +++ b/api/ip.js @@ -1,5 +1,5 @@ /** - * @module IP + * @module ip * * Various functions for working with IP addresses. * diff --git a/api/ipc.js b/api/ipc.js index 4dd0058d27..456846feca 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -1,5 +1,5 @@ /** - * @module IPC + * @module ipc * * This is a low-level API that you don't need unless you are implementing * a library on top of Socket runtime. A Socket app has one or more processes. @@ -33,10 +33,11 @@ * ``` */ -/* global webkit, chrome, external */ +/* global webkit, chrome, external, reportError */ import { AbortError, InternalError, + ErrnoError, TimeoutError } from './errors.js' @@ -49,20 +50,29 @@ import { parseJSON } from './util.js' +import { URL, protocols } from './url.js' import * as errors from './errors.js' import { Buffer } from './buffer.js' import { rand64 } from './crypto.js' -import { URL } from './url.js' -import console from './console.js' +import bookmarks from './fs/bookmarks.js' +import serialize from './internal/serialize.js' +import location from './location.js' +import gc from './gc.js' let nextSeq = 1 const cache = {} function initializeXHRIntercept () { if (typeof globalThis.XMLHttpRequest !== 'function') return + const patched = Symbol.for('socket.runtime.ipc.XMLHttpRequest.patched') + if (globalThis.XMLHttpRequest.prototype[patched]) { + return + } + + globalThis.XMLHttpRequest.prototype[patched] = true + const { send, open } = globalThis.XMLHttpRequest.prototype - const B5_PREFIX_BUFFER = new Uint8Array([0x62, 0x35]) // literally, 'b5' const encoder = new TextEncoder() Object.assign(globalThis.XMLHttpRequest.prototype, { open (method, url, ...args) { @@ -70,97 +80,39 @@ function initializeXHRIntercept () { this.readyState = globalThis.XMLHttpRequest.OPENED } catch (_) {} this.method = method - this.url = new URL(url) + this.url = new URL(url, location.origin) this.seq = this.url.searchParams.get('seq') return open.call(this, method, url, ...args) }, async send (body) { - const { method, seq, url } = this - const index = globalThis.__args?.index || 0 + let { method, seq, url } = this - if (url?.protocol === 'ipc:') { + if ( + url?.protocol && ( + url.protocol === 'ipc:' || + protocols.handlers.has(url.protocol.slice(0, -1)) + ) + ) { if ( - /put|post/i.test(method) && - typeof body !== 'undefined' && - typeof seq !== 'undefined' + /put|post|patch/i.test(method) && + typeof body !== 'undefined' ) { if (typeof body === 'string') { body = encoder.encode(body) } if (/android/i.test(primordials.platform)) { - await postMessage(`ipc://buffer.map?seq=${seq}`, body) - body = null - } - - if (/win32/i.test(primordials.platform) && body) { - // 1. send `ipc://buffer.create` - // - The native side should create a shared buffer for `index` and `seq` pair of `size` bytes - // - `index` is the target window - // - `seq` is the sequence is used to know how to return the value to the sender - // 2. wait for 'sharedbufferreceived' event - // - The webview will wait for this event on `window` - // - The event should include "additional data" that is JSON and includes the `index` and `seq` values - // 3. filter on `index` and `seq` for this request - // - The webview will filter on the `index` and `seq` values before calling `getBuffer()` - // 4. write `body` to _shared_ `buffer` - // - The webview should write all bytes to the buffer - // 5. resolve promise - // - After promise resolution, the XHR request will continue - // - The native side should look up the shared buffer for the `index` and `seq` values and use it - // as the bytes for the request when routing the IPC request through the bridge router - // - The native side should release the shared buffer - // size here assumes latin1 encoding. - await postMessage(`ipc://buffer.create?index=${index}&seq=${seq}&size=${body.length}`) - await new Promise((resolve) => { - globalThis.chrome.webview - .addEventListener('sharedbufferreceived', function onSharedBufferReceived (event) { - const { additionalData } = event - if (additionalData.index === index && additionalData.seq === seq) { - const buffer = new Uint8Array(event.getBuffer()) - buffer.set(body) - globalThis.chrome.webview.removeEventListener('sharedbufferreceived', onSharedBufferReceived) - resolve() - } - }) - }) - } - - if (/linux/i.test(primordials.platform)) { - if (body?.buffer instanceof ArrayBuffer) { - const header = new Uint8Array(24) - const buffer = new Uint8Array( - B5_PREFIX_BUFFER.length + - header.length + - body.length - ) - - header.set(encoder.encode(index)) - header.set(encoder.encode(seq), 4) - - // <type> | <header> | <body> - // "b5"(2) | index(2) + seq(2) | body(n) - buffer.set(B5_PREFIX_BUFFER) - buffer.set(header, B5_PREFIX_BUFFER.length) - buffer.set(body, B5_PREFIX_BUFFER.length + header.length) - - let data = [] - const quota = 64 * 1024 - for (let i = 0; i < buffer.length; i += quota) { - data.push(String.fromCharCode(...buffer.subarray(i, i + quota))) - } - - data = data.join('') - - try { - // @ts-ignore - data = decodeURIComponent(escape(data)) - } catch (_) {} - await postMessage(data) + if (!seq) { + seq = 'R' + Math.random().toString().slice(2, 8) + 'X' } + this.setRequestHeader('runtime-xhr-seq', seq) + await postMessage(`ipc://buffer.map?seq=${seq}`, body) + if (!globalThis.window && globalThis.self) { + await new Promise((resolve) => setTimeout(resolve, 200)) + } body = null } } @@ -171,11 +123,12 @@ function initializeXHRIntercept () { }) } -function getErrorClass (type, fallback) { +function getErrorClass (type, fallback = null) { if (typeof globalThis !== 'undefined' && typeof globalThis[type] === 'function') { // eslint-disable-next-line return new Function(`return function ${type} () { const object = Object.create(globalThis['${type}']?.prototype ?? {}, { + message: { enumerable: true, configurable: true, writable: true, value: null }, code: { value: null } }) @@ -273,6 +226,7 @@ function getRequestResponse (request, options) { } const { status, responseURL, statusText } = request + // @ts-ignore const message = Message.from(responseURL) const source = message.command @@ -295,6 +249,35 @@ function getRequestResponse (request, options) { return response } +function getFileSystemBookmarkName (options) { + const names = [ + options.params.get('src'), + options.params.get('path'), + options.params.get('value') + ] + return names.find(Boolean) ?? null +} + +function isFileSystemBookmark (options) { + const names = [ + options.params.get('src'), + options.params.get('path'), + options.params.get('value') + ].filter(Boolean) + + if (names.some((name) => bookmarks.temporary.has(name))) { + return true + } + + for (const [, fd] of bookmarks.temporary.entries()) { + if (fd === options.params.get('id') || fd === options.params.get('fd')) { + return true + } + } + + return false +} + export function maybeMakeError (error, caller) { const errors = { AbortError: getErrorClass('AbortError'), @@ -303,6 +286,7 @@ export function maybeMakeError (error, caller) { GeolocationPositionError: getErrorClass('GeolocationPositionError'), IndexSizeError: getErrorClass('IndexSizeError'), InternalError, + DOMException: getErrorClass('DOMContentLoaded'), InvalidAccessError: getErrorClass('InvalidAccessError'), NetworkError: getErrorClass('NetworkError'), NotAllowedError: getErrorClass('NotAllowedError'), @@ -330,8 +314,10 @@ export function maybeMakeError (error, caller) { delete error.type - if (type in errors) { - err = new errors[type](error.message || '') + if (code && ErrnoError.errno?.constants && -code in ErrnoError.errno.strings) { + err = new ErrnoError(-code) + } else if (type in errors) { + err = new errors[type](error.message || '', error.code) } else { for (const E of Object.values(errors)) { if ((E.code && type === E.code) || (code && code === E.code)) { @@ -353,9 +339,11 @@ export function maybeMakeError (error, caller) { } if ( + // @ts-ignore typeof Error.captureStackTrace === 'function' && typeof caller === 'function' ) { + // @ts-ignore Error.captureStackTrace(err, caller) } @@ -384,7 +372,7 @@ export const TIMEOUT = 32 * 1000 * Symbol for the `ipc.debug.enabled` property * @ignore */ -export const kDebugEnabled = Symbol.for('ipc.debug.enabled') +export const kDebugEnabled = Symbol.for('socket.runtime.ipc.debug.enabled') /** * Parses `seq` as integer value @@ -414,6 +402,10 @@ export function debug (enable) { return debug.enabled } +/** + * @type {boolean} + */ +debug.enabled = false Object.defineProperty(debug, 'enabled', { enumerable: false, set (value) { @@ -444,18 +436,25 @@ export class Headers extends globalThis.Headers { * @ignore */ static from (input) { - if (input?.headers) return this.from(input.headers) + if (input?.headers && typeof input.headers === 'object') { + input = input.headers + } - if (typeof input?.entries === 'function') { - return new this(input.entries()) + if (Array.isArray(input) && !Array.isArray(input[0])) { + input = input.join('\n') + } else if (typeof input?.entries === 'function') { + // @ts-ignore + return new this(Array.from(input.entries())) } else if (isPlainObject(input) || isArrayLike(input)) { return new this(input) } else if (typeof input?.getAllResponseHeaders === 'function') { input = input.getAllResponseHeaders() } else if (typeof input?.headers?.entries === 'function') { - return new this(input.headers.entries()) + // @ts-ignore + return new this(Array.from(input.headers.entries())) } + // @ts-ignore return new this(parseHeaders(String(input))) } @@ -474,27 +473,62 @@ export class Headers extends globalThis.Headers { } } +/** + * Find transfers for an in worker global `postMessage` + * that is proxied to the main thread. + * @ignore + */ +export function findMessageTransfers (transfers, object) { + if (ArrayBuffer.isView(object)) { + add(object.buffer) + } else if (object instanceof ArrayBuffer) { + add(object) + } else if (Array.isArray(object)) { + for (const value of object) { + findMessageTransfers(transfers, value) + } + } else if (object && typeof object === 'object') { + for (const key in object) { + findMessageTransfers(transfers, object[key]) + } + } + + return transfers + + function add (value) { + if (!transfers.includes(value)) { + transfers.push(value) + } + } +} + /** * @ignore */ -export async function postMessage (message, ...args) { +export function postMessage (message, ...args) { if (globalThis?.webkit?.messageHandlers?.external?.postMessage) { + // @ts-ignore return webkit.messageHandlers.external.postMessage(message, ...args) } else if (globalThis?.chrome?.webview?.postMessage) { + // @ts-ignore return chrome.webview.postMessage(message, ...args) + // @ts-ignore } else if (globalThis?.external?.postMessage) { + // @ts-ignore return external.postMessage(message, ...args) } else if (globalThis.postMessage) { + const transfer = [] + findMessageTransfers(transfer, args) // worker if (globalThis.self && !globalThis.window) { - return await globalThis?.postMessage({ + return globalThis?.postMessage({ __runtime_worker_ipc_request: { message, bytes: args[0] ?? null } - }) + }, { transfer }) } else { - return globalThis?.postMessage(message, args) + return globalThis.top.postMessage(message, ...args) } } @@ -747,11 +781,11 @@ export class Message extends URL { /** * Get a parameter value by `key`. * @param {string} key - * @param {any} defaultValue + * @param {any=} [defaultValue] * @return {any} * @ignore */ - get (key, defaultValue) { + get (key, defaultValue = undefined) { if (!this.has(key)) { return defaultValue } @@ -891,7 +925,7 @@ export class Result { const id = result?.id || null const err = maybeMakeError(result?.err || maybeError || null, Result.from) const data = !err && result?.data !== null && result?.data !== undefined - ? result.data?.data ?? result.data + ? result.data : (!err && !id && !result?.source ? result?.err ?? result : null) const source = result?.source || maybeSource || null @@ -1000,8 +1034,9 @@ export async function ready () { function loop () { // this can hang on android. Give it some time because emulators can be slow. if (Date.now() - startReady > 10000) { - reject(new Error('failed to resolve globalThis.__args')) + reject(new Error('Failed to resolve globalThis.__args')) } else if (globalThis.__args) { + // @ts-ignore queueMicrotask(() => resolve()) } else { queueMicrotask(loop) @@ -1012,8 +1047,8 @@ export async function ready () { const { toString } = Object.prototype -class IPCSearchParams extends URLSearchParams { - constructor (params, nonce) { +export class IPCSearchParams extends URLSearchParams { + constructor (params, nonce = null) { let value if (params !== undefined && toString.call(params) !== '[object Object]') { value = params @@ -1023,7 +1058,7 @@ class IPCSearchParams extends URLSearchParams { super({ ...params, index: globalThis.__args?.index ?? 0, - seq: 'R' + nextSeq++ + seq: params?.seq ?? ('R' + nextSeq++) }) if (value !== undefined) { @@ -1037,6 +1072,37 @@ class IPCSearchParams extends URLSearchParams { if (globalThis.RUNTIME_WORKER_ID) { this.set('runtime-worker-id', globalThis.RUNTIME_WORKER_ID) } + + if (globalThis.RUNTIME_WORKER_LOCATION) { + this.set('runtime-worker-location', globalThis.RUNTIME_WORKER_LOCATION) + } + + const runtimeFrameSource = globalThis.document + // @ts-ignore + ? globalThis.document.querySelector('meta[name=runtime-frame-source]')?.content + : '' + + // @ts-ignore + if (globalThis.top && globalThis.top !== globalThis) { + this.set('runtime-frame-type', 'nested') + } else if (!globalThis.window && globalThis.self === globalThis) { + this.set('runtime-frame-type', 'worker') + if ( + globalThis.isServiceWorkerScope || + (globalThis.clients && globalThis.FetchEvent) || + globalThis.RUNTIME_WORKER_TYPE === 'serviceWorker' + ) { + this.set('runtime-worker-type', 'serviceworker') + } else { + this.set('runtime-worker-type', 'worker') + } + } else { + this.set('runtime-frame-type', 'top-level') + } + + if (runtimeFrameSource) { + this.set('runtime-frame-source', runtimeFrameSource) + } } toString () { @@ -1053,7 +1119,7 @@ class IPCSearchParams extends URLSearchParams { * @return {Result} * @ignore */ -export function sendSync (command, value, options = {}) { +export function sendSync (command, value = '', options = null, buffer = null) { if (!globalThis.XMLHttpRequest) { const err = new Error('XMLHttpRequest is not supported in environment') return Result.from(err) @@ -1063,17 +1129,59 @@ export function sendSync (command, value, options = {}) { return cache[command] } - const request = new globalThis.XMLHttpRequest() const params = new IPCSearchParams(value, Date.now()) + params.set('__sync__', 'true') const uri = `ipc://${command}?${params}` + if ( + typeof globalThis.__global_ipc_extension_handler === 'function' && + (options?.useExtensionIPCIfAvailable || command.startsWith('fs.')) + ) { + // eslint-disable-next-line + do { + if (command.startsWith('fs.')) { + if (isFileSystemBookmark({ params })) { + break + } + } + + let response = null + try { + response = globalThis.__global_ipc_extension_handler(uri) + } catch (err) { + return Result.from(null, err) + } + + if (typeof response === 'string') { + try { + response = JSON.parse(response) + } catch {} + } + + return Result.from(response, null, command) + } while (0) + } + + const request = new globalThis.XMLHttpRequest() + if (debug.enabled) { debug.log('ipc.sendSync: %s', uri) } - request.responseType = options?.responseType ?? '' - request.open('GET', uri, false) - request.send() + if (options?.responseType && typeof primordials !== 'undefined') { + if (!(/android/i.test(primordials.platform) && globalThis.document)) { + // @ts-ignore + request.responseType = options.responseType + } + } + + if (buffer) { + request.open('POST', uri, false) + request.send(buffer) + } else { + request.open('GET', uri, false) + request.send() + } const response = getRequestResponse(request, options) const headers = request.getAllResponseHeaders() @@ -1087,6 +1195,16 @@ export function sendSync (command, value, options = {}) { cache[command] = result } + if (command.startsWith('fs.') && isFileSystemBookmark({ params })) { + if (!result.err) { + const id = result.data?.id ?? result.data?.fd + const name = getFileSystemBookmarkName({ params }) + if (id && name) { + bookmarks.temporary.set(name, id) + } + } + } + return result } @@ -1156,7 +1274,7 @@ export async function resolve (seq, value) { * @param {boolean=} [options.bytes=false] * @return {Promise<Result>} */ -export async function send (command, value, options) { +export async function send (command, value, options = null) { await ready() if (options?.cache === true && cache[command]) { @@ -1170,6 +1288,35 @@ export async function send (command, value, options) { const params = new IPCSearchParams(value) const uri = `ipc://${command}?${params}` + if ( + typeof globalThis.__global_ipc_extension_handler === 'function' && + (options?.useExtensionIPCIfAvailable || command.startsWith('fs.')) + ) { + // eslint-disable-next-line + do { + if (command.startsWith('fs.')) { + if (isFileSystemBookmark({ params })) { + break + } + } + + let response = null + try { + response = await globalThis.__global_ipc_extension_handler(uri) + } catch (err) { + return Result.from(null, err) + } + + if (typeof response === 'string') { + try { + response = JSON.parse(response) + } catch {} + } + + return Result.from(response, null, command) + } while (0) + } + if (options?.bytes) { postMessage(uri, options.bytes) } else { @@ -1189,6 +1336,16 @@ export async function send (command, value, options) { cache[command] = result } + if (command.startsWith('fs.') && isFileSystemBookmark({ params })) { + if (!result.err) { + const id = result.data?.id ?? result.data?.fd + const name = getFileSystemBookmarkName({ params }) + if (id && name) { + bookmarks.temporary.set(name, id) + } + } + } + resolve(result) } }) @@ -1210,11 +1367,41 @@ export async function write (command, value, buffer, options) { await ready() - const signal = options?.signal - const request = new globalThis.XMLHttpRequest() const params = new IPCSearchParams(value, Date.now()) const uri = `ipc://${command}?${params}` + if ( + typeof globalThis.__global_ipc_extension_handler === 'function' && + (options?.useExtensionIPCIfAvailable || command.startsWith('fs.')) + ) { + // eslint-disable-next-line + do { + if (command.startsWith('fs.')) { + if (isFileSystemBookmark({ params })) { + break + } + } + + let response = null + try { + response = await globalThis.__global_ipc_extension_handler(uri, buffer) + } catch (err) { + return Result.from(null, err) + } + + if (typeof response === 'string') { + try { + response = JSON.parse(response) + } catch {} + } + + return Result.from(response, null, command) + } while (0) + } + + const signal = options?.signal + const request = new globalThis.XMLHttpRequest() + let resolved = false let aborted = false let timeout = null @@ -1307,10 +1494,40 @@ export async function request (command, value, options) { await ready() + const params = new IPCSearchParams(value, Date.now()) + const uri = `ipc://${command}?${params}` + + if ( + typeof globalThis.__global_ipc_extension_handler === 'function' && + (options?.useExtensionIPCIfAvailable || command.startsWith('fs.')) + ) { + // eslint-disable-next-line + do { + if (command.startsWith('fs.')) { + if (isFileSystemBookmark({ params })) { + break + } + } + + let response = null + try { + response = await globalThis.__global_ipc_extension_handler(uri) + } catch (err) { + return Result.from(null, err) + } + + if (typeof response === 'string') { + try { + response = JSON.parse(response) + } catch {} + } + + return Result.from(response, null, command) + } while (0) + } + const signal = options?.signal const request = new globalThis.XMLHttpRequest() - const params = new IPCSearchParams(value, Date.now()) - const uri = `ipc://${command}` let resolved = false let aborted = false @@ -1329,14 +1546,12 @@ export async function request (command, value, options) { }) } - const query = `?${params}` - request.responseType = options?.responseType ?? '' - request.open('GET', uri + query) + request.open('GET', uri) request.send(null) if (debug.enabled) { - debug.log('ipc.request:', uri + query) + debug.log('ipc.request:', uri) } return await new Promise((resolve) => { @@ -1377,6 +1592,16 @@ export async function request (command, value, options) { cache[command] = result } + if (command.startsWith('fs.') && isFileSystemBookmark({ params })) { + if (!result.err) { + const id = result.data?.id ?? result.data?.fd + const name = getFileSystemBookmarkName({ params }) + if (id && name) { + bookmarks.temporary.set(name, id) + } + } + } + return resolve(result) } } @@ -1466,19 +1691,405 @@ export const primordials = sendSync('platform.primordials')?.data || {} if (primordials.cwd) { primordials.cwd = primordials.cwd.replace(/\\$/, '') } + +if ( + globalThis.__RUNTIME_PRIMORDIAL_OVERRIDES__ && + typeof globalThis.__RUNTIME_PRIMORDIAL_OVERRIDES__ === 'object' +) { + Object.assign(primordials, globalThis.__RUNTIME_PRIMORDIAL_OVERRIDES__) +} + Object.freeze(primordials) + initializeXHRIntercept() if (typeof globalThis?.window !== 'undefined') { - document.addEventListener('DOMContentLoaded', () => { + if (globalThis.document.readyState === 'complete') { queueMicrotask(async () => { try { await send('platform.event', 'domcontentloaded') } catch (err) { - console.error('ERR:', err) + reportError(err) } }) - }) + } else { + globalThis.document.addEventListener('DOMContentLoaded', () => { + queueMicrotask(async () => { + try { + await send('platform.event', 'domcontentloaded') + } catch (err) { + reportError(err) + } + }) + }) + } +} + +export function inflateIPCMessageTransfers (object, types = new Map()) { + if (ArrayBuffer.isView(object)) { + return object + } else if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + object[i] = inflateIPCMessageTransfers(object[i], types) + } + } else if (object && typeof object === 'object') { + if ('__type__' in object && types.has(object.__type__)) { + const Type = types.get(object.__type__) + if (typeof Type === 'function') { + if (typeof Type.from === 'function') { + return Type.from(object) + } else { + return new Type(object) + } + } + } + + if (object.__type__ === 'IPCMessagePort' && object.id) { + return IPCMessagePort.create(object) + } else { + for (const key in object) { + const value = object[key] + object[key] = inflateIPCMessageTransfers(value, types) + } + } + } + + return object +} + +export function findIPCMessageTransfers (transfers, object) { + if (ArrayBuffer.isView(object)) { + add(object.buffer) + } else if (object instanceof ArrayBuffer) { + add(object) + } else if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + object[i] = findIPCMessageTransfers(transfers, object[i]) + } + } else if (object && typeof object === 'object') { + if ( + object instanceof MessagePort || ( + typeof object.postMessage === 'function' && + Object.getPrototypeOf(object).constructor.name === 'MessagePort' + ) + ) { + const port = IPCMessagePort.create(object) + object.addEventListener('message', function onMessage (event) { + if (port.closed === true) { + port.onmessage = null + event.preventDefault() + event.stopImmediatePropagation() + object.removeEventListener('message', onMessage) + return false + } + + port.dispatchEvent(new MessageEvent('message', event)) + }) + + port.onmessage = (event) => { + if (port.closed === true) { + port.onmessage = null + event.preventDefault() + event.stopImmediatePropagation() + return false + } + + const transfers = new Set() + findIPCMessageTransfers(transfers, event.data) + object.postMessage(event.data, { + transfer: Array.from(transfers) + }) + } + add(port) + return port + } else { + for (const key in object) { + object[key] = findIPCMessageTransfers(transfers, object[key]) + } + } + } + + return object + + function add (value) { + if ( + value && + !transfers.has(value) && + !(Symbol.for('socket.runtime.serialize') in value) + ) { + transfers.add(value) + } + } +} + +export const ports = new Map() + +export class IPCMessagePort extends MessagePort { + static from (options = null) { + return this.create(options) + } + + static create (options = null) { + const id = String(options?.id ?? rand64()) + const port = Object.create(this.prototype) + const token = String(rand64()) + const channel = typeof options?.channel === 'string' + ? new BroadcastChannel(options.channel) + : options.channel ?? new BroadcastChannel(id) + + port[Symbol.for('socket.runtime.IPCMessagePort.id')] = id + ports.set(id, Object.create(null, { + id: { writable: true, value: id }, + token: { writable: false, value: token }, + closed: { writable: true, value: false }, + started: { writable: true, value: false }, + channel: { writable: true, value: channel }, + onmessage: { writable: true, value: null }, + onmessageerror: { writable: true, value: null }, + eventTarget: { writable: true, value: new EventTarget() } + })) + + channel.addEventListener('message', function onMessage (event) { + const state = ports.get(id) + + if (!state || state?.closed === true) { + event.preventDefault() + event.stopImmediatePropagation() + channel.removeEventListener('message', onMessage) + return false + } + + if (state?.started && event.data?.token !== state.token) { + port.dispatchEvent(new MessageEvent('message', { + ...event, + data: event.data?.data + })) + } + }) + + gc.ref(port) + return port + } + + get id () { + return this[Symbol.for('socket.runtime.IPCMessagePort.id')] ?? null + } + + get started () { + if (!ports.has(this.id)) { + return false + } + + return ports.get(this.id)?.started ?? false + } + + get closed () { + if (!ports.has(this.id)) { + return true + } + + return ports.get(this.id)?.closed ?? false + } + + get onmessage () { + return ports.get(this.id)?.onmessage ?? null + } + + set onmessage (onmessage) { + const port = ports.get(this.id) + + if (!port) { + return + } + + if (typeof this.onmessage === 'function') { + this.removeEventListener('message', this.onmessage) + port.onmessage = null + } + + if (typeof onmessage === 'function') { + this.addEventListener('message', onmessage) + port.onmessage = onmessage + if (!port.started) { + this.start() + } + } + } + + get onmessageerror () { + return ports.get(this.id)?.onmessageerror ?? null + } + + set onmessageerror (onmessageerror) { + const port = ports.get(this.id) + + if (!port) { + return + } + + if (typeof this.onmessageerror === 'function') { + this.removeEventListener('messageerror', this.onmessageerror) + port.onmessageerror = null + } + + if (typeof onmessageerror === 'function') { + this.addEventListener('messageerror', onmessageerror) + port.onmessageerror = onmessageerror + } + } + + start () { + const port = ports.get(this.id) + if (port) { + port.started = true + } + } + + close (purge = true) { + const port = ports.get(this.id) + if (port) { + port.closed = true + } + + if (purge) { + ports.delete(this.id) + } + } + + postMessage (message, optionsOrTransferList) { + const port = ports.get(this.id) + const options = { transfer: [] } + + if (!port) { + return + } + + if (Array.isArray(optionsOrTransferList)) { + options.transfer.push(...optionsOrTransferList) + } else if (Array.isArray(optionsOrTransferList?.transfer)) { + options.transfer.push(...optionsOrTransferList.transfer) + } + + const transfers = new Set(options.transfer) + const handle = this[Symbol.for('socket.runtime.ipc.MessagePort.handlePostMessage')] + + const serializedMessage = serialize(findIPCMessageTransfers(transfers, message)) + options.transfer = Array.from(transfers) + + if (typeof handle === 'function') { + if (handle.call(this, serializedMessage, options) === false) { + return + } + } + + port.channel.postMessage({ + token: port.token, + data: serializedMessage + }, options) + } + + addEventListener (...args) { + const eventTarget = ports.get(this.id)?.eventTarget + + if (eventTarget) { + return eventTarget.addEventListener(...args) + } + + return false + } + + removeEventListener (...args) { + const eventTarget = ports.get(this.id)?.eventTarget + + if (eventTarget) { + return eventTarget.removeEventListener(...args) + } + + return false + } + + dispatchEvent (event) { + const eventTarget = ports.get(this.id)?.eventTarget + + if (eventTarget) { + if (event.type === 'message') { + return eventTarget.dispatchEvent(new MessageEvent('message', { + ...event, + data: inflateIPCMessageTransfers(event.data) + })) + } else { + return eventTarget.dispatchEvent(event) + } + } + + return false + } + + [Symbol.for('socket.runtime.ipc.MessagePort.handlePostMessage')] ( + message, + options = null + ) { + return true + } + + [Symbol.for('socket.runtime.serialize')] () { + return { + __type__: 'IPCMessagePort', + channel: ports.get(this.id)?.channel?.name ?? null, + id: this.id + } + } + + [Symbol.for('socket.runtime.gc.finalizer')] () { + return { + args: [this.id], + handle (id) { + ports.delete(id) + } + } + } +} + +export class IPCMessageChannel extends MessageChannel { + #id = null + #port1 = null + #port2 = null + #channel = null + + constructor (options = null) { + super() + this.#id = String(options?.id ?? rand64()) + this.#channel = options?.channel ?? new BroadcastChannel(this.#id) + + this.#port1 = IPCMessagePort.create(options?.port1) + this.#port2 = IPCMessagePort.create(options?.port2) + + this.port1[Symbol.for('socket.runtime.ipc.MessagePort.handlePostMessage')] = (message, options) => { + this.port2.channel.postMessage(message, options) + return false + } + + this.port2[Symbol.for('socket.runtime.ipc.MessagePort.handlePostMessage')] = (message, options) => { + this.port2.channel.postMessage(message, options) + return false + } + } + + get id () { + return this.#id + } + + get port1 () { + return this.#port1 + } + + get port2 () { + return this.#port2 + } + + get channel () { + return this.#channel + } } // eslint-disable-next-line diff --git a/api/language.js b/api/language.js index 331c1502bd..40635b647b 100644 --- a/api/language.js +++ b/api/language.js @@ -1,5 +1,5 @@ /** - * @module Language + * @module language * * A module for querying ISO 639-1 language names and codes and working with * RFC 5646 language tags. @@ -886,7 +886,7 @@ export class LanguageQueryResult { /** * @ignore */ - [Symbol.for('socket.util.inspect.custom')] () { + [Symbol.for('socket.runtime.util.inspect.custom')] () { return this.inspect() } @@ -968,7 +968,7 @@ export class LanguageDescription { /** * @ignore */ - [Symbol.for('socket.util.inspect.custom')] () { + [Symbol.for('socket.runtime.util.inspect.custom')] () { return this.inspect() } diff --git a/api/latica.js b/api/latica.js new file mode 100644 index 0000000000..e033e4e0d0 --- /dev/null +++ b/api/latica.js @@ -0,0 +1,3 @@ +import def from './latica/index.js' +export * from './latica/index.js' +export default def diff --git a/api/latica/api.js b/api/latica/api.js new file mode 100644 index 0000000000..9e40f86012 --- /dev/null +++ b/api/latica/api.js @@ -0,0 +1,398 @@ +import { Peer, Encryption, sha256 } from './index.js' +import { PeerWorkerProxy } from './proxy.js' +import { sodium } from '../crypto.js' +import { Buffer } from '../buffer.js' +import { isBufferLike } from '../util.js' +import { Packet, CACHE_TTL } from './packets.js' + +/** + * Initializes and returns the network bus. + * + * @async + * @function + * @param {object} options - Configuration options for the network bus. + * @param {object} events - A nodejs compatibe implementation of the events module. + * @param {object} dgram - A nodejs compatible implementation of the dgram module. + * @returns {Promise<events.EventEmitter>} - A promise that resolves to the initialized network bus. + */ +async function api (options = {}, events, dgram) { + await sodium.ready + const bus = new events.EventEmitter() + bus._on = bus.on + bus._once = bus.once + bus._emit = bus.emit + + if (!options.indexed) { + if (!options.clusterId && !options.config?.clusterId) { + throw new Error('expected options.clusterId') + } + + if (typeof options.signingKeys !== 'object') throw new Error('expected options.signingKeys to be of type Object') + if (options.signingKeys.publicKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.publicKey to be of type Uint8Array') + if (options.signingKeys.privateKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.privateKey to be of type Uint8Array') + } + + let clusterId = bus.clusterId = options.clusterId || options.config?.clusterId + + if (clusterId) clusterId = Buffer.from(clusterId) // some peers don't have clusters + + let useWorker = globalThis.isSocketRuntime + if (options.worker === false) useWorker = false + + const Ctor = useWorker ? PeerWorkerProxy : Peer + const _peer = new Ctor(options, dgram) + + _peer.onJoin = (packet, ...args) => { + packet = Packet.from(packet) + if (packet.clusterId.compare(clusterId) !== 0) return + bus._emit('#join', packet, ...args) + } + + _peer.onPacket = (packet, ...args) => { + packet = Packet.from(packet) + if (packet.clusterId.compare(clusterId) !== 0) return + bus._emit('#packet', packet, ...args) + } + + _peer.onStream = (packet, ...args) => { + packet = Packet.from(packet) + if (packet.clusterId.compare(clusterId) !== 0) return + bus._emit('#stream', packet, ...args) + } + + _peer.onData = (...args) => bus._emit('#data', ...args) + _peer.onDebug = (...args) => bus._emit('#debug', ...args) + _peer.onSend = (...args) => bus._emit('#send', ...args) + _peer.onFirewall = (...args) => bus._emit('#firewall', ...args) + _peer.onMulticast = (...args) => bus._emit('#multicast', ...args) + _peer.onSync = (...args) => bus._emit('#sync', ...args) + _peer.onSyncStart = (...args) => bus._emit('#sync-start', ...args) + _peer.onSyncEnd = (...args) => bus._emit('#sync-end', ...args) + _peer.onDisconnection = (...args) => bus._emit('#disconnection', ...args) + _peer.onQuery = (...args) => bus._emit('#query', ...args) + _peer.onNat = (...args) => bus._emit('#network-change', ...args) + _peer.onWarn = (...args) => bus._emit('#warning', ...args) + _peer.onState = (...args) => bus._emit('#state', ...args) + _peer.onConnecting = (...args) => bus._emit('#connecting', ...args) + _peer.onConnection = (...args) => bus._emit('#connection', ...args) + + // TODO check if its not a network error + _peer.onError = (...args) => bus._emit('#error', ...args) + + _peer.onReady = info => { + Object.assign(_peer, { + isReady: true, + ...info + }) + + bus._emit('#ready', info) + } + + bus.subclusters = new Map() + + /** + * Gets general, read only information of the network peer. + * + * @function + * @returns {object} - The general information. + */ + bus.getInfo = () => _peer.getInfo() + bus.getMetrics = () => _peer.getMetrics() + + /** + * Gets the read only state of the network peer. + * + * @function + * @returns {object} - The address information. + */ + bus.getState = () => _peer.getState() + + /** + * Indexes a new peer in the network. + * + * @function + * @param {object} params - Peer information. + * @param {string} params.peerId - The peer ID. + * @param {string} params.address - The peer address. + * @param {number} params.port - The peer port. + * @throws {Error} - Throws an error if required parameters are missing. + */ + bus.addIndexedPeer = ({ peerId, address, port }) => { + return _peer.addIndexedPeer({ peerId, address, port }) + } + + bus.close = () => _peer.close() + bus.sync = (peerId) => _peer.sync(peerId) + bus.reconnect = () => _peer.reconnect() + bus.disconnect = () => _peer.disconnect() + + bus.sealUnsigned = (m, v = options.signingKeys) => _peer.sealUnsigned(m, v) + bus.openUnsigned = (m, v = options.signingKeys) => _peer.openUnsigned(m, v) + + bus.seal = (m, v = options.signingKeys) => _peer.seal(m, v) + bus.open = (m, v = options.signingKeys) => _peer.open(m, v) + + bus.send = (...args) => _peer.send(...args) + + bus.query = (...args) => _peer.query(...args) + + bus.MAX_CACHE_TTL = CACHE_TTL + + const pack = async (eventName, value, opts = {}) => { + if (typeof eventName !== 'string') throw new Error('event name must be a string') + if (eventName.length === 0) throw new Error('event name too short') + + if (opts.ttl) opts.ttl = Math.min(opts.ttl, CACHE_TTL) + + const args = { + clusterId, + ...opts, + usr1: await sha256(eventName, { bytes: true }) + } + + if (!isBufferLike(value) && typeof value === 'object') { + try { + args.message = Buffer.from(JSON.stringify(value)) + } catch (err) { + return bus._emit('error', err) + } + } else { + args.message = Buffer.from(value) + } + + args.usr2 = Buffer.from(options.signingKeys.publicKey) + args.sig = Encryption.sign(args.message, options.signingKeys.privateKey) + + return args + } + + const unpack = async packet => { + let verified + const scid = Buffer.from(packet.subclusterId).toString('base64') + const sub = bus.subclusters.get(scid) + if (!sub) return {} + + const opened = await _peer.open(packet.message, scid) + + if (!opened) { + sub._emit('unopened', { packet }) + return {} + } + + if (packet.sig) { + try { + if (Encryption.verify(opened, packet.sig, packet.usr2)) { + verified = true + } + } catch (err) { + sub._emit('unverified', packet) + return {} + } + } + + return { opened: Buffer.from(opened), verified } + } + + /** + * Publishes an event to the network bus. + * + * @async + * @function + * @param {string} eventName - The name of the event. + * @param {any} value - The value associated with the event. + * @param {object} opts - Additional options for publishing. + * @returns {Promise<any>} - A promise that resolves to the published event details. + */ + bus.emit = async (eventName, value, opts = {}) => { + const args = await pack(eventName, value, opts) + if (!options.sharedKey) { + throw new Error('Can\'t emit to the top level cluster, a shared key was not provided in the constructor or the arguments options') + } + + return await _peer.publish(options.sharedKey || opts.sharedKey, args) + } + + bus.on = async (eventName, cb) => { + if (eventName[0] !== '#') eventName = await sha256(eventName) + bus._on(eventName, cb) + } + + bus.subcluster = async (options = {}) => { + if (!options.sharedKey?.constructor.name) { + throw new Error('expected options.sharedKey to be of type Uint8Array') + } + + const derivedKeys = await Encryption.createKeyPair(options.sharedKey) + const subclusterId = Buffer.from(derivedKeys.publicKey) + const scid = subclusterId.toString('base64') + + if (bus.subclusters.has(scid)) return bus.subclusters.get(scid) + + const sub = new events.EventEmitter() + sub._emit = sub.emit + sub._on = sub.on + sub.peers = new Map() + + bus.subclusters.set(scid, sub) + + sub.peerId = _peer.peerId + sub.subclusterId = subclusterId + sub.sharedKey = options.sharedKey + sub.derivedKeys = derivedKeys + + sub.stream = async (eventName, value, opts = {}) => { + opts.clusterId = opts.clusterId || clusterId + opts.subclusterId = opts.subclusterId || sub.subclusterId + + const args = await pack(eventName, value, opts) + + let packets + + for (const p of sub.peers.values()) { + const result = await _peer.stream(p.peerId, sub.sharedKey, args) + if (!packets) packets = result + } + return packets + } + + sub.emit = async (eventName, value, opts = {}) => { + opts.clusterId = opts.clusterId || clusterId + opts.subclusterId = opts.subclusterId || sub.subclusterId + + const args = await pack(eventName, value, opts) + + if (sub.peers.values().length) { + let packets = [] + + for (const p of sub.peers.values()) { + const r = await _peer.stream(p.peerId, sub.sharedKey, args) + if (packets.length === 0) packets = r + } + + for (const packet of packets) { + const p = Packet.from(packet) + const pid = packet.packetId.toString('hex') + _peer.cache.insert(pid, p) + + _peer.unpublished[pid] = Date.now() + if (!Peer.onLine()) continue + + _peer.mcast(packet) + } + + const head = packets.find(p => p.index === 0) + + if (_peer.onPacket && head) { // try to emit a single packet + const p = await _peer.cache.compose(head) + _peer.onPacket(p, _peer.port, _peer.address, true) + return [p] + } + + return packets + } else { + const packets = await _peer.publish(sub.sharedKey, args) + return packets + } + } + + sub.on = async (eventName, cb) => { + if (eventName[0] !== '#') eventName = await sha256(eventName) + sub._on(eventName, cb) + } + + sub.off = async (eventName, fn) => { + if (eventName[0] !== '#') eventName = await sha256(eventName) + sub.removeListener(eventName, fn) + } + + sub.join = () => _peer.join(sub.sharedKey, options) + + bus._on('#ready', () => { + const scid = sub.subclusterId.toString('base64') + const subcluster = bus.subclusters.get(scid) + if (subcluster) _peer.join(subcluster.sharedKey, options) + }) + + _peer.join(sub.sharedKey, options) + return sub + } + + bus._on('#join', async (packet, peer) => { + const scid = packet.subclusterId.toString('base64') + const sub = bus.subclusters.get(scid) + if (!sub) return + if (!peer || !peer.peerId) return + + let ee = sub.peers.get(peer.peerId) + + if (!ee) { + ee = new events.EventEmitter() + + ee._on = ee.on + ee._emit = ee.emit + + ee.peerId = peer.peerId + ee.address = peer.address + ee.port = peer.port + + ee.emit = async (eventName, value, opts = {}) => { + opts.clusterId = opts.clusterId || clusterId + opts.subclusterId = opts.subclusterId || sub.subclusterId + + const args = await pack(eventName, value, opts) + return _peer.stream(peer.peerId, sub.sharedKey, args) + } + + ee.on = async (eventName, cb) => { + if (eventName[0] !== '#') eventName = await sha256(eventName) + ee._on(eventName, cb) + } + } + + const oldPeer = sub.peers.has(peer.peerId) + const portChange = oldPeer.port !== peer.port + const addressChange = oldPeer.address !== peer.address + const natChange = oldPeer.natType !== peer.natType + const change = portChange || addressChange || natChange + + ee._peer = peer + + sub.peers.set(peer.peerId, ee) + const isStateChange = !oldPeer || change + + _peer.onDebug(_peer.peerId, `<-- API CONNECTION JOIN (scid=${scid}, peerId=${peer.peerId.slice(0, 6)})`) + + sub._emit('#join', ee, packet, isStateChange) + }) + + const handlePacket = async (packet, peer, port, address) => { + const scid = packet.subclusterId.toString('base64') + const sub = bus.subclusters.get(scid) + if (!sub) return + + const eventName = packet.usr1.toString('hex') + const { verified, opened } = await unpack(packet) + if (verified) packet.verified = true + + sub._emit(eventName, opened, packet) + + const ee = sub.peers.get(packet.streamFrom || peer?.peerId) + if (ee) ee._emit(eventName, opened, packet) + } + + bus._on('#stream', handlePacket) + bus._on('#packet', handlePacket) + + bus._on('#disconnection', peer => { + for (const sub of [...bus.subclusters.values()]) { + sub._emit('#leave', peer) + sub.peers.delete(peer.peerId) + } + }) + + await _peer.init() + return bus +} + +export { api } +export default api diff --git a/api/stream-relay/cache.js b/api/latica/cache.js similarity index 93% rename from api/stream-relay/cache.js rename to api/latica/cache.js index 1893b72a99..fd44690a3a 100644 --- a/api/stream-relay/cache.js +++ b/api/latica/cache.js @@ -3,12 +3,6 @@ import { Buffer } from '../buffer.js' import { createDigest } from '../crypto.js' import { Packet, PacketPublish, PACKET_BYTES, sha256 } from './packets.js' -const EMPTY_CACHE = 'da39a3ee5e6b4b0d3255bfef95601890afd80709' - -export const trim = (/** @type {Buffer} */ buf) => { - return buf.toString().split('~')[0].split('\x00')[0] -} - /** * Tries to convert input to a `Buffer`, if possible, otherwise `null`. * @ignore @@ -89,6 +83,7 @@ export class Cache { maxSize = DEFAULT_MAX_SIZE static HASH_SIZE_BYTES = 20 + static HASH_EMPTY = 'da39a3ee5e6b4b0d3255bfef95601890afd80709' /** * `Cache` class constructor. @@ -142,7 +137,7 @@ export class Cache { // some random and cluster-related. .pop() - this.data.delete(oldest.packetId.toString('hex')) + this.data.delete(Buffer.from(oldest.packetId).toString('hex')) if (this.onEjected) this.onEjected(oldest) } @@ -189,17 +184,19 @@ export class Cache { async compose (packet, source = this.data) { let previous = packet - if (packet?.index > 0) previous = source.get(packet.previousId.toString('hex')) + if (packet?.index > 0) previous = source.get(packet.previousId?.toString('hex')) if (!previous) return null const { meta, size, indexes, ts } = previous.message // follow the chain to get the buffers in order - const bufs = [...source.values()] - .filter(p => Buffer.from(p.previousId).toString('hex') === Buffer.from(previous.packetId).toString('hex')) - .sort((a, b) => a.index - b.index) + let bufs = [...source.values()].filter(p => { + if (!p.previousId) return false + return Buffer.from(p.previousId).compare(Buffer.from(previous.packetId)) === 0 + }) if (!indexes || bufs.length < indexes) return null + bufs = bufs.sort((a, b) => a.index - b.index) // sort after confirming they are all there // concat and then hash, the original should match const messages = bufs.map(p => p.message) @@ -208,7 +205,7 @@ export class Cache { if (!meta.ts) meta.ts = ts // generate a new packet ID - const packetId = await sha256(Buffer.concat([packet.previousId, message]), { bytes: true }) + const packetId = await sha256(Buffer.concat([Buffer.from(packet.previousId || ''), message]), { bytes: true }) return Packet.from({ ...packet, @@ -294,7 +291,7 @@ export class Cache { if (!buckets.every(b => b === null)) { hash = await this.sha1(buckets.join(''), true) } else { - hash = EMPTY_CACHE + hash = Cache.HASH_EMPTY } return { prefix, hash, buckets } @@ -356,6 +353,16 @@ export class Cache { return { prefix, hash, buckets } } + + /** + * Test a summary hash format is valid + * + * @param {string} hash + * @returns boolean + */ + static isValidSummaryHashFormat (hash) { + return typeof hash === 'string' && /^[A-Fa-f0-9]{40}$/.test(hash) + } } export default Cache diff --git a/api/stream-relay/encryption.js b/api/latica/encryption.js similarity index 92% rename from api/stream-relay/encryption.js rename to api/latica/encryption.js index c4b0cdac86..7946496d18 100644 --- a/api/stream-relay/encryption.js +++ b/api/latica/encryption.js @@ -67,6 +67,9 @@ export class Encryption { * @param {Uint8Array} privateKey - Private key. */ add (publicKey, privateKey) { + if (!publicKey) throw new Error('encryption.add expects publicKey') + if (!privateKey) throw new Error('encryption.add expects privateKey') + const to = Buffer.from(publicKey).toString('base64') this.keys[to] = { publicKey, privateKey, ts: Date.now() } } @@ -118,52 +121,64 @@ export class Encryption { } /** - * Decrypts a sealed message for a specific receiver. + * Opens a sealed message using the specified key. * @param {Buffer} message - The sealed message. * @param {Object|string} v - Key object or public key. * @returns {Buffer} - Decrypted message. - * @throws {Error} - Throws ENOKEY if the key is not found, EMALFORMED if the message is malformed, ENOTVERIFIED if the message cannot be verified. + * @throws {Error} - Throws ENOKEY if the key is not found. */ - open (message, v) { + openUnsigned (message, v) { if (typeof v === 'string') v = this.keys[v] if (!v) throw new Error(`ENOKEY (key=${v})`) const pk = toPK(v.publicKey) const sk = toSK(v.privateKey) - const buf = sodium.crypto_box_seal_open(message, pk, sk) - - if (buf.byteLength <= sodium.crypto_sign_BYTES) { - throw new Error('EMALFORMED') - } + return Buffer.from(sodium.crypto_box_seal_open(message, pk, sk)) + } - const sig = buf.subarray(0, sodium.crypto_sign_BYTES) - const ct = buf.subarray(sodium.crypto_sign_BYTES) + sealUnsigned (message, v) { + if (typeof v === 'string') v = this.keys[v] + if (!v) throw new Error(`ENOKEY (key=${v})`) - if (!sodium.crypto_sign_verify_detached(sig, ct, v.publicKey)) { - throw new Error('ENOTVERIFIED') - } + this.add(v.publicKey, v.privateKey) - return Buffer.from(sodium.crypto_box_seal_open(ct, pk, sk)) + const pk = toPK(v.publicKey) + const pt = toUint8Array(message) + const ct = sodium.crypto_box_seal(pt, pk) + return sodium.crypto_box_seal(toUint8Array(ct), pk) } /** - * Opens a sealed message using the specified key. + * Decrypts a sealed and signed message for a specific receiver. * @param {Buffer} message - The sealed message. * @param {Object|string} v - Key object or public key. * @returns {Buffer} - Decrypted message. - * @throws {Error} - Throws ENOKEY if the key is not found. + * @throws {Error} - Throws ENOKEY if the key is not found, EMALFORMED if the message is malformed, ENOTVERIFIED if the message cannot be verified. */ - openMessage (message, v) { + open (message, v) { if (typeof v === 'string') v = this.keys[v] if (!v) throw new Error(`ENOKEY (key=${v})`) const pk = toPK(v.publicKey) const sk = toSK(v.privateKey) - return Buffer.from(sodium.crypto_box_seal_open(message, pk, sk)) + const buf = sodium.crypto_box_seal_open(message, pk, sk) + + if (buf.byteLength <= sodium.crypto_sign_BYTES) { + throw new Error('EMALFORMED') + } + + const sig = buf.subarray(0, sodium.crypto_sign_BYTES) + const ct = buf.subarray(sodium.crypto_sign_BYTES) + + if (!sodium.crypto_sign_verify_detached(sig, ct, v.publicKey)) { + throw new Error('ENOTVERIFIED') + } + + return sodium.crypto_box_seal_open(ct, pk, sk) } /** - * Seals a message for a specific receiver using their public key. + * Seals and signs a message for a specific receiver using their public key. * * `Seal(message, receiver)` performs an _encrypt-sign-encrypt_ (ESE) on * a plaintext `message` for a `receiver` identity. This prevents repudiation diff --git a/api/latica/index.js b/api/latica/index.js new file mode 100644 index 0000000000..ad1b04be9e --- /dev/null +++ b/api/latica/index.js @@ -0,0 +1,2337 @@ +/** + * @module network + * @status Experimental + * + * This module provides primitives for creating a p2p network. + */ +import { isBufferLike } from '../util.js' +import { Buffer } from '../buffer.js' +import { sodium, randomBytes } from '../crypto.js' + +import { Encryption } from './encryption.js' +import { Cache } from './cache.js' +import * as NAT from './nat.js' + +import { + Packet, + PacketPing, + PacketPong, + PacketIntro, + PacketPublish, + PacketStream, + PacketSync, + PacketJoin, + PacketQuery, + sha256, + VERSION +} from './packets.js' + +export { Packet, sha256, Cache, Encryption, NAT } + +/** + * Retry delay in milliseconds for ping. + * @type {number} + */ +export const PING_RETRY = 500 + +/** + * Probe wait timeout in milliseconds. + * @type {number} + */ +export const PROBE_WAIT = 512 + +/** + * Default keep alive timeout. + * @type {number} + */ +export const DEFAULT_KEEP_ALIVE = 30_000 + +/** + * Default rate limit threshold in milliseconds. + * @type {number} + */ +export const DEFAULT_RATE_LIMIT_THRESHOLD = 8000 + +const PRIV_PORTS = 1024 +const MAX_PORTS = 65535 - PRIV_PORTS +const MAX_BANDWIDTH = 1024 * 32 + +const PEERID_REGEX = /^([A-Fa-f0-9]{2}){32}$/ + +/** + * Port generator factory function. + * @param {object} ports - the cache to use (a set) + * @param {number?} p - initial port + * @return {number} + */ +export const getRandomPort = (ports = new Set(), p) => { + do { + p = Math.max(1024, Math.ceil(Math.random() * 0xffff)) + } while (ports.has(p) && ports.size < MAX_PORTS) + + ports.add(p) + return p +} + +const isReplicatable = type => ( + type === PacketPublish.type || + type === PacketJoin.type +) + +/** + * Computes rate limit predicate value for a port and address pair for a given + * threshold updating an input rates map. This method is accessed concurrently, + * the rates object makes operations atomic to avoid race conditions. + * + * @param {Map} rates + * @param {number} type + * @param {number} port + * @param {string} address + * @return {boolean} + */ +export function rateLimit (rates, type, port, address, subclusterIdQuota) { + const R = isReplicatable(type) + const key = (R ? 'R' : 'C') + ':' + address + ':' + port + const quota = subclusterIdQuota || (R ? 1024 : 1024 * 1024) + const time = Math.floor(Date.now() / 60000) + const rate = rates.get(key) || { time, quota, used: 0 } + + rate.mtime = Date.now() // checked by mainLoop for garabge collection + + if (time !== rate.time) { + rate.time = time + if (rate.used > rate.quota) rate.quota -= 1 + else if (rate.used < quota) rate.quota += 1 + rate.used = 0 + } + + rate.used += 1 + + rates.set(key, rate) + if (rate.used >= rate.quota) return true +} + +/** + * A `RemotePeer` represents an initial, discovered, or connected remote peer. + * Typically, you will not need to create instances of this class directly. + */ +export class RemotePeer { + peerId = null + address = null + port = 0 + natType = null + clusters = {} + pingId = null + distance = 0 + connected = false + opening = 0 + probed = 0 + proxy = null + clock = 0 + uptime = 0 + lastUpdate = 0 + lastRequest = 0 + localPeer = null + + /** + * `RemotePeer` class constructor. + * @param {{ + * peerId?: string, + * address?: string, + * port?: number, + * natType?: number, + * clusters: object, + * reflectionId?: string, + * distance?: number, + * publicKey?: string, + * privateKey?: string, + * clock?: number, + * lastUpdate?: number, + * lastRequest?: number + * }} o + */ + constructor (o, peer) { + this.localPeer = peer + + if (!o.peerId) throw new Error('expected .peerId') + if (o.indexed) o.natType = NAT.UNRESTRICTED + if (o.natType && !NAT.isValid(o.natType)) throw new Error(`invalid .natType (${o.natType})`) + + const cid = Buffer.from(o.clusterId || '').toString('base64') + const scid = Buffer.from(o.subclusterId || '').toString('base64') + + if (cid && scid) { + this.clusters[cid] = { [scid]: { rateLimit: MAX_BANDWIDTH } } + } + + Object.assign(this, o) + } + + async write (sharedKey, args) { + let rinfo = this + if (this.proxy) rinfo = this.proxy + + const keys = await Encryption.createKeyPair(sharedKey) + + args.subclusterId = keys.publicKey + args.clusterId = this.localPeer.clusterId + args.usr3 = Buffer.from(this.peerId, 'hex') + args.usr4 = Buffer.from(this.localPeer.peerId, 'hex') + args.message = this.localPeer.encryption.seal(args.message, keys) + + const cache = new Map() + const packets = await this.localPeer._message2packets(PacketStream, args.message, args) + + if (this.proxy) { + this.localPeer._onDebug(`>> WRITE STREAM HAS PROXY ${this.proxy.address}:${this.proxy.port}`) + } + + for (const packet of packets) { + const from = this.localPeer.peerId.slice(0, 6) + const to = this.peerId.slice(0, 6) + this.localPeer._onDebug(`>> WRITE STREAM (from=${from}, to=${to}, via=${rinfo.address}:${rinfo.port})`) + + const pid = packet.packetId.toString('hex') + cache.set(pid, packet) + this.localPeer.gate.set(pid, 1) + await this.localPeer.send(await Packet.encode(packet), rinfo.port, rinfo.address, this.socket) + } + + const head = packets.find(p => p.index === 0) // has a head, should compose + const p = await this.localPeer.cache.compose(head, cache) + return [p] + } +} + +/** + * `Peer` class factory. + * @param {{ createSocket: function('udp4', null, object?): object }} options + */ +export class Peer { + port = null + address = null + natType = NAT.UNKNOWN + nextNatType = NAT.UNKNOWN + clusters = {} + syncs = {} + reflectionId = null + reflectionTimeout = null + reflectionStage = 0 + reflectionRetry = 1 + reflectionFirstResponder = null + peerId = '' + isListening = false + ctime = Date.now() + lastUpdate = 0 + lastSync = 0 + closing = false + clock = 0 + unpublished = {} + cache = null + uptime = 0 + maxHops = 16 + bdpCache = /** @type {number[]} */ ([]) + + dgram = null + + onListening = null + onDelete = null + + sendQueue = [] + firewall = null + rates = new Map() + streamBuffer = new Map() + gate = new Map() + returnRoutes = new Map() + + metrics = { + i: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, DROPPED: 0 }, + o: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0 } + } + + peers = JSON.parse(/* snapshot_start=1691579150299, filter=easy,static */` + [{"address":"44.213.42.133","port":10885,"peerId":"4825fe0475c44bc0222e76c5fa7cf4759cd5ef8c66258c039653f06d329a9af5","natType":31,"indexed":true},{"address":"107.20.123.15","port":31503,"peerId":"2de8ac51f820a5b9dc8a3d2c0f27ccc6e12a418c9674272a10daaa609eab0b41","natType":31,"indexed":true},{"address":"54.227.171.107","port":43883,"peerId":"7aa3d21ceb527533489af3888ea6d73d26771f30419578e85fba197b15b3d18d","natType":31,"indexed":true},{"address":"54.157.134.116","port":34420,"peerId":"1d2315f6f16e5f560b75fbfaf274cad28c12eb54bb921f32cf93087d926f05a9","natType":31,"indexed":true},{"address":"184.169.205.9","port":52489,"peerId":"db00d46e23d99befe42beb32da65ac3343a1579da32c3f6f89f707d5f71bb052","natType":31,"indexed":true},{"address":"35.158.123.13","port":31501,"peerId":"4ba1d23266a2d2833a3275c1d6e6f7ce4b8657e2f1b8be11f6caf53d0955db88","natType":31,"indexed":true},{"address":"3.68.89.3","port":22787,"peerId":"448b083bd8a495ce684d5837359ce69d0ff8a5a844efe18583ab000c99d3a0ff","natType":31,"indexed":true},{"address":"3.76.100.161","port":25761,"peerId":"07bffa90d89bf74e06ff7f83938b90acb1a1c5ce718d1f07854c48c6c12cee49","natType":31,"indexed":true},{"address":"3.70.241.230","port":61926,"peerId":"1d7ee8d965794ee286ac425d060bab27698a1de92986dc6f4028300895c6aa5c","natType":31,"indexed":true},{"address":"3.70.160.181","port":41141,"peerId":"707c07171ac9371b2f1de23e78dad15d29b56d47abed5e5a187944ed55fc8483","natType":31,"indexed":true},{"address":"3.122.250.236","port":64236,"peerId":"a830615090d5cdc3698559764e853965a0d27baad0e3757568e6c7362bc6a12a","natType":31,"indexed":true},{"address":"18.130.98.23","port":25111,"peerId":"ba483c1477ab7a99de2d9b60358d9641ff6a6dc6ef4e3d3e1fc069b19ac89da4","natType":31,"indexed":true},{"address":"13.42.10.247","port":2807,"peerId":"032b79de5b4581ee39c6d15b12908171229a8eb1017cf68fd356af6bbbc21892","natType":31,"indexed":true},{"address":"18.229.140.216","port":36056,"peerId":"73d726c04c05fb3a8a5382e7a4d7af41b4e1661aadf9020545f23781fefe3527","natType":31,"indexed":true}] + `/* snapshot_end=1691579150299 */).map((/** @type {object} */ o) => new RemotePeer({ ...o, indexed: true }, this)) + + /** + * `Peer` class constructor. + * @param {object=} opts - Options + * @param {Buffer} opts.peerId - A 32 byte buffer (ie, `Encryption.createId()`). + * @param {Buffer} opts.clusterId - A 32 byte buffer (ie, `Encryption.createClusterId()`). + * @param {number=} opts.port - A port number. + * @param {number=} opts.probeInternalPort - An internal port number (semi-private for testing). + * @param {number=} opts.probeExternalPort - An external port number (semi-private for testing). + * @param {number=} opts.natType - A nat type. + * @param {string=} opts.address - An ipv4 address. + * @param {number=} opts.keepalive - The interval of the main loop. + * @param {function=} opts.siblingResolver - A function that can be used to determine canonical data in case two packets have concurrent clock values. + * @param {object} dgram - A nodejs compatible implementation of the dgram module (sans multicast). + */ + constructor (persistedState = {}, dgram) { + if (!dgram) { + throw new Error('dgram implementation required in constructor as second argument') + } + + this.dgram = dgram + + const config = persistedState?.config ?? persistedState ?? {} + + this.encryption = new Encryption() + + if (!config.peerId) throw new Error('constructor expected .peerId') + if (!Peer.isValidPeerId(config.peerId)) throw new Error(`invalid .peerId (${config.peerId})`) + + // + // The purpose of this.config is to seperate transitioned state from initial state. + // + this.config = { // TODO(@heapwolf): Object.freeze this maybe + keepalive: DEFAULT_KEEP_ALIVE, + ...config + } + + let cacheData + + if (persistedState?.data?.length > 0) { + cacheData = new Map(persistedState.data) + } + + this.cache = new Cache(cacheData, config.siblingResolver) + this.cache.onEjected = p => this.mcast(p) + + this.unpublished = persistedState?.unpublished || {} + this._onError = err => this.onError && this.onError(err) + + Object.assign(this, config) + + if (!this.indexed && !this.clusterId) throw new Error('constructor expected .clusterId') + if (typeof this.peerId !== 'string') throw new Error('peerId should be of type string') + + this.port = config.port || null + this.natType = config.natType || null + this.address = config.address || null + + this.socket = this.dgram.createSocket('udp4', null, this) + this.probeSocket = this.dgram.createSocket('udp4', null, this).unref() + + const isRecoverable = err => + err.code === 'ECONNRESET' || + err.code === 'ECONNREFUSED' || + err.code === 'EADDRINUSE' || + err.code === 'ETIMEDOUT' + + this.socket.on('error', err => isRecoverable(err) && this._listen()) + this.probeSocket.on('error', err => isRecoverable(err) && this._listen()) + } + + /** + * An implementation for clearning an interval that can be overridden by the test suite + * @param Number the number that identifies the timer + * @return {undefined} + * @ignore + */ + _clearInterval (tid) { + clearInterval(tid) + } + + /** + * An implementation for clearning a timeout that can be overridden by the test suite + * @param Number the number that identifies the timer + * @return {undefined} + * @ignore + */ + _clearTimeout (tid) { + clearTimeout(tid) + } + + /** + * An implementation of an internal timer that can be overridden by the test suite + * @return {Number} + * @ignore + */ + _setInterval (fn, t) { + return setInterval(fn, t) + } + + /** + * An implementation of an timeout timer that can be overridden by the test suite + * @return {Number} + * @ignore + */ + _setTimeout (fn, t) { + return setTimeout(fn, t) + } + + _onDebug (...args) { + if (this.onDebug) this.onDebug(this.peerId, ...args) + } + + /** + * A method that encapsulates the listing procedure + * @return {undefined} + * @ignore + */ + async _listen () { + await sodium.ready + + this.socket.removeAllListeners() + this.probeSocket.removeAllListeners() + + this.socket.on('message', (...args) => this._onMessage(...args)) + this.socket.on('error', (...args) => this._onError(...args)) + this.probeSocket.on('message', (...args) => this._onProbeMessage(...args)) + this.probeSocket.on('error', (...args) => this._onError(...args)) + + this.socket.setMaxListeners(2048) + this.probeSocket.setMaxListeners(2048) + + const listening = Promise.all([ + new Promise(resolve => this.socket.on('listening', resolve)), + new Promise(resolve => this.probeSocket.on('listening', resolve)) + ]) + + this.socket.bind(this.config.port || 0) + this.probeSocket.bind(this.config.probeInternalPort || 0) + + await listening + + this.config.port = this.socket.address().port + this.config.probeInternalPort = this.probeSocket.address().port + + if (this.onListening) this.onListening() + this.isListening = true + + this._onDebug(`++ INIT (config.internalPort=${this.config.port}, config.probeInternalPort=${this.config.probeInternalPort})`) + } + + /* + * This method will bind the sockets, begin pinging known peers, and start + * the main program loop. + * @return {Any} + */ + async init (cb) { + if (!this.isListening) await this._listen() + if (cb) this.onReady = cb + + this._mainLoop(Date.now()) + this.mainLoopTimer = this._setInterval(ts => this._mainLoop(ts), this.config.keepalive) + + if (this.indexed && this.onReady) return this.onReady(await this.getInfo()) + } + + /** + * Continuously evaluate the state of the peer and its network + * @return {undefined} + * @ignore + */ + async _mainLoop (ts) { + if (this.closing) return this._clearInterval(this.mainLoopTimer) + + if (!Peer.onLine()) { + if (this.onConnecting) this.onConnecting({ code: -2, status: 'Offline' }) + return true + } + + if (!this.reflectionId) this.requestReflection() + if (this.onInterval) this.onInterval() + + this.uptime += this.config.keepalive + + // heartbeat + for (const [, peer] of Object.entries(this.peers)) { + this.ping(peer, false, { + message: { + requesterPeerId: this.peerId, + natType: this.natType + } + }) + } + + // wait for nat type to be discovered + if (!NAT.isValid(this.natType)) return true + + for (const [k, packet] of [...this.cache.data]) { + const p = Packet.from(packet) + if (!p) continue + if (!p.timestamp) p.timestamp = ts + const clusterId = p.clusterId.toString('base64') + + const mult = this.clusters[clusterId] ? 2 : 1 + const ttl = (p.ttl < Packet.ttl) ? p.ttl : Packet.ttl * mult + const deadline = p.timestamp + ttl + + if (deadline <= ts) { + if (p.hops < this.maxHops) this.mcast(p) + this.cache.delete(k) + this._onDebug('-- DELETE', k, this.cache.size) + if (this.onDelete) this.onDelete(p) + } + } + + for (let [k, v] of this.gate.entries()) { + v -= 1 + if (!v) this.gate.delete(k) + else this.gate.set(k, v) + } + + for (let [k, v] of this.returnRoutes.entries()) { + v -= 1 + if (!v) this.returnRoutes.delete(k) + else this.returnRoutes.set(k, v) + } + + // prune peer list + for (const [i, peer] of Object.entries(this.peers)) { + if (peer.indexed) continue + const expired = (peer.lastUpdate + this.config.keepalive) < Date.now() + if (expired) { // || !NAT.isValid(peer.natType)) { + const p = this.peers.splice(i, 1) + if (this.onDisconnection) this.onDisconnection(p) + continue + } + } + + // if this peer has previously tried to join any clusters, multicast a + // join messages for each into the network so we are always searching. + for (const cluster of Object.values(this.clusters)) { + for (const subcluster of Object.values(cluster)) { + this.join(subcluster.sharedKey, subcluster) + } + } + return true + } + + /** + * Enqueue packets to be sent to the network + * @param {Buffer} data - An encoded packet + * @param {number} port - The desination port of the remote host + * @param {string} address - The destination address of the remote host + * @param {Socket=this.socket} socket - The socket to send on + * @return {undefined} + * @ignore + */ + send (data, port, address, socket = this.socket) { + this.sendQueue.push({ data, port, address, socket }) + this._scheduleSend() + } + + /** + * @private + */ + async stream (peerId, sharedKey, args) { + const p = this.peers.find(p => p.peerId === peerId) + if (p) return p.write(sharedKey, args) + } + + /** + * @private + */ + _scheduleSend () { + if (this.sendTimeout) this._clearTimeout(this.sendTimeout) + this.sendTimeout = this._setTimeout(() => { this._dequeue() }) + } + + /** + * @private + */ + _dequeue () { + if (!this.sendQueue.length) return + const { data, port, address, socket } = this.sendQueue.shift() + + socket.send(data, port, address, err => { + if (this.sendQueue.length) this._scheduleSend() + if (err) return this._onError(err) + + const packet = Packet.decode(data) + if (!packet) return + + this.metrics.o[packet.type]++ + delete this.unpublished[packet.packetId.toString('hex')] + if (this.onSend && packet.type) this.onSend(packet, port, address) + this._onDebug(`>> SEND (from=${this.address}:${this.port}, to=${address}:${port}, type=${packet.type})`) + }) + } + + /** + * Send any unpublished packets + * @return {undefined} + * @ignore + */ + async sendUnpublished () { + for (const [packetId] of Object.entries(this.unpublished)) { + const packet = this.cache.get(packetId) + + if (!packet) { // it may have been purged already + delete this.unpublished[packetId] + continue + } + + await this.mcast(packet) + this._onDebug(`-> RESEND (packetId=${packetId})`) + if (this.onState) this.onState() + } + } + + /** + * Get the serializable state of the peer (can be passed to the constructor or create method) + * @return {undefined} + */ + getState () { + this.config.clock = this.clock // save off the clock + + const peers = this.peers.map(p => { + p = { ...p } + delete p.localPeer + return p + }) + + return { + peers, + syncs: this.syncs, + config: this.config, + data: [...this.cache.data.entries()], + unpublished: this.unpublished + } + } + + async getInfo () { + return { + address: this.address, + port: this.port, + clock: this.clock, + uptime: this.uptime, + natType: this.natType, + natName: NAT.toString(this.natType), + peerId: this.peerId + } + } + + async cacheInsert (packet) { + const p = Packet.from(packet) + this.cache.insert(p.packetId.toString('hex'), p) + } + + async addIndexedPeer (info) { + if (!info.peerId) throw new Error('options.peerId required') + if (!info.address) throw new Error('options.address required') + if (!info.port) throw new Error('options.port required') + info.indexed = true + this.peers.push(new RemotePeer(info)) + } + + async reconnect () { + for (const cluster of Object.values(this.clusters)) { + for (const subcluster of Object.values(cluster)) { + this.join(subcluster.sharedKey, subcluster) + } + } + } + + async disconnect () { + this.natType = null + this.reflectionStage = 0 + this.reflectionId = null + this.reflectionTimeout = null + this.probeReflectionTimeout = null + } + + async sealUnsigned (...args) { + return this.encryption.sealUnsigned(...args) + } + + async openUnsigned (...args) { + return this.encryption.openUnsigned(...args) + } + + async seal (...args) { + return this.encryption.seal(...args) + } + + async open (...args) { + return this.encryption.open(...args) + } + + async addEncryptionKey (...args) { + return this.encryption.add(...args) + } + + /** + * Get a selection of known peers + * @return {Array<RemotePeer>} + * @ignore + */ + getPeers (packet, peers, ignorelist, filter = o => o) { + const rand = () => Math.random() - 0.5 + + const base = p => { + if (ignorelist.findIndex(ilp => (ilp.port === p.port) && (ilp.address === p.address)) > -1) return false + if (p.lastUpdate === 0) return false + if (p.lastUpdate < Date.now() - (this.config.keepalive * 4)) return false + if (this.peerId === p.peerId) return false // same as me + if (packet.message.requesterPeerId === p.peerId) return false // same as requester - @todo: is this true in all cases? + if (!p.port || !NAT.isValid(p.natType)) return false + return true + } + + const candidates = peers + .filter(filter) + .filter(base) + .sort(rand) + + const list = candidates.slice(0, 3) + + if (!list.some(p => p.indexed)) { + const indexed = candidates.filter(p => p.indexed && !list.includes(p)) + if (indexed.length) list.push(indexed[0]) + } + + const clusterId = packet.clusterId.toString('base64') + const friends = candidates.filter(p => p.clusters && p.clusters[clusterId] && !list.includes(p)) + if (friends.length) { + list.unshift(friends[0]) + list.unshift(...candidates.filter(c => c.address === friends[0].address && c.peerId === friends[0].peerId)) + } + + return list + } + + /** + * Send an eventually consistent packet to a selection of peers (fanout) + * @return {undefined} + * @ignore + */ + async mcast (packet, ignorelist = []) { + const peers = this.getPeers(packet, this.peers, ignorelist) + const pid = packet.packetId.toString('hex') + + packet.hops += 1 + + for (const peer of peers) { + this.send(await Packet.encode(packet), peer.port, peer.address) + } + + if (this.onMulticast) this.onMulticast(packet) + if (this.gate.has(pid)) return + this.gate.set(pid, 1) + } + + /** + * The process of determining this peer's NAT behavior (firewall and dependentness) + * @return {undefined} + * @ignore + */ + async requestReflection () { + if (this.closing || this.indexed || this.reflectionId) { + this._onDebug('<> REFLECT ABORTED', this.reflectionId) + return + } + + if (this.natType && (this.lastUpdate > 0 && (Date.now() - this.config.keepalive) < this.lastUpdate)) { + this._onDebug(`<> REFLECT NOT NEEDED (last-recv=${Date.now() - this.lastUpdate}ms)`) + return + } + + this._onDebug('-> REQ REFLECT', this.reflectionId, this.reflectionStage) + if (this.onConnecting) this.onConnecting({ code: -1, status: `Entering reflection (lastUpdate ${Date.now() - this.lastUpdate}ms)` }) + + const peers = [...this.peers] + .filter(p => p.lastUpdate !== 0) + .filter(p => p.natType === NAT.UNRESTRICTED || p.natType === NAT.ADDR_RESTRICTED || p.indexed) + + if (peers.length < 2) { + if (this.onConnecting) this.onConnecting({ code: -1, status: 'Not enough pingable peers' }) + this._onDebug('XX REFLECT NOT ENOUGH PINGABLE PEERS - RETRYING', peers) + + // tell all well-known peers that we would like to hear from them, if + // we hear from any we can ask for the reflection information we need. + for (const peer of this.peers.filter(p => p.indexed).sort(() => Math.random() - 0.5).slice(0, 32)) { + await this.ping(peer, false, { message: { isConnection: true, requesterPeerId: this.peerId } }) + } + + if (++this.reflectionRetry > 16) this.reflectionRetry = 1 + return this._setTimeout(() => this.requestReflection(), this.reflectionRetry * 256) + } + + this.reflectionRetry = 1 + + const requesterPeerId = this.peerId + const opts = { requesterPeerId, isReflection: true } + + this.reflectionId = opts.reflectionId = randomBytes(6).toString('hex').padStart(12, '0') + + if (this.onConnecting) { + this.onConnecting({ code: 0.5, status: `Found ${peers.length} elegible peers for reflection` }) + } + // + // # STEP 1 + // The purpose of this step is strictily to discover the external port of + // the probe socket. + // + if (this.reflectionStage === 0) { + if (this.onConnecting) this.onConnecting({ code: 1, status: 'Discover External Port' }) + // start refelection with an zeroed NAT type + if (this.reflectionTimeout) this._clearTimeout(this.reflectionTimeout) + this.reflectionStage = 1 + + this._onDebug('-> NAT REFLECT - STAGE1: A', this.reflectionId) + const list = peers.filter(p => p.probed).sort(() => Math.random() - 0.5) + const peer = list.length ? list[0] : peers[0] + peer.probed = Date.now() // mark this peer as being used to provide port info + this.ping(peer, false, { message: { ...opts, isProbe: true } }, this.probeSocket) + + // we expect onMessageProbe to fire and clear this timer or it will timeout + this.probeReflectionTimeout = this._setTimeout(() => { + this.probeReflectionTimeout = null + if (this.reflectionStage !== 1) return + this._onDebug('XX NAT REFLECT - STAGE1: C - TIMEOUT', this.reflectionId) + if (this.onConnecting) this.onConnecting({ code: 1, status: 'Timeout' }) + + this.reflectionStage = 1 + this.reflectionId = null + this.requestReflection() + }, 1024) + + this._onDebug('-> NAT REFLECT - STAGE1: B', this.reflectionId) + return + } + + // + // # STEP 2 + // + // The purpose of step 2 is twofold: + // + // 1) ask two different peers for the external port and address for our primary socket. + // If they are different, we can determine that our NAT is a `ENDPOINT_DEPENDENT`. + // + // 2) ask the peers to also reply to our probe socket from their probe socket. + // These packets will both be dropped for `FIREWALL_ALLOW_KNOWN_IP_AND_PORT` and will both + // arrive for `FIREWALL_ALLOW_ANY`. If one packet arrives (which will always be from the peer + // which was previously probed), this indicates `FIREWALL_ALLOW_KNOWN_IP`. + // + if (this.reflectionStage === 1) { + this.reflectionStage = 2 + const { probeExternalPort } = this.config + if (this.onConnecting) this.onConnecting({ code: 1.5, status: 'Discover NAT' }) + + // peer1 is the most recently probed (likely the same peer used in step1) + // using the most recent guarantees that the the NAT mapping is still open + const peer1 = peers.filter(p => p.probed).sort((a, b) => b.probed - a.probed)[0] + + // peer has NEVER previously been probed + const peer2 = peers.filter(p => !p.probed).sort(() => Math.random() - 0.5)[0] + + if (!peer1 || !peer2) { + this._onDebug('XX NAT REFLECT - STAGE2: INSUFFICENT PEERS - RETRYING') + if (this.onConnecting) this.onConnecting({ code: 1.5, status: 'Insufficent Peers' }) + return this._setTimeout(() => this.requestReflection(), 256) + } + + this._onDebug('-> NAT REFLECT - STAGE2: START', this.reflectionId) + + // reset reflection variables to defaults + this.nextNatType = NAT.UNKNOWN + this.reflectionFirstResponder = null + + this.ping(peer1, false, { message: { ...opts, probeExternalPort } }) + this.ping(peer2, false, { message: { ...opts, probeExternalPort } }) + + if (this.onConnecting) { + this.onConnecting({ code: 2, status: `Requesting reflection from ${peer1.address}` }) + this.onConnecting({ code: 2, status: `Requesting reflection from ${peer2.address}` }) + } + + if (this.reflectionTimeout) { + this._clearTimeout(this.reflectionTimeout) + this.reflectionTimeout = null + } + + this.reflectionTimeout = this._setTimeout(ts => { + this.reflectionTimeout = null + if (this.reflectionStage !== 2) return + if (this.onConnecting) this.onConnecting({ code: 2, status: 'Timeout' }) + this.reflectionStage = 1 + this.reflectionId = null + this._onDebug('XX NAT REFLECT - STAGE2: TIMEOUT', this.reflectionId) + return this.requestReflection() + }, 2048) + } + } + + /** + * Ping another peer + * @return {PacketPing} + * @ignore + */ + async ping (peer, withRetry, props, socket) { + if (!peer) { + return + } + + props.message.requesterPeerId = this.peerId + props.message.uptime = this.uptime + props.message.timestamp = Date.now() + props.clusterId = this.config.clusterId + + const packet = new PacketPing(props) + const data = await Packet.encode(packet) + + const send = async () => { + if (this.closing) return false + + const p = this.peers.find(p => p.peerId === peer.peerId) + // if (p?.reflectionId && p.reflectionId === packet.message.reflectionId) { + // return false + // } + + this.send(data, peer.port, peer.address, socket) + if (p) p.lastRequest = Date.now() + } + + send() + + if (withRetry) { + this._setTimeout(send, PING_RETRY) + this._setTimeout(send, PING_RETRY * 4) + } + + return packet + } + + /** + * Get a peer + * @return {RemotePeer} + * @ignore + */ + getPeer (id) { + return this.peers.find(p => p.peerId === id) + } + + /** + * This should be called at least once when an app starts to multicast + * this peer, and starts querying the network to discover peers. + * @param {object} keys - Created by `Encryption.createKeyPair()`. + * @param {object=} args - Options + * @param {number=MAX_BANDWIDTH} args.rateLimit - How many requests per second to allow for this subclusterId. + * @return {RemotePeer} + */ + async join (sharedKey, args = { rateLimit: MAX_BANDWIDTH }) { + const keys = await Encryption.createKeyPair(sharedKey) + this.encryption.add(keys.publicKey, keys.privateKey) + + if (!this.port || !this.natType) return + + args.sharedKey = sharedKey + + const clusterId = args.clusterId || this.config.clusterId + const subclusterId = keys.publicKey + + const cid = Buffer.from(clusterId || '').toString('base64') + const scid = Buffer.from(subclusterId || '').toString('base64') + + this.clusters[cid] ??= {} + this.clusters[cid][scid] = args + + this.clock += 1 + + const packet = new PacketJoin({ + clock: this.clock, + clusterId, + subclusterId, + message: { + requesterPeerId: this.peerId, + natType: this.natType, + address: this.address, + port: this.port, + key: [cid, scid].join(':') + } + }) + + this._onDebug(`-> JOIN (clusterId=${cid.slice(0, 6)}, subclusterId=${scid.slice(0, 6)}, clock=${packet.clock}/${this.clock})`) + if (this.onState) this.onState() + + this.mcast(packet) + this.gate.set(packet.packetId.toString('hex'), 1) + } + + /** + * @param {Packet} T - The constructor to be used to create packets. + * @param {Any} message - The message to be split and packaged. + * @return {Array<Packet<T>>} + * @ignore + */ + async _message2packets (T, message, args) { + const { clusterId, subclusterId, packet, nextId, meta = {}, usr1, usr2, sig } = args + + let messages = [message] + const len = message?.byteLength ?? message?.length ?? 0 + let clock = packet?.clock || 0 + + const siblings = packet && [...this.cache.data.values()] + .filter(Boolean) + .filter(p => { + if (!p.previousId || !packet.packetId) return false + return Buffer.from(p.previousId).compare(Buffer.from(packet.packetId)) === 0 + }) + + if (siblings?.length) { + // if there are siblings of the previous packet + // pick the highest clock value, the parent packet or the sibling + const sort = (a, b) => a.clock - b.clock + const sib = siblings.sort(sort).reverse()[0] + clock = Math.max(clock, sib.clock) + 1 + } + + clock += 1 + + if (len > 1024) { // Split packets that have messages bigger than Packet.maxLength + messages = [{ + meta, + ts: Date.now(), + size: message.length, + indexes: Math.ceil(message.length / 1024) + }] + let pos = 0 + while (pos < message.length) messages.push(message.slice(pos, pos += 1024)) + } + + // turn each message into an actual packet + const packets = messages.map(message => new T({ + ...args, + clusterId, + subclusterId, + clock, + message, + usr1, + usr2, + usr3: args.usr3, + usr4: args.usr4, + sig + })) + + if (packet) packets[0].previousId = Buffer.from(packet.packetId) + if (nextId) packets[packets.length - 1].nextId = Buffer.from(nextId) + + // set the .packetId (any maybe the .previousId and .nextId) + for (let i = 0; i < packets.length; i++) { + if (packets.length > 1) packets[i].index = i + + if (i === 0) { + packets[0].packetId = await sha256(packets[0].message, { bytes: true }) + } else { + // all fragments will have the same previous packetId + // the index is used to stitch them back together in order. + packets[i].previousId = Buffer.from(packets[0].packetId) + } + + if (packets[i + 1]) { + packets[i + 1].packetId = await sha256( + Buffer.concat([ + await sha256(packets[i].packetId, { bytes: true }), + await sha256(packets[i + 1].message, { bytes: true }) + ]), + { bytes: true } + ) + + packets[i].nextId = Buffer.from(packets[i + 1].packetId) + } + } + + return packets + } + + /** + * Sends a packet into the network that will be replicated and buffered. + * Each peer that receives it will buffer it until TTL and then replicate + * it provided it has has not exceeded their maximum number of allowed hops. + * + * @param {object} keys - the public and private key pair created by `Encryption.createKeyPair()`. + * @param {object} args - The arguments to be applied. + * @param {Buffer} args.message - The message to be encrypted by keys and sent. + * @param {Packet<T>=} args.packet - The previous packet in the packet chain. + * @param {Buffer} args.usr1 - 32 bytes of arbitrary clusterId in the protocol framing. + * @param {Buffer} args.usr2 - 32 bytes of arbitrary clusterId in the protocol framing. + * @return {Array<PacketPublish>} + */ + async publish (sharedKey, args) { // wtf to do here, we need subclusterId and the actual user keys + if (!sharedKey) throw new Error('.publish() expected "sharedKey" argument in first position') + if (!isBufferLike(args.message)) throw new Error('.publish() will only accept a message of type buffer') + + const keys = await Encryption.createKeyPair(sharedKey) + + args.subclusterId = keys.publicKey + args.clusterId = args.clusterId || this.config.clusterId + + const cache = new Map() + const message = this.encryption.seal(args.message, keys) + const packets = await this._message2packets(PacketPublish, message, args) + + for (let packet of packets) { + packet = Packet.from(packet) + cache.set(packet.packetId.toString('hex'), packet) + this.cacheInsert(packet) + + if (this.onPacket && packet.index === -1) { + this.onPacket(packet, this.port, this.address, true) + } + + this.unpublished[packet.packetId.toString('hex')] = Date.now() + if (!Peer.onLine()) continue + + this.mcast(packet) + } + + const head = [...cache.values()][0] + // if there is a head, we can recompose the packets, this gives this + // peer a consistent view of the data as it has been published. + if (this.onPacket && head && head.index === 0) { + const p = await this.cache.compose(head, cache) + if (p) { + this.onPacket(p, this.port, this.address, true) + this._onDebug(`-> PUBLISH (multicasted=true, packetId=${p.packetId.toString('hex').slice(0, 8)})`) + return [p] + } + } + + return packets + } + + /** + * @return {undefined} + */ + async sync (peer, ptime = Date.now()) { + if (typeof peer === 'string') { + peer = this.peers.find(p => p.peerId === peer) + } + + const rinfo = peer?.proxy || peer + + this.lastSync = Date.now() + const summary = await this.cache.summarize('', this.cachePredicate(ptime)) + + this._onDebug(`-> SYNC START (dest=${peer.peerId.slice(0, 8)}, to=${rinfo.address}:${rinfo.port})`) + if (this.onSyncStart) this.onSyncStart(peer, rinfo.port, rinfo.address) + + // if we are out of sync send our cache summary + const data = await Packet.encode(new PacketSync({ + message: Cache.encodeSummary(summary), + usr4: Buffer.from(String(ptime)) + })) + + this.send(data, rinfo.port, rinfo.address, peer.socket) + } + + close () { + this._clearInterval(this.mainLoopTimer) + + if (this.closing) return + + this.closing = true + this.socket.close() + this.probeSocket.close() + + if (this.onClose) this.onClose() + } + + /** + * Deploy a query into the network + * @return {undefined} + * + */ + async query (query) { + const packet = new PacketQuery({ + message: query, + usr1: Buffer.from(String(Date.now())), + usr3: Buffer.from(randomBytes(32)), + usr4: Buffer.from(String(1)) + }) + const data = await Packet.encode(packet) + + const p = Packet.decode(data) // finalize a packet + const pid = p.packetId.toString('hex') + + if (this.gate.has(pid)) return + this.returnRoutes.set(p.usr3.toString('hex'), {}) + this.gate.set(pid, 1) // prevent accidental spam + + this._onDebug(`-> QUERY (type=question, query=${query}, packet=${pid.slice(0, 8)})`) + + await this.mcast(p) + } + + /** + * + * This is a default implementation for deciding what to summarize + * from the cache when receiving a request to sync. that can be overridden + * + */ + cachePredicate (ts) { + const max = Date.now() - Packet.ttl + const T = Math.min(ts || max, max) + + return packet => { + return packet.version === VERSION && packet.timestamp > T + } + } + + /** + * A connection was made, add the peer to the local list of known + * peers and call the onConnection if it is defined by the user. + * + * @return {undefined} + * @ignore + */ + async _onConnection (packet, peerId, port, address, proxy, socket) { + if (this.closing) return + + const natType = packet.message.natType + if (!NAT.isValid(natType)) return + if (!Peer.isValidPeerId(peerId)) return + if (peerId === this.peerId) return + + const cid = packet.clusterId.toString('base64') + const scid = packet.subclusterId.toString('base64') + + let peer = this.getPeer(peerId) + const firstContact = !peer + + if (firstContact) { + peer = new RemotePeer({ peerId }) + + if (this.peers.length >= 256) { + // TODO evicting an older peer definitely needs some more thought. + const oldPeerIndex = this.peers.findIndex(p => !p.lastUpdate && !p.indexed) + if (oldPeerIndex > -1) this.peers.splice(oldPeerIndex, 1) + } + + this._onDebug(`<- CONNECTION ADDING PEER (id=${peer.peerId}, address=${address}:${port})`) + this.peers.push(peer) + } + + peer.connected = true + peer.lastUpdate = Date.now() + peer.port = port + peer.natType = natType + peer.address = address + + if (proxy) peer.proxy = proxy + if (socket) peer.socket = socket + + if (cid) peer.clusters[cid] ??= {} + + if (cid && scid) { + const cluster = peer.clusters[cid] + cluster[scid] = { rateLimit: MAX_BANDWIDTH } + } + + if (!peer.localPeer) peer.localPeer = this + + this._onDebug('<- CONNECTION (' + + `peerId=${peer.peerId.slice(0, 6)}, ` + + `address=${address}:${port}, ` + + `type=${packet.type}, ` + + `clusterId=${cid.slice(0, 6)}, ` + + `subclusterId=${scid.slice(0, 6)})` + ) + + if (this.onJoin && this.clusters[cid]) { + this.onJoin(packet, peer, port, address) + } + + if (firstContact && this.onConnection) { + this.onConnection(packet, peer, port, address) + + const now = Date.now() + const key = [peer.address, peer.port].join(':') + let first = false + + // + // If you've never sync'd before, you can ask for 6 hours of data from + // other peers. If we have synced with a peer before we can just ask for + // data that they have seen since then, this will avoid the risk of + // spamming them and getting rate-limited. + // + if (!this.syncs[key]) { + this.syncs[key] = now - Packet.ttl + first = true + } + + const lastSyncSeconds = (now - this.syncs[key]) / 1000 + const syncWindow = this.config.syncWindow ?? 6000 + + if (first || now - this.syncs[key] > syncWindow) { + this.sync(peer.peerId, this.syncs[key]) + this._onDebug(`-> SYNC SEND (peerId=${peer.peerId.slice(0, 6)}, address=${key}, since=${lastSyncSeconds} seconds ago)`) + this.syncs[key] = now + } + } + } + + /** + * Received a Sync Packet + * @return {undefined} + * @ignore + */ + async _onSync (packet, port, address) { + this.metrics.i[packet.type]++ + + this.lastSync = Date.now() + const pid = packet.packetId.toString('hex') + + let ptime = Date.now() + + if (packet.usr4.byteLength > 8 || packet.usr4.byteLength < 16) { + const usr4 = parseInt(Buffer.from(packet.usr4).toString(), 10) + ptime = Math.min(ptime - Packet.ttl, usr4) + } + + if (!isBufferLike(packet.message)) return + if (this.gate.has(pid)) return + + this.gate.set(pid, 1) + + const remote = Cache.decodeSummary(packet.message) + const local = await this.cache.summarize(remote.prefix, this.cachePredicate(ptime)) + + if (!remote || !remote.hash || !local || !local.hash || local.hash === remote.hash) { + if (this.onSyncFinished) this.onSyncFinished(packet, port, address) + return + } + + if (this.onSync) this.onSync(packet, port, address, { remote, local }) + + const remoteBuckets = remote.buckets.filter(Boolean).length + this._onDebug(`<- ON SYNC (from=${address}:${port}, local=${local.hash.slice(0, 8)}, remote=${remote.hash.slice(0, 8)} remote-buckets=${remoteBuckets})`) + + for (let i = 0; i < local.buckets.length; i++) { + // + // nothing to send/sync, expect peer to send everything they have + // + if (!local.buckets[i] && !remote.buckets[i]) continue + + // + // you dont have any of these, im going to send them to you + // + if (!remote.buckets[i]) { + for (const [key, p] of this.cache.data.entries()) { + if (!key.startsWith(local.prefix + i.toString(16))) continue + + const packet = Packet.from(p) + if (!this.cachePredicate(ptime)(packet)) continue + + const pid = packet.packetId.toString('hex') + this._onDebug(`-> SYNC SEND PACKET (type=data, packetId=${pid.slice(0, 8)}, to=${address}:${port})`) + + this.send(await Packet.encode(packet), port, address) + } + } else { + // + // need more details about what exactly isn't synce'd + // + const nextLevel = await this.cache.summarize(local.prefix + i.toString(16), this.cachePredicate(ptime)) + const data = await Packet.encode(new PacketSync({ + message: Cache.encodeSummary(nextLevel), + usr4: Buffer.from(String(Date.now())) + })) + this.send(data, port, address) + } + } + } + + /** + * Received a Query Packet + * + * a -> b -> c -> (d) -> c -> b -> a + * + * @return {undefined} + * @example + * + * ```js + * peer.onQuery = (packet) => { + * // + * // read a database or something + * // + * return { + * message: Buffer.from('hello'), + * publicKey: '', + * privateKey: '' + * } + * } + * ``` + */ + async _onQuery (packet, port, address) { + this.metrics.i[packet.type]++ + + const pid = packet.packetId.toString('hex') + const queryId = packet.usr3.toString('hex') + const queryTimestamp = parseInt(packet.usr1.toString(), 10) + const queryType = parseInt(packet.usr4.toString(), 10) + + // if the timestamp in usr1 is older than now - 2s, bail + if (queryTimestamp < (Date.now() - 2048)) return + + const type = queryType === 1 ? 'question' : 'answer' + this._onDebug(`<- QUERY (type=${type}, from=${address}:${port}, packet=${pid.slice(0, 8)})`) + + let rinfo = { port, address } + + // + // receiving an answer + // + if (this.returnRoutes.has(queryId) && type === 'answer') { + rinfo = this.returnRoutes.get(queryId) + + let p = packet.copy() + if (p.index > -1) p = await this.cache.compose(p) + + if (p?.index === -1) { + this.returnRoutes.delete(p.previousId.toString('hex')) + p.type = PacketPublish.type + delete p.usr3 + delete p.usr4 + if (this.onAnswer) return this.onAnswer(p.message, p, port, address) + } + + if (!rinfo.address) return + } else if (type === 'question') { + if (this.gate.has(pid)) return + // + // receiving a query + // + this.returnRoutes.set(queryId, { address, port }) + + const query = packet.message + const packets = [] + + // + // The requestor is looking for an exact packetId. In this case, + // the peer has a packet with a previousId or nextId that points + // to a packetId they don't have. There is no need to specify the + // index in the query, split packets will have a nextId. + // + // if cache packet = { nextId: 'deadbeef...' } + // then query = { packetId: packet.nextId } + // or query = { packetId: packet.previousId } + // + if (query.packetId && this.cache.has(query.packetId)) { + const p = this.cache.get(query.packetId) + if (p) packets.push(p) + } else if (this.onQuery) { + const q = await this.onQuery(query) + if (q) packets.push(...await this._message2packets(PacketQuery, q.message, q)) + } + + if (packets.length) { + for (const p of packets) { + p.type = PacketQuery.type // convert the type during transport + p.usr3 = packet.usr3 // ensure the packet has the queryId + p.usr4 = Buffer.from(String(2)) // mark it as an answer packet + this.send(await Packet.encode(p), rinfo.port, rinfo.address) + this.gate.set(pid, 1) + } + return + } + } + + if (packet.hops >= this.maxHops) return + if (this.gate.has(pid)) return + + this._onDebug('>> QUERY RELAY', port, address) + await this.mcast(packet) + } + + /** + * Received a Ping Packet + * @return {undefined} + * @ignore + */ + async _onPing (packet, port, address) { + this.metrics.i[packet.type]++ + + this.lastUpdate = Date.now() + const { reflectionId, isReflection, isConnection, requesterPeerId, natType } = packet.message + + if (requesterPeerId === this.peerId) return // from self? + + const { probeExternalPort, isProbe, pingId } = packet.message + + // if (peer && reflectionId) peer.reflectionId = reflectionId + if (!port) port = packet.message.port + if (!address) address = packet.message.address + + const message = { + cacheSize: this.cache.size, + uptime: this.uptime, + responderPeerId: this.peerId, + requesterPeerId, + port, + isProbe, + address + } + + if (reflectionId) message.reflectionId = reflectionId + if (pingId) message.pingId = pingId + + if (isReflection) { + message.isReflection = true + message.port = port + message.address = address + } else { + message.natType = this.natType + } + + if (isConnection && natType) { + this._onDebug('<- CONNECTION (source=ping)') + this._onConnection(packet, requesterPeerId, port, address) + + message.isConnection = true + delete message.address + delete message.port + delete message.isProbe + } + + const { hash } = await this.cache.summarize('', this.cachePredicate()) + message.cacheSummaryHash = hash + + const packetPong = new PacketPong({ message }) + const buf = await Packet.encode(packetPong) + + this.send(buf, port, address) + + if (probeExternalPort) { + message.port = probeExternalPort + const packetPong = new PacketPong({ message }) + const buf = await Packet.encode(packetPong) + this.send(buf, probeExternalPort, address, this.probeSocket) + } + } + + /** + * Received a Pong Packet + * @return {undefined} + * @ignore + */ + async _onPong (packet, port, address) { + this.metrics.i[packet.type]++ + + this.lastUpdate = Date.now() + + const { reflectionId, pingId, isReflection, responderPeerId } = packet.message + + if (responderPeerId === this.peerId) return // from self? + + this._onDebug(`<- PONG (from=${address}:${port}, hash=${packet.message.cacheSummaryHash}, isConnection=${!!packet.message.isConnection})`) + const peer = this.getPeer(responderPeerId) + + if (packet.message.isConnection) { + if (pingId) peer.pingId = pingId + this._onDebug('<- CONNECTION (source=pong)') + this._onConnection(packet, responderPeerId, port, address) + return + } + + if (!peer) return + + if (isReflection && !this.indexed) { + if (reflectionId !== this.reflectionId) return + + this._clearTimeout(this.reflectionTimeout) + + if (!this.reflectionFirstResponder) { + this.reflectionFirstResponder = { port, address, responderPeerId, reflectionId, packet } + if (this.onConnecting) this.onConnecting({ code: 2.5, status: `Received reflection from ${address}:${port}` }) + this._onDebug('<- NAT REFLECT - STAGE2: FIRST RESPONSE', port, address, this.reflectionId) + this.reflectionFirstReponderTimeout = this._setTimeout(() => { + this.reflectionStage = 0 + this.lastUpdate = 0 + this.reflectionId = null + this._onDebug('<- NAT REFLECT FAILED TO ACQUIRE SECOND RESPONSE', this.reflectionId) + this.requestReflection() + }, PROBE_WAIT) + } else { + this._clearTimeout(this.reflectionFirstReponderTimeout) + if (this.onConnecting) this.onConnecting({ code: 2.5, status: `Received reflection from ${address}:${port}` }) + this._onDebug('<- NAT REFLECT - STAGE2: SECOND RESPONSE', port, address, this.reflectionId) + if (packet.message.address !== this.address) return + + this.nextNatType |= ( + packet.message.port === this.reflectionFirstResponder.packet.message.port + ) + ? NAT.MAPPING_ENDPOINT_INDEPENDENT + : NAT.MAPPING_ENDPOINT_DEPENDENT + + this._onDebug( + this.peerId, + `++ NAT REFLECT - STATE UPDATE (natType=${this.natType}, nextType=${this.nextNatType})`, + packet.message.port, + this.reflectionFirstResponder.packet.message.port + ) + + // wait PROBE_WAIT milliseconds for zero or more probe responses to arrive. + this._setTimeout(async () => { + // build the NAT type by combining information about the firewall with + // information about the endpoint independence + let natType = this.nextNatType + + // in the case where we recieved zero probe responses, we assume the firewall + // is of the hardest type 'FIREWALL_ALLOW_KNOWN_IP_AND_PORT'. + if (!NAT.isFirewallDefined(natType)) natType |= NAT.FIREWALL_ALLOW_KNOWN_IP_AND_PORT + + // if ((natType & NAT.MAPPING_ENDPOINT_DEPENDENT) === 1) natType = NAT.ENDPOINT_RESTRICTED + + if (NAT.isValid(natType)) { + // const oldType = this.natType + this.natType = natType + this.reflectionId = null + this.reflectionStage = 0 + + // if (natType !== oldType) { + // alert all connected peers of our new NAT type + for (const peer of this.peers) { + peer.lastRequest = Date.now() + + this._onDebug(`-> PING (to=${peer.address}:${peer.port}, peer-id=${peer.peerId.slice(0, 8)}, is-connection=true)`) + + this.ping(peer, false, { + message: { + requesterPeerId: this.peerId, + natType: this.natType, + cacheSize: this.cache.size, + isConnection: true + } + }) + } + + this._setTimeout(() => this._mainLoop(Date.now()), 1024) + + if (this.onNat) this.onNat(this.natType) + + this._onDebug(`++ NAT (type=${NAT.toString(this.natType)})`) + this.sendUnpublished() + // } + + if (this.onConnecting) this.onConnecting({ code: 3, status: `Discovered! (nat=${NAT.toString(this.natType)})` }) + if (this.onReady) this.onReady(await this.getInfo()) + } + + this.reflectionId = null + this.reflectionFirstResponder = null + }, PROBE_WAIT) + } + + this.address = packet.message.address + this.port = packet.message.port + this._onDebug(`++ NAT UPDATE STATE (address=${this.address}, port=${this.port})`) + } + } + + /** + * Received an Intro Packet + * @return {undefined} + * @ignore + */ + async _onIntro (packet, port, address, _, opts = { attempts: 0 }) { + this.metrics.i[packet.type]++ + if (this.closing) return + + const pid = packet.packetId.toString('hex') + // the packet needs to be gated, but should allow for attempt + // recursion so that the fallback can still be selected. + if (this.gate.has(pid) && opts.attempts === 0) return + this.gate.set(pid, 1) + + const ts = packet.usr1.length && Number(Buffer.from(packet.usr1).toString()) + + if (packet.hops >= this.maxHops) return + if (!isNaN(ts) && ((ts + this.config.keepalive) < Date.now())) return + if (packet.message.requesterPeerId === this.peerId) return // intro to myself? + if (packet.message.responderPeerId === this.peerId) return // intro from myself? + + // this is the peer that is being introduced to the new peers + const peerId = packet.message.requesterPeerId + const peerPort = packet.message.port + const peerAddress = packet.message.address + const natType = packet.message.natType + const { clusterId, subclusterId, clock } = packet + + // already introduced in the laste minute, just drop the packet + if (opts.attempts === 0 && this.gate.has(peerId + peerAddress + peerPort)) return + this.gate.set(peerId + peerAddress + peerPort, 2) + + // we already know this peer, and we're even connected to them! + let peer = this.getPeer(peerId) + if (!peer) peer = new RemotePeer({ peerId, natType, port: peerPort, address: peerAddress, clock, clusterId, subclusterId }) + if (peer.connected) return // already connected + if (clock > 0 && clock < peer.clock) return + peer.clock = clock + + // a mutex per inbound peer to ensure that it's not connecting concurrently, + // the check of the attempts ensures its allowed to recurse before failing so + // it can still fall back + if (this.gate.has('CONN' + peer.peerId) && opts.attempts === 0) return + this.gate.set('CONN' + peer.peerId, 1) + + const cid = clusterId.toString('base64') + const scid = subclusterId.toString('base64') + + this._onDebug('<- INTRO (' + + `isRendezvous=${packet.message.isRendezvous}, ` + + `from=${address}:${port}, ` + + `to=${packet.message.address}:${packet.message.port}, ` + + `clusterId=${cid.slice(0, 6)}, ` + + `subclusterId=${scid.slice(0, 6)}` + + ')') + + if (this.onIntro) this.onIntro(packet, peer, peerPort, peerAddress) + + const pingId = randomBytes(6).toString('hex').padStart(12, '0') + const { hash } = await this.cache.summarize('', this.cachePredicate()) + + const props = { + clusterId, + subclusterId, + message: { + natType: this.natType, + isConnection: true, + cacheSummaryHash: hash, + pingId: packet.message.pingId, + requesterPeerId: this.peerId + } + } + + const strategy = NAT.connectionStrategy(this.natType, packet.message.natType) + const proxyCandidate = this.peers.find(p => p.peerId === packet.message.responderPeerId) + + if (opts.attempts >= 2) { + this._onDebug('<- CONNECTION (source=intro)') + this._onConnection(packet, peer.peerId, peerPort, peerAddress, proxyCandidate) + return false + } + + this._setTimeout(() => { + if (this.getPeer(peer.peerId)) return + opts.attempts = 2 + this._onIntro(packet, port, address, _, opts) + }, 1024 * 2) + + if (packet.message.isRendezvous) { + this._onDebug(`<- JOIN INTRO FROM RENDEZVOUS (to=${packet.message.address}:${packet.message.port}, dest=${packet.message.requesterPeerId.slice(0, 6)}, via=${address}:${port}, strategy=${NAT.toStringStrategy(strategy)})`) + } + + this._onDebug(`++ JOIN INTRO (strategy=${NAT.toStringStrategy(strategy)}, from=${this.address}:${this.port} [${NAT.toString(this.natType)}], to=${packet.message.address}:${packet.message.port} [${NAT.toString(packet.message.natType)}])`) + + if (strategy === NAT.STRATEGY_TRAVERSAL_CONNECT) { + this._onDebug(`## NAT CONNECT (from=${this.address}:${this.port}, to=${peerAddress}:${peerPort}, pingId=${pingId})`) + + let i = 0 + if (!this.socketPool) { + this.socketPool = Array.from({ length: 256 }, (_, index) => { + return this.dgram.createSocket('udp4', null, this, index).unref() + }) + } + + // A probes 1 target port on B from 1024 source ports + // (this is 1.59% of the search clusterId) + // B probes 256 target ports on A from 1 source port + // (this is 0.40% of the search clusterId) + // + // Probability of successful traversal: 98.35% + // + const interval = this._setInterval(async () => { + // send messages until we receive a message from them. giveup after sending ±1024 + // packets and fall back to using the peer that sent this as the initial proxy. + if (i++ >= 1024) { + this._clearInterval(interval) + + opts.attempts++ + this._onIntro(packet, port, address, _, opts) + return false + } + + const p = { + clusterId, + subclusterId, + message: { + requesterPeerId: this.peerId, + cacheSummaryHash: hash, + natType: this.natType, + uptime: this.uptime, + isConnection: true, + timestamp: Date.now(), + pingId + } + } + + const data = await Packet.encode(new PacketPing(p)) + + const rand = () => Math.random() - 0.5 + const pooledSocket = this.socketPool.sort(rand).find(s => !s.active) + if (!pooledSocket) return // TODO recover from exausted socket pool + + // mark socket as active & deactivate it after timeout + pooledSocket.active = true + pooledSocket.reclaim = this._setTimeout(() => { + pooledSocket.active = false + pooledSocket.removeAllListeners() + }, 1024) + + pooledSocket.on('message', async (msg, rinfo) => { + // if (rinfo.port !== peerPort || rinfo.address !== peerAddress) return + + // cancel scheduled events + this._clearInterval(interval) + this._clearTimeout(pooledSocket.reclaim) + + // remove any events currently bound on the socket + pooledSocket.removeAllListeners() + pooledSocket.on('message', (msg, rinfo) => { + this._onMessage(msg, rinfo) + }) + + this._onDebug('<- CONNECTION (source=intro)') + this._onConnection(packet, peer.peerId, rinfo.port, rinfo.address, undefined, pooledSocket) + + const p = { + clusterId, + subclusterId, + clock: this.clock, + message: { + requesterPeerId: this.peerId, + natType: this.natType, + isConnection: true + } + } + + const data = await Packet.encode(new PacketPing(p)) + + pooledSocket.send(data, rinfo.port, rinfo.address) + + // create a new socket to replace it in the pool + const oldIndex = this.socketPool.findIndex(s => s === pooledSocket) + this.socketPool[oldIndex] = this.dgram.createSocket('udp4', null, this).unref() + + this._onMessage(msg, rinfo) + }) + + try { + pooledSocket.send(data, peerPort, peerAddress) + } catch (err) { + console.error('STRATEGY_TRAVERSAL_CONNECT error', err) + } + }, 10) + + return + } + + if (strategy === NAT.STRATEGY_PROXY && !peer.proxy) { + // TODO could allow multiple proxies + this._onDebug('<- CONNECTION (source=proxy)') + this._onConnection(packet, peer.peerId, peerPort, peerAddress, proxyCandidate) + this._onDebug('++ INTRO CHOSE PROXY STRATEGY') + } + + if (strategy === NAT.STRATEGY_TRAVERSAL_OPEN) { + peer.opening = Date.now() + + const portsCache = new Set() + + if (!this.bdpCache.length) { + globalThis.bdpCache = this.bdpCache = Array.from({ length: 1024 }, () => getRandomPort(portsCache)) + } + + for (const port of this.bdpCache) { + this.send(Buffer.from([0x1]), port, packet.message.address) + } + + return + } + + if (strategy === NAT.STRATEGY_DIRECT_CONNECT) { + this._onDebug('++ NAT STRATEGY_DIRECT_CONNECT') + } + + if (strategy === NAT.STRATEGY_DEFER) { + this._onDebug('++ NAT STRATEGY_DEFER') + } + + this.ping(peer, true, props) + } + + /** + * Received an Join Packet + * @return {undefined} + * @ignore + */ + async _onJoin (packet, port, address, data) { + this.metrics.i[packet.type]++ + + const pid = packet.packetId.toString('hex') + if (packet.message.requesterPeerId === this.peerId) return // from self? + if (this.gate.has(pid)) return + if (packet.clusterId.length !== 32) return + + this.lastUpdate = Date.now() + + const peerId = packet.message.requesterPeerId + const rendezvousDeadline = packet.message.rendezvousDeadline + const clusterId = packet.clusterId + const subclusterId = packet.subclusterId + const peerAddress = packet.message.address + const peerPort = packet.message.port + + // prevents premature pruning; a peer is not directly connecting + const peer = this.peers.find(p => p.peerId === peerId) + if (peer) peer.lastUpdate = Date.now() + + // a rendezvous isn't relevant if it's too old, just drop the packet + if (rendezvousDeadline && rendezvousDeadline < Date.now()) return + + const cid = clusterId.toString('base64') + const scid = subclusterId.toString('base64') + + this._onDebug('<- JOIN (' + + `peerId=${peerId.slice(0, 6)}, ` + + `clock=${packet.clock}, ` + + `hops=${packet.hops}, ` + + `clusterId=${cid.slice(0, 6)}, ` + + `subclusterId=${scid.slice(0, 6)}, ` + + `address=${address}:${port})` + ) + + if (this.onJoin && this.clusters[cid]) { + this.onJoin(packet, peer, port, address) + } + + // + // This packet represents a peer who wants to join the network and is a + // member of our cluster. The packet was replicated though the network + // and contains the details about where the peer can be reached, in this + // case we want to ping that peer so we can be introduced to them. + // + if (rendezvousDeadline && !this.indexed && this.clusters[cid]) { + if (!packet.message.rendezvousRequesterPeerId) { + const pid = packet.packetId.toString('hex') + this.gate.set(pid, 2) + + // TODO it would tighten up the transition time between dropped peers + // if we check strategy from (packet.message.natType, this.natType) and + // make introductions that create more mutually known peers. + this._onDebug(`<- JOIN RENDEZVOUS RECV (dest=${packet.message.requesterPeerId?.slice(0, 6)}, to=${peerAddress}:${peerPort}, via=${packet.message.rendezvousAddress}:${packet.message.rendezvousPort})`) + + const data = await Packet.encode(new PacketJoin({ + clock: packet.clock, + subclusterId: packet.subclusterId, + clusterId: packet.clusterId, + message: { + requesterPeerId: this.peerId, + natType: this.natType, + address: this.address, + port: this.port, + rendezvousType: packet.message.natType, + rendezvousRequesterPeerId: packet.message.requesterPeerId + } + })) + + this.send( + data, + packet.message.rendezvousPort, + packet.message.rendezvousAddress + ) + + this._onDebug(`-> JOIN RENDEZVOUS SEND ( to=${packet.message.rendezvousAddress}:${packet.message.rendezvousPort})`) + } + } + + const filter = p => ( + p.connected && // you can't intro peers who aren't connected + p.peerId !== packet.message.requesterPeerId && + p.peerId !== packet.message.rendezvousRequesterPeerId && + !p.indexed + ) + + let peers = this.getPeers(packet, this.peers, [{ port, address }], filter) + + // + // A peer who belongs to the same cluster as the peer who's replicated + // join was discovered, sent us a join that has a specification for who + // they want to be introduced to. + // + if (packet.message.rendezvousRequesterPeerId && this.peerId === packet.message.rendezvousPeerId) { + const peer = this.peers.find(p => p.peerId === packet.message.rendezvousRequesterPeerId) + + if (!peer) { + this._onDebug('<- INTRO FROM RENDEZVOUS FAILED', packet) + return + } + + // peer.natType = packet.message.rendezvousType + peers = [peer] + + this._onDebug(`<- JOIN EXECUTING RENDEZVOUS (from=${packet.message.address}:${packet.message.port}, to=${peer.address}:${peer.port})`) + } + + for (const peer of peers) { + const message1 = { + requesterPeerId: peer.peerId, + responderPeerId: this.peerId, + isRendezvous: !!packet.message.rendezvousPeerId, + natType: peer.natType, + address: peer.address, + port: peer.port + } + + const message2 = { + requesterPeerId: packet.message.requesterPeerId, + responderPeerId: this.peerId, + isRendezvous: !!packet.message.rendezvousPeerId, + natType: packet.message.natType, + address: packet.message.address, + port: packet.message.port + } + + const opts = { + hops: packet.hops + 1, + clusterId, + subclusterId, + usr1: String(Date.now()) + } + + const intro1 = await Packet.encode(new PacketIntro({ ...opts, message: message1 })) + const intro2 = await Packet.encode(new PacketIntro({ ...opts, message: message2 })) + + // + // Send intro1 to the peer described in the message + // Send intro2 to the peer in this loop + // + this._onDebug(`>> INTRO SEND (from=${peer.address}:${peer.port}, to=${packet.message.address}:${packet.message.port})`) + this._onDebug(`>> INTRO SEND (from=${packet.message.address}:${packet.message.port}, to=${peer.address}:${peer.port})`) + + peer.lastRequest = Date.now() + + this.send(intro2, peer.port, peer.address) + this.send(intro1, packet.message.port, packet.message.address) + + this.gate.set(Packet.decode(intro1).packetId.toString('hex'), 2) + this.gate.set(Packet.decode(intro2).packetId.toString('hex'), 2) + } + + this.gate.set(packet.packetId.toString('hex'), 2) + + if (packet.hops >= this.maxHops) return + if (this.indexed && !packet.clusterId) return + + if (packet.hops === 1 && this.natType === NAT.UNRESTRICTED && !packet.message.rendezvousDeadline) { + packet.message.rendezvousAddress = this.address + packet.message.rendezvousPort = this.port + packet.message.rendezvousType = this.natType + packet.message.rendezvousPeerId = this.peerId + packet.message.rendezvousDeadline = Date.now() + this.config.keepalive + } + + this._onDebug(`-> JOIN RELAY (peerId=${peerId.slice(0, 6)}, from=${peerAddress}:${peerPort})`) + this.mcast(packet, [{ port, address }, { port: peerPort, address: peerAddress }]) + + if (packet.hops <= 1) { + this._onDebug('<- CONNECTION (source=join)') + this._onConnection(packet, packet.message.requesterPeerId, port, address) + } + } + + /** + * Received an Publish Packet + * @return {undefined} + * @ignore + */ + async _onPublish (packet, port, address, data) { + this.metrics.i[packet.type]++ + + // only cache if this packet if i am part of this subclusterId + // const cluster = this.clusters[packet.clusterId] + // if (cluster && cluster[packet.subclusterId]) { + + const pid = packet.packetId.toString('hex') + const cid = packet.clusterId.toString('base64') + const scid = packet.subclusterId.toString('base64') + + if (this.gate.has(pid)) { + this.metrics.i.DROPPED++ + return + } + + this.gate.set(pid, 6) + + if (this.cache.has(pid)) { + this.metrics.i.DROPPED++ + this._onDebug(`<- DROP (packetId=${pid.slice(0, 6)}, clusterId=${cid.slice(0, 6)}, subclueterId=${scid.slice(0, 6)}, from=${address}:${port}, hops=${packet.hops})`) + return + } + + this.cacheInsert(packet) + + const ignorelist = [{ address, port }] + + if (!this.indexed && this.encryption.has(scid)) { + let p = packet.copy() + + if (p.index > -1) { + this._onDebug(`<- PUBLISH REQUIRES COMPOSE (packetId=${pid.slice(0, 6)}, index=${p.index}, from=${address}:${port})`) + + p = await this.cache.compose(p) + if (p?.isComposed) this._onDebug(`<- PUBLISH COMPOSED (packetId=${pid.slice(0, 6)}, from=${address}:${port})`) + } + + if (p?.index === -1 && this.onPacket) { + this._onDebug(`<- PUBLISH (packetId=${pid.slice(0, 6)}, from=${address}:${port})`) + this.onPacket(p, port, address) + } + } else { + this._onDebug(`<- PUBLISH (packetId=${pid.slice(0, 6)}, index=${packet.index}, from=${address}:${port})`) + } + + if (packet.hops >= this.maxHops) return + + this.mcast(packet, ignorelist) + + // } + } + + /** + * Received an Stream Packet + * @return {undefined} + * @ignore + */ + async _onStream (packet, port, address, data) { + this.metrics.i[packet.type]++ + + const pid = packet.packetId.toString('hex') + + const streamTo = packet.usr3.toString('hex') + const streamFrom = packet.usr4.toString('hex') + + // only help packets with a higher hop count if they are in our cluster + // if (packet.hops > 2 && !this.clusters[packet.cluster]) return + + this._onDebug(`<- STREAM (from=${address}:${port}, pid=${pid}, hops=${packet.hops}, to=${streamTo}, from=${streamFrom})`) + + // stream message is for this peer + if (streamTo === this.peerId) { + if (this.gate.has(pid)) return + this.gate.set(pid, 1) + + this._onDebug(`<- STREAM ACCEPTED (received=true, from=${address}:${port})`) + const scid = packet.subclusterId.toString('base64') + + if (this.encryption.has(scid)) { + let p = packet.copy() // clone the packet so it's not modified + + if (packet.index > -1) { // if it needs to be composed... + if (packet.index === 0) this.streamBuffer.clear() + p.timestamp = Date.now() + this.streamBuffer.set(pid, p) // cache the partial + + p = await this.cache.compose(p, this.streamBuffer) // try to compose + if (!p) return // could not compose + + this._onDebug(`<- STREAM COMPOSED (pid=${pid.slice(0, 6)}, bufsize=${this.streamBuffer.size})`) + + const previousId = p.index === 0 ? p.packetId : p.previousId + const parentId = previousId.toString('hex') + + this.streamBuffer.forEach((v, k) => { + if (k === parentId) this.streamBuffer.delete(k) + if (v.previousId.compare(previousId) === 0) this.streamBuffer.delete(k) + }) + } + + this._onDebug(`<- STREAM COMPLETE (pid=${pid.slice(0, 6)}, bufsize=${this.streamBuffer.size})`) + + if (this.onStream) { + const peerFrom = this.peers.find(p => p.peerId === streamFrom) + if (peerFrom) this.onStream(p, peerFrom, port, address) + } + } + + return + } + + // stream message is for another peer + const peerTo = this.peers.find(p => p.peerId === streamTo) + if (!peerTo) { + this._onDebug(`XX STREAM RELAY FORWARD DESTINATION NOT REACHABLE (to=${streamTo})`) + return + } + + if (packet.hops >= this.maxHops) { + this._onDebug(`XX STREAM RELAY MAX HOPS EXCEEDED (to=${streamTo})`) + return + } + + this._onDebug(`>> STREAM RELAY (to=${peerTo.address}:${peerTo.port}, id=${peerTo.peerId.slice(0, 6)})`) + // I am the proxy! + this.send(await Packet.encode(packet), peerTo.port, peerTo.address) + + // + // What % of packets hit the server. + // + + // if (packet.hops === 1 && this.natType === NAT.UNRESTRICTED) { + // this.mcast(packet, [{ port, address }, { port: peerFrom.port, address: peerFrom.address }]) + // } + } + + /** + * Received any packet on the probe port to determine the firewall: + * are you port restricted, host restricted, or unrestricted. + * @return {undefined} + * @ignore + */ + _onProbeMessage (data, { port, address }) { + this._clearTimeout(this.probeReflectionTimeout) + + const packet = Packet.decode(data) + if (!packet || packet.version !== VERSION) return + if (packet?.type !== PacketPong.type) return + + const pid = packet.packetId.toString('hex') + if (this.gate.has(pid)) return + this.gate.set(pid, 1) + + const { reflectionId } = packet.message + this._onDebug(`<- NAT PROBE (from=${address}:${port}, stage=${this.reflectionStage}, id=${reflectionId})`) + + if (this.onProbe) this.onProbe(data, port, address) + if (this.reflectionId !== reflectionId || !this.reflectionId) return + + // reflection stage is encoded in the last hex char of the reflectionId, or 0 if not available. + // const reflectionStage = reflectionId ? parseInt(reflectionId.slice(-1), 16) : 0 + + if (this.reflectionStage === 1) { + this._onDebug('<- NAT REFLECT - STAGE1: probe received', reflectionId) + if (!packet.message?.port) return // message must include a port number + + // successfully discovered the probe socket external port + this.config.probeExternalPort = packet.message.port + + // move to next reflection stage + this.reflectionStage = 1 + this.reflectionId = null + this.requestReflection() + return + } + + if (this.reflectionStage === 2) { + this._onDebug('<- NAT REFLECT - STAGE2: probe received', reflectionId) + + // if we have previously sent an outbount message to this peer on the probe port + // then our NAT will have a mapping for their IP, but not their IP+Port. + if (!NAT.isFirewallDefined(this.nextNatType)) { + this.nextNatType |= NAT.FIREWALL_ALLOW_KNOWN_IP + this._onDebug(`<> PROBE STATUS: NAT.FIREWALL_ALLOW_KNOWN_IP (${packet.message.port} -> ${this.nextNatType})`) + } else { + this.nextNatType |= NAT.FIREWALL_ALLOW_ANY + this._onDebug(`<> PROBE STATUS: NAT.FIREWALL_ALLOW_ANY (${packet.message.port} -> ${this.nextNatType})`) + } + + // wait for all messages to arrive + } + } + + /** + * When a packet is received it is decoded, the packet contains the type + * of the message. Based on the message type it is routed to a function. + * like WebSockets, don't answer queries unless we know its another SRP peer. + * + * @param {Buffer|Uint8Array} data + * @param {{ port: number, address: string }} info + */ + async _onMessage (data, { port, address }) { + const packet = Packet.decode(data) + if (!packet || packet.version !== VERSION) return + + const peer = this.peers.find(p => p.address === address && p.port === port) + if (peer) peer.lastUpdate = Date.now() + + const cid = Buffer.from(packet.clusterId).toString('base64') + const scid = Buffer.from(packet.subclusterId).toString('base64') + + // onDebug('<- PACKET', packet.type, port, address) + const clusters = this.clusters[cid] + const subcluster = clusters && clusters[scid] + + if (!this.config.limitExempt) { + if (rateLimit(this.rates, packet.type, port, address, subcluster)) { + this._onDebug(`XX RATE LIMIT HIT (from=${address}, type=${packet.type})`) + this.metrics.i.DROPPED++ + return + } + if (this.onLimit && !this.onLimit(packet, port, address)) return + } + + const args = [packet, port, address, data] + + if (this.firewall) if (!this.firewall(...args)) return + if (this.onData) this.onData(...args) + + switch (packet.type) { + case PacketPing.type: return this._onPing(...args) + case PacketPong.type: return this._onPong(...args) + } + + if (!this.natType && !this.indexed) return + + switch (packet.type) { + case PacketIntro.type: return this._onIntro(...args) + case PacketJoin.type: return this._onJoin(...args) + case PacketPublish.type: return this._onPublish(...args) + case PacketStream.type: return this._onStream(...args) + case PacketSync.type: return this._onSync(...args) + case PacketQuery.type: return this._onQuery(...args) + } + } + + /** + * Test a peerID is valid + * + * @param {string} pid + * @returns boolean + */ + static isValidPeerId (pid) { + return typeof pid === 'string' && PEERID_REGEX.test(pid) + } + + /** + * Test a reflectionID is valid + * + * @param {string} rid + * @returns boolean + */ + static isValidReflectionId (rid) { + return typeof rid === 'string' && /^[A-Fa-f0-9]{12}$/.test(rid) + } + + /** + * Test a pingID is valid + * + * @param {string} pid + * @returns boolean + */ + static isValidPingId (pid) { + return typeof pid === 'string' && /^[A-Fa-f0-9]{12,13}$/.test(pid) + + // the above line is provided for backwards compatibility due to a breaking change introduced in: + // https://github.com/socketsupply/latica/commit/f02db9e37ad3ed476cebc7f6269738f4e0c9acaf + // once all peers have received that commit we can enforce an exact length of 12 hex chars: + // return typeof pid === 'string' && /^[A-Fa-f0-9]{12}$/.test(pid) + } + + /** + * Returns the online status of the browser, else true. + * + * note: globalThis.navigator was added to node in v22. + * + * @returns boolean + */ + static onLine () { + return globalThis.navigator?.onLine !== false + } +} + +export default Peer diff --git a/api/stream-relay/nat.js b/api/latica/nat.js similarity index 100% rename from api/stream-relay/nat.js rename to api/latica/nat.js diff --git a/api/stream-relay/packets.js b/api/latica/packets.js similarity index 67% rename from api/stream-relay/packets.js rename to api/latica/packets.js index 4f188815e3..6e0dae1c5a 100644 --- a/api/stream-relay/packets.js +++ b/api/latica/packets.js @@ -1,7 +1,7 @@ import { randomBytes } from '../crypto.js' import { isBufferLike } from '../util.js' import { Buffer } from '../buffer.js' -import debug from './index.js' +import Peer, { Cache, NAT } from './index.js' /** * Hash function factory. @@ -141,25 +141,46 @@ export const PACKET_BYTES = FRAME_BYTES + MESSAGE_BYTES export const MAX_HOPS = 16 /** - * @param {object} message - * @param {{ [key: string]: { required: boolean, type: string }}} constraints + * @typedef constraint + * @type {Object} + * @property {string} type + * @property {boolean} [required] + * @property {function} [assert] optional validator fn returning boolean */ -export const validatePacket = (o, constraints) => { - if (!o) throw new Error('Expected object') + +/** + * @param {object} o + * @param {{ [key: string]: constraint }} constraints + */ +export const validateMessage = (o, constraints) => { + if (({}).toString.call(o) !== '[object Object]') throw new Error('expected object') + if (({}).toString.call(constraints) !== '[object Object]') throw new Error('expected constraints') + const allowedKeys = Object.keys(constraints) const actualKeys = Object.keys(o) - const unknown = actualKeys.filter(k => allowedKeys.indexOf(k) === -1) - if (unknown.length) throw new Error(`Unexpected keys [${unknown}]`) + const unknownKeys = actualKeys.filter(k => allowedKeys.indexOf(k) === -1) + if (unknownKeys.length) throw new Error(`unexpected keys [${unknownKeys}]`) for (const [key, con] of Object.entries(constraints)) { - if (con.required && !o[key]) { - debug(new Error(`${key} is required (${JSON.stringify(o, null, 2)})`), JSON.stringify(o)) + const unset = !Object.prototype.hasOwnProperty.call(o, key) + + if (con.required && unset) { + // console.warn(new Error(`${key} is required (${JSON.stringify(o, null, 2)})`), JSON.stringify(o)) + throw new Error(`key '${key}' is required`) } + const empty = typeof o[key] === 'undefined' + if (empty) return // nothing to validate + const type = ({}).toString.call(o[key]).slice(8, -1).toLowerCase() + if (type !== con.type) { + // console.warn(`expected .${key} to be of type ${con.type}, got ${type} in packet.. ` + JSON.stringify(o)) + throw new Error(`expected '${key}' to be of type '${con.type}', got '${type}'`) + } - if (o[key] && type !== con.type) { - debug(`expected .${key} to be of type ${con.type}, got ${type} in packet.. ` + JSON.stringify(o)) + if (typeof con.assert === 'function' && !con.assert(o[key])) { + // console.warn(`expected .${key} to be of type ${con.type}, got ${type} in packet.. ` + JSON.stringify(o)) + throw new Error(`expected '${key}' to pass constraint assertion`) } } } @@ -194,7 +215,7 @@ export const decode = buf => { buf = buf.slice(MAGIC_BYTES) - const o = new Packet() + const o = {} let offset = 0 for (const [k, spec] of Object.entries(PACKET_SPEC)) { @@ -229,7 +250,7 @@ export const decode = buf => { } } - return o + return Packet.from(o) } export const getTypeFromBytes = (buf) => buf.byteLength > 4 ? buf.readUInt8(4) : 0 @@ -251,8 +272,19 @@ export class Packet { * @return {Packet} */ static from (packet) { - if (packet instanceof Packet) return packet - return new Packet(packet) + if (packet instanceof Packet && packet.constructor !== Packet) return packet + + switch (packet.type) { + case PacketPing.type: return new PacketPing(packet) + case PacketPong.type: return new PacketPong(packet) + case PacketIntro.type: return new PacketIntro(packet) + case PacketJoin.type: return new PacketJoin(packet) + case PacketPublish.type: return new PacketPublish(packet) + case PacketStream.type: return new PacketStream(packet) + case PacketSync.type: return new PacketSync(packet) + case PacketQuery.type: return new PacketQuery(packet) + default: throw new Error('invalid packet type', packet.type) + } } /** @@ -260,7 +292,8 @@ export class Packet { * @return {Packet} */ copy () { - return Object.assign(new Packet({}), this) + const PacketConstructor = this.constructor + return Object.assign(new PacketConstructor({}), this) } /** @@ -320,7 +353,7 @@ export class Packet { p.message = String(p.message) } - if (p.message?.length > Packet.MESSAGE_BYTES) throw new Error('ETOOBIG') + if (p.message?.length > Packet.maxLength) throw new Error('ETOOBIG') // we only have p.nextId when we know ahead of time, if it's empty that's fine. if (p.packetId.length === 1 && p.packetId[0] === 0) { @@ -354,7 +387,6 @@ export class Packet { continue } - // console.log(k, value, spec) const encoded = Buffer.from(value || spec.default, spec.encoding) if (value?.length && encoded.length > spec.bytes) { @@ -369,28 +401,32 @@ export class Packet { bufs.push(buf) } - return Buffer.concat(bufs, FRAME_BYTES) + return Buffer.concat(bufs) } static decode (buf) { - return decode(buf) + try { + return decode(buf) + } catch (e) { + console.warn('failed to decode packet', e) + return null + } } } export class PacketPing extends Packet { static type = 1 - constructor ({ message, clusterId, subclusterId }) { - super({ type: PacketPing.type, message, clusterId, subclusterId }) - - validatePacket(message, { - requesterPeerId: { required: true, type: 'string' }, - cacheSummaryHash: { type: 'string' }, - probeExternalPort: { type: 'number' }, - reflectionId: { type: 'string' }, - pingId: { type: 'string' }, - natType: { type: 'number' }, + constructor (args) { + super({ ...args, type: PacketPing.type }) + + validateMessage(args.message, { + requesterPeerId: { required: true, type: 'string', assert: Peer.isValidPeerId }, + cacheSummaryHash: { type: 'string', assert: Cache.isValidSummaryHashFormat }, + probeExternalPort: { type: 'number', assert: isValidPort }, + reflectionId: { type: 'string', assert: Peer.isValidReflectionId }, + pingId: { type: 'string', assert: Peer.isValidPingId }, + natType: { type: 'number', assert: NAT.isValid }, uptime: { type: 'number' }, - isHeartbeat: { type: 'number' }, cacheSize: { type: 'number' }, isConnection: { type: 'boolean' }, isReflection: { type: 'boolean' }, @@ -403,23 +439,22 @@ export class PacketPing extends Packet { export class PacketPong extends Packet { static type = 2 - constructor ({ message, clusterId, subclusterId }) { - super({ type: PacketPong.type, message, clusterId, subclusterId }) + constructor (args) { + super({ ...args, type: PacketPong.type }) - validatePacket(message, { - requesterPeerId: { required: true, type: 'string' }, - responderPeerId: { required: true, type: 'string' }, - cacheSummaryHash: { type: 'string' }, + validateMessage(args.message, { + requesterPeerId: { required: true, type: 'string', assert: Peer.isValidPeerId }, + responderPeerId: { required: true, type: 'string', assert: Peer.isValidPeerId }, + cacheSummaryHash: { type: 'string', assert: Cache.isValidSummaryHashFormat }, port: { type: 'number' }, address: { type: 'string' }, uptime: { type: 'number' }, cacheSize: { type: 'number' }, - natType: { type: 'number' }, + natType: { type: 'number', assert: NAT.isValid }, isReflection: { type: 'boolean' }, - isHeartbeat: { type: 'number' }, isConnection: { type: 'boolean' }, - reflectionId: { type: 'string' }, - pingId: { type: 'string' }, + reflectionId: { type: 'string', assert: Peer.isValidReflectionId }, + pingId: { type: 'string', assert: Peer.isValidPingId }, isDebug: { type: 'boolean' }, isProbe: { type: 'boolean' }, rejected: { type: 'boolean' } @@ -429,16 +464,16 @@ export class PacketPong extends Packet { export class PacketIntro extends Packet { static type = 3 - constructor ({ clock, hops, clusterId, subclusterId, usr1, message }) { - super({ type: PacketIntro.type, clock, hops, clusterId, subclusterId, usr1, message }) + constructor (args) { + super({ ...args, type: PacketIntro.type }) - validatePacket(message, { - requesterPeerId: { required: true, type: 'string' }, - responderPeerId: { required: true, type: 'string' }, + validateMessage(args.message, { + requesterPeerId: { required: true, type: 'string', assert: Peer.isValidPeerId }, + responderPeerId: { required: true, type: 'string', assert: Peer.isValidPeerId }, isRendezvous: { type: 'boolean' }, - natType: { required: true, type: 'number' }, + natType: { required: true, type: 'number', assert: NAT.isValid }, address: { required: true, type: 'string' }, - port: { required: true, type: 'number' }, + port: { required: true, type: 'number', assert: isValidPort }, timestamp: { type: 'number' } }) } @@ -446,52 +481,54 @@ export class PacketIntro extends Packet { export class PacketJoin extends Packet { static type = 4 - constructor ({ clock, hops, clusterId, subclusterId, message }) { - super({ type: PacketJoin.type, clock, hops, clusterId, subclusterId, message }) + constructor (args) { + super({ ...args, type: PacketJoin.type }) - validatePacket(message, { + validateMessage(args.message, { rendezvousAddress: { type: 'string' }, - rendezvousPort: { type: 'number' }, - rendezvousType: { type: 'number' }, - rendezvousPeerId: { type: 'string' }, + rendezvousPort: { type: 'number', assert: isValidPort }, + rendezvousType: { type: 'number', assert: NAT.isValid }, + rendezvousPeerId: { type: 'string', assert: Peer.isValidPeerId }, rendezvousDeadline: { type: 'number' }, - rendezvousRequesterPeerId: { type: 'string' }, - requesterPeerId: { required: true, type: 'string' }, - natType: { required: true, type: 'number' }, - initial: { type: 'boolean' }, + rendezvousRequesterPeerId: { type: 'string', assert: Peer.isValidPeerId }, + requesterPeerId: { required: true, type: 'string', assert: Peer.isValidPeerId }, + natType: { required: true, type: 'number', assert: NAT.isValid }, address: { required: true, type: 'string' }, - port: { required: true, type: 'number' }, + key: { type: 'string' }, + port: { required: true, type: 'number', assert: isValidPort }, isConnection: { type: 'boolean' } }) } } export class PacketPublish extends Packet { - static type = 5 // no need to validatePacket, message is whatever you want - constructor ({ message, sig, packetId, clusterId, subclusterId, nextId, clock, hops, usr1, usr2, ttl, previousId }) { - super({ type: PacketPublish.type, message, sig, packetId, clusterId, subclusterId, nextId, clock, hops, usr1, usr2, ttl, previousId }) + static type = 5 // no need to validateMessage, message is whatever you want + constructor (args) { + super({ ...args, type: PacketPublish.type }) } } export class PacketStream extends Packet { static type = 6 - constructor ({ message, sig, packetId, clusterId, subclusterId, nextId, clock, ttl, usr1, usr2, usr3, usr4, previousId }) { - super({ type: PacketStream.type, message, sig, packetId, clusterId, subclusterId, nextId, clock, ttl, usr1, usr2, usr3, usr4, previousId }) + constructor (args) { + super({ ...args, type: PacketStream.type }) } } export class PacketSync extends Packet { static type = 7 - constructor ({ packetId, message = Buffer.from([0b0]) }) { - super({ type: PacketSync.type, packetId, message }) + constructor (args) { + super({ message: Buffer.from([0b0]), ...args, type: PacketSync.type }) } } export class PacketQuery extends Packet { static type = 8 - constructor ({ packetId, previousId, subclusterId, usr1, usr2, usr3, usr4, message = {} }) { - super({ type: PacketQuery.type, packetId, previousId, subclusterId, usr1, usr2, usr3, usr4, message }) + constructor (args) { + super({ message: {}, ...args, type: PacketQuery.type }) } } export default Packet + +const isValidPort = (n) => typeof n === 'number' && (n & 0xFFFF) === n && n !== 0 diff --git a/api/latica/proxy.js b/api/latica/proxy.js new file mode 100644 index 0000000000..f89e4fd8c1 --- /dev/null +++ b/api/latica/proxy.js @@ -0,0 +1,308 @@ +/** + * A utility to run run the protocol in a thread seperate from the UI. + * + * import { network } from + * + * Socket Node + * --- --- + * API API + * Proxy Protocol + * Protocol + * + */ + +function deepClone (object, map = new Map()) { + if (map.has(object)) return map.get(object) + + const isNull = object === null + const isNotObject = typeof object !== 'object' + const isArrayBuffer = object instanceof ArrayBuffer + const isNodeBuffer = object?.constructor?.name === 'Buffer' + const isArray = Array.isArray(object) + const isUint8Array = object instanceof Uint8Array + const isMessagePort = object instanceof MessagePort + + if (isMessagePort || isNotObject || isNull || isArrayBuffer) return object + if (isUint8Array) return new Uint8Array(object) + if (isNodeBuffer) return Uint8Array.from(object) + if (isArrayBuffer) return object.slice(0) + + if (isArray) { + const clonedArray = [] + map.set(object, clonedArray) + for (const item of object) { + clonedArray.push(deepClone(item, map)) + } + return clonedArray + } + + const clonedObj = {} + map.set(object, clonedObj) + for (const key in object) { + clonedObj[key] = deepClone(object[key], map) + } + + return clonedObj +} + +function transferOwnership (...objects) { + const transfers = [] + + function add (value) { + if (!transfers.includes(value)) { + transfers.push(value) + } + } + + objects.forEach(object => { + if (object instanceof ArrayBuffer || ArrayBuffer.isView(object)) { + add(object.buffer) + } else if (Array.isArray(object) || (object && typeof object === 'object')) { + for (const value of Object.values(object)) { + if (value instanceof MessagePort) add(value) + } + } + }) + + return transfers +} + +/** + * `Proxy` class factory, returns a Proxy class that is a proxy to the Peer. + * @param {{ createSocket: function('udp4', null, object?): object }} options + */ +class PeerWorkerProxy { + #promises = new Map() + #channel = null + #worker = null + #index = 0 + #port = null + + constructor (options, port, fn) { + if (!options.isWorkerThread) { + this.#channel = new MessageChannel() + this.#worker = new globalThis.Worker(new URL('./worker.js', import.meta.url)) + + this.#worker.addEventListener('error', err => { + throw err + }) + + this.#worker.postMessage({ + port: this.#channel.port2 + }, [this.#channel.port2]) + + // when the main thread receives a message from the worker + this.#channel.port1.onmessage = ({ data: args }) => { + const { + err, + prop, + data, + seq + } = args + + if (!prop && err) { + throw new Error(err) + } + + if (prop && typeof this[prop] === 'function') { + try { + if (Array.isArray(data)) { + this[prop](...data) + } else { + this[prop](data) + } + } catch (err) { + throw new Error(`Unable to call ${prop} (${err.message})`) + } + return + } + + const p = this.#promises.get(seq) + if (!p) return + + if (!p) { + console.warn(`No promise was found for the sequence (${seq})`) + return + } + + if (err) { + p.reject(err) + } else { + p.resolve(data) + } + + this.#promises.delete(seq) + } + + this.callWorkerThread('create', options) + return + } + + this.#port = port + this.#port.onmessage = fn.bind(this) + } + + async init () { + return await this.callWorkerThread('init') + } + + async reconnect () { + return await this.callWorkerThread('reconnect') + } + + async disconnect () { + return await this.callWorkerThread('disconnect') + } + + async getInfo () { + return await this.callWorkerThread('getInfo') + } + + async getMetrics () { + return await this.callWorkerThread('metrics') + } + + async getState () { + return await this.callWorkerThread('getState') + } + + async open (...args) { + return await this.callWorkerThread('open', args) + } + + async seal (...args) { + return await this.callWorkerThread('seal', args) + } + + async sealUnsigned (...args) { + return await this.callWorkerThread('sealUnsigned', args) + } + + async openUnsigned (...args) { + return await this.callWorkerThread('openUnsigned', args) + } + + async addEncryptionKey (...args) { + return await this.callWorkerThread('addEncryptionKey', args) + } + + async send (...args) { + return await this.callWorkerThread('send', args) + } + + async sendUnpublished (...args) { + return await this.callWorkerThread('sendUnpublished', args) + } + + async cacheInsert (...args) { + return await this.callWorkerThread('cacheInsert', args) + } + + async mcast (...args) { + return await this.callWorkerThread('mcast', args) + } + + async requestReflection (...args) { + return await this.callWorkerThread('requestReflection', args) + } + + async stream (...args) { + return await this.callWorkerThread('stream', args) + } + + async join (...args) { + return await this.callWorkerThread('join', args) + } + + async publish (...args) { + return await this.callWorkerThread('publish', args) + } + + async sync (...args) { + return await this.callWorkerThread('sync', args) + } + + async close (...args) { + return await this.callWorkerThread('close', args) + } + + async query (...args) { + return await this.callWorkerThread('query', args) + } + + async compileCachePredicate (src) { + return await this.callWorkerThread('compileCachePredicate', src) + } + + callWorkerThread (prop, data) { + let transfer = [] + + if (data) { + data = deepClone(data) + transfer = transferOwnership(data) + } + + const seq = ++this.#index + const { promise, resolve, reject } = Promise.withResolvers() + const d = promise + d.resolve = resolve + d.reject = reject + + this.#channel.port1.postMessage( + { prop, data, seq }, + { transfer } + ) + + this.#promises.set(seq, d) + return d + } + + callMainThread (prop, args) { + for (const i in args) { + const arg = args[i] + + if (arg?.constructor.name === 'RemotePeer' || arg?.constructor.name === 'Peer') { + args[i] = { // what details we want to expose outside of the protocol + address: arg.address, + clusters: arg.clusters, + connected: arg.connected, + lastRequest: arg.lastRequest, + lastUpdate: arg.lastUpdate, + natType: arg.natType, + peerId: arg.peerId, + port: arg.port, + proxy: !!arg.proxy, + } + + delete args[i].localPeer // don't copy this over + } + } + + try { + this.#port.postMessage( + { data: deepClone(args), prop }, + { transfer: transferOwnership(args) } + ) + } catch (err) { + this.#port.postMessage({ data: { err: err.message, prop } }) + } + } + + resolveMainThread (seq, result) { + if (result.err) { // err result of the worker try/catch + return this.#port.postMessage({ data: { err: result.err.message } }) + } + + try { + this.#port.postMessage( + { data: deepClone(result.data), seq }, + { transfer: transferOwnership(result.data) } + ) + } catch (err) { // can't post the data + this.#port.postMessage({ data: { err: err.message } }) + } + } +} + +export { PeerWorkerProxy } +export default PeerWorkerProxy diff --git a/api/latica/worker.js b/api/latica/worker.js new file mode 100644 index 0000000000..0e805d10c9 --- /dev/null +++ b/api/latica/worker.js @@ -0,0 +1,82 @@ +import { Peer } from './index.js' +import { PeerWorkerProxy } from './proxy.js' +import dgram from '../dgram.js' + +let proxy +let peer + +globalThis.addEventListener('message', ({ data: source }) => { + if (proxy) return + + const opts = { isWorkerThread: true } + + proxy = new PeerWorkerProxy(opts, source.port, async function ({ data: args }) { + const { + prop, + data, + seq + } = args + + switch (prop) { + case 'create': { + peer = new Peer(data, dgram) + + peer.onDebug = (...args) => this.callMainThread('onDebug', args) + peer.onConnecting = (...args) => this.callMainThread('onConnecting', args) + peer.onConnection = (...args) => this.callMainThread('onConnection', args) + peer.onDisconnection = (...args) => this.callMainThread('onDisconnection', args) + peer.onPacket = (...args) => this.callMainThread('onPacket', args) + peer.onStream = (...args) => this.callMainThread('onStream', args) + peer.onData = (...args) => this.callMainThread('onData', args) + peer.onSend = (...args) => this.callMainThread('onSend', args) + peer.onFirewall = (...args) => this.callMainThread('onFirewall', args) + peer.onMulticast = (...args) => this.callMainThread('onMulticast', args) + peer.onJoin = (...args) => this.callMainThread('onJoin', args) + peer.onSync = (...args) => this.callMainThread('onSync', args) + peer.onSyncStart = (...args) => this.callMainThread('onSyncStart', args) + peer.onSyncEnd = (...args) => this.callMainThread('onSyncEnd', args) + peer.onQuery = (...args) => this.callMainThread('onQuery', args) + peer.onNat = (...args) => this.callMainThread('onNat', args) + peer.onState = (...args) => this.callMainThread('onState', args) + peer.onWarn = (...args) => this.callMainThread('onWarn', args) + peer.onError = (...args) => this.callMainThread('onError', args) + peer.onReady = (...args) => this.callMainThread('onReady', args) + break + } + + case 'compileCachePredicate': { + // eslint-disable-next-line + let predicate = new Function(`return ${data.toString()}`)() + predicate = predicate.bind(peer) + peer.cachePredicate = packet => predicate(packet) + break + } + + default: { + if (isNaN(seq) && peer[prop]) { + peer[prop] = data + return + } + + let r + + try { + if (typeof peer[prop] === 'function') { + if (Array.isArray(data)) { + r = await peer[prop](...data) + } else { + r = await peer[prop](data) + } + } else { + r = peer[prop] + } + } catch (err) { + console.error(err) + return this.resolveMainThread(seq, { err }) + } + + this.resolveMainThread(seq, { data: r }) + } + } + }) +}) diff --git a/api/location.js b/api/location.js index f73773c1a7..180fecd224 100644 --- a/api/location.js +++ b/api/location.js @@ -1,38 +1,68 @@ -export const globalLocation = globalThis.location ?? { - origin: 'socket:///', - host: '', - hostname: '', - pathname: '/', - href: '' -} +export class Location { + get url () { + if (globalThis.location === this) { + return null + } -export const href = globalLocation.href.replace(/https?:/, 'socket:') -export const protocol = 'socket:' -export const hostname = ( - // eslint-disable-next-line - href.match(/^[a-zA-Z]+:[\/]{2}?(.*)(\/.*)$/) ?? [] -)[1] ?? '' + if (globalThis.location.href.startsWith('blob:')) { + return new URL(globalThis.RUNTIME_WORKER_LOCATION || globalThis.location.pathname) + } -export const host = hostname -export const search = href.split('?')[1] ?? '' -export const hash = href.split('#')[1] ?? '' -export const pathname = href.slice( - href.indexOf(hostname) + hostname.length -) + if (globalThis.location.origin === 'null') { + return new URL( + globalThis.location.pathname + + globalThis.location.search + + globalThis.location.hash, + globalThis.__args?.config?.meta_bundle_identifier ?? 'null' + ) + } -export const origin = `${protocol}//${(host + pathname).replace(/\/\//g, '/')}` + return new URL(globalThis.location.href) + } -export function toString () { - return href -} + get protocol () { + return 'socket:' + } + + get host () { + return this.url.host + } + + get hostname () { + return this.url.hostname + } + + get port () { + return this.url.port + } + + get pathname () { + return this.url.pathname + } -export default { - origin, - href, - protocol, - hostname, - host, - search, - pathname, - toString + get search () { + return this.url.search + } + + get origin () { + const origin = this.url.origin && this.url.origin !== 'null' + ? this.url.origin + : globalThis.origin || globalThis.location.origin + + return origin.replace('https://', 'socket://') + } + + get href () { + return this.url.href + } + + get hash () { + return this.url.hash + } + + toString () { + return this.href + } } + +export default new Location() diff --git a/api/mime/application.json b/api/mime/application.json index f684ee3864..8a5b871615 100644 --- a/api/mime/application.json +++ b/api/mime/application.json @@ -168,9 +168,7 @@ "fhir+xml": "application/fhir+xml", "fits": "application/fits", "flexfec": "application/flexfec", - "font-sfnt": "-", "font-tdpfr": "application/font-tdpfr", - "font-woff": "-", "framework-attributes+xml": "application/framework-attributes+xml", "geo+json": "application/geo+json", "geo+json-seq": "application/geo+json-seq", @@ -865,7 +863,6 @@ "vnd.gerber": "application/vnd.gerber", "vnd.globalplatform.card-content-mgt": "application/vnd.globalplatform.card-content-mgt", "vnd.globalplatform.card-content-mgt-response": "application/vnd.globalplatform.card-content-mgt-response", - "vnd.gmx": "-", "vnd.gnu.taler.exchange+json": "application/vnd.gnu.taler.exchange+json", "vnd.gnu.taler.merchant+json": "application/vnd.gnu.taler.merchant+json", "vnd.google-earth.kml+xml": "application/vnd.google-earth.kml+xml", diff --git a/api/mime/audio.json b/api/mime/audio.json index 48c8eb8f8c..0d9d16c281 100644 --- a/api/mime/audio.json +++ b/api/mime/audio.json @@ -155,7 +155,6 @@ "vnd.nuera.ecelp9600": "audio/vnd.nuera.ecelp9600", "vnd.octel.sbc": "audio/vnd.octel.sbc", "vnd.presonus.multitrack": "audio/vnd.presonus.multitrack", - "vnd.qcelp": "-", "vnd.rhetorex.32kadpcm": "audio/vnd.rhetorex.32kadpcm", "vnd.rip": "audio/vnd.rip", "vnd.sealedmedia.softseal.mpeg": "audio/vnd.sealedmedia.softseal.mpeg", diff --git a/api/mime/index.js b/api/mime/index.js index 3e002044f2..7131bce244 100644 --- a/api/mime/index.js +++ b/api/mime/index.js @@ -1,3 +1,6 @@ +/* global XMLHttpRequest */ +import ipc from '../ipc.js' + /** * A container for a database lookup query. */ @@ -12,10 +15,15 @@ export class DatabaseQueryResult { */ mime = '' + /** + * @type {Database?} + */ + database = null + /** * `DatabaseQueryResult` class constructor. * @ignore - * @param {Database} database + * @param {Database|null} database * @param {string} name * @param {string} mime */ @@ -84,6 +92,36 @@ export class Database { for (const [key, value] of Object.entries(json)) { this.map.set(key, value) + // @ts-ignore + this.index.set(value.toLowerCase(), key.toLowerCase()) + } + } + } + + /** + * Loads database MIME entries synchronously into internal map. + */ + loadSync () { + if (this.map.size === 0) { + const request = new XMLHttpRequest() + + request.open('GET', this.url, false) + request.send(null) + + let responseText = null + + try { + // @ts-ignore + responseText = request.responseText // can throw `InvalidStateError` error + } catch { + responseText = request.response + } + + const json = JSON.parse(responseText) + + for (const [key, value] of Object.entries(json)) { + this.map.set(key, value) + // @ts-ignore this.index.set(value.toLowerCase(), key.toLowerCase()) } } @@ -92,12 +130,30 @@ export class Database { /** * Lookup MIME type by name or content type * @param {string} query - * @return {Promise<DatabaseQueryResult>} + * @return {Promise<DatabaseQueryResult[]>} */ async lookup (query) { - query = query.toLowerCase() - await this.load() + return this.query(query) + } + + /** + * Lookup MIME type by name or content type synchronously. + * @param {string} query + * @return {Promise<DatabaseQueryResult[]>} + */ + lookupSync (query) { + this.loadSync() + return this.query(query) + } + + /** + * Queries database map and returns an array of results + * @param {string} query + * @return {DatabaseQueryResult[]} + */ + query (query) { + query = query.toLowerCase() const queryParts = query.split('+') const results = [] @@ -215,7 +271,7 @@ export const databases = [ export async function lookup (query) { const results = [] - // preload all databasees + // preload all databases await Promise.all(databases.map((db) => db.load())) for (const database of databases) { @@ -223,14 +279,133 @@ export async function lookup (query) { results.push(...result) } + if (query && results.length === 0) { + const result = await ipc.request('mime.lookup', query) + + if (result.err) { + throw result.err + } + + results.push(new DatabaseQueryResult(null, '', result.data.type)) + } + return results } +/** + * Look up a MIME type in various MIME databases synchronously. + * @param {string} query + * @return {DatabaseQueryResult[]} + */ +export async function lookupSync (query) { + const results = [] + + for (const database of databases) { + const result = database.lookupSync(query) + results.push(...result) + } + + if (query && results.length === 0) { + const result = await ipc.sendSync('mime.lookup', query) + + if (result.err) { + throw result.err + } + + results.push(new DatabaseQueryResult(null, '', result.data.type)) + } + + return results +} + +export class MIMEParams extends Map { + toString () { + return Array + .from(this.entries()) + .map((entry) => entry.join('=')) + .join(';') + } +} + +export class MIMEType { + #type = null + #params = null + #subtype = null + + constructor (input) { + input = String(input) + + const args = input.split(';') + const mime = args.shift() + const types = mime.split('/') + + if (types.length !== 2 || !types[0] || !types[1]) { + throw new TypeError(`Invalid MIMEType input given: ${mime}`) + } + + const [type, subtype] = types + + this.#type = type.toLowerCase() + // @ts-ignore + this.#params = new MIMEParams(args.map((a) => a.trim().split('=').map((v) => v.trim()))) + this.#subtype = subtype + } + + get type () { + return this.#type + } + + set type (value) { + if (!value || typeof value !== 'string') { + throw new TypeError('MIMEType type must be a string') + } + + this.#type = value + } + + get subtype () { + return this.#subtype + } + + set subtype (value) { + if (!value || typeof value !== 'string') { + throw new TypeError('MIMEType subtype must be a string') + } + + this.#subtype = value + } + + get essence () { + return `${this.type}/${this.subtype}` + } + + get params () { + return this.#params + } + + toString () { + const params = this.params.toString() + + if (params) { + return `${this.essence};${params}` + } + + return this.essence + } + + toJSON () { + return this.toString() + } +} + export default { // API Database, databases, lookup, + lookupSync, + MIMEParams, + MIMEType, // databases application, diff --git a/api/module.js b/api/module.js index 2b0c31087f..d5f1c44abf 100644 --- a/api/module.js +++ b/api/module.js @@ -1,709 +1,20 @@ -/* global XMLHttpRequest */ -/* eslint-disable import/no-duplicates, import/first, no-void, no-sequences */ /** * @module module - * A module for loading CommonJS modules with a `require` function in an - * ESM context. */ -import { ModuleNotFoundError } from './errors.js' -import { ErrorEvent, Event } from './events.js' -import { Headers } from './ipc.js' -import location from './location.js' -import { URL } from './url.js' - -import * as exports from './module.js' -export default exports - -// builtins -import buffer from './buffer.js' -import console from './console.js' -import dgram from './dgram.js' -import dns from './dns.js' -import events from './events.js' -import extension from './extension.js' -import fs from './fs.js' -import gc from './gc.js' -import ipc from './ipc.js' -import os from './os.js' -import { posix as path } from './path.js' -import process from './process.js' -import stream from './stream.js' -import test from './test.js' -import url from './url.js' -import util from './util.js' -import vm from './vm.js' - -/** - * @typedef {function(string, Module, function): undefined} ModuleResolver - */ - -const cache = new Map() - -class ModuleRequest { - id = null - url = null - - static load (pathname, parent) { - const request = new this(pathname, parent) - return request.load() - } - - constructor (pathname, parent) { - this.url = new URL(pathname, parent || location.origin || '/') - this.id = this.url.toString() - } - - status () { - const { id } = this - const request = new XMLHttpRequest() - request.open('HEAD', id, false) - request.send(null) - return request.status - } - - load () { - const { id } = this - - if (cache.has(id)) { - return cache.get(id) - } - - if (this.status() >= 400) { - return new ModuleResponse(this) - } - - const request = new XMLHttpRequest() - - request.open('GET', id, false) - request.send(null) - - const headers = Headers.from(request) - let responseText = null - - try { - // @ts-ignore - responseText = request.responseText // can throw `InvalidStateError` error - } catch { - responseText = request.response - } - - const response = new ModuleResponse(this, headers, responseText) - - if (request.status < 400 && responseText) { - cache.set(id, response) - } - - return response - } -} - -class ModuleResponse { - request = null - headers = null - data = null - - constructor (request, headers, data) { - this.request = request - this.headers = headers ?? null - this.data = data ?? null - } -} - -// sync request for files -function request (pathname, parent) { - return ModuleRequest.load(pathname, parent) -} - -/** - * CommonJS module scope with module scoped globals. - * @ignore - */ -function CommonJSModuleScope ( - exports, - require, - module, - __filename, - __dirname -) { - // eslint-disable-next-line no-unused-vars - const process = require('socket:process') - // eslint-disable-next-line no-unused-vars - const console = require('socket:console') - // eslint-disable-next-line no-unused-vars - const global = new Proxy(globalThis, { - get (target, key, receiver) { - if (key === 'process') { - return process - } - - if (key === 'console') { - return console - } - - return Reflect.get(target, key, receiver) - } - }) - - // eslint-disable-next-line no-unused-expressions - void exports, require, module, __filename, __dirname - // eslint-disable-next-line no-unused-expressions - void process, console, global - - return (async function () { - 'module code' - })() -} +import builtins, { defineBuiltin, isBuiltin } from './commonjs/builtins.js' +import { createRequire, Module } from './commonjs/module.js' /** - * A limited set of builtins exposed to CommonJS modules. + * @typedef {import('./commonjs/module.js').ModuleOptions} ModuleOptions + * @typedef {import('./commonjs/module.js').ModuleResolver} ModuleResolver + * @typedef {import('./commonjs/module.js').ModuleLoadOptions} ModuleLoadOptions + * @typedef {import('./commonjs/module.js').RequireFunction} RequireFunction + * @typedef {import('./commonjs/module.js').CreateRequireOptions} CreateRequireOptions */ -export const builtins = { - buffer, - console, - dgram, - dns, - 'dns/promises': dns.promises, - events, - extension, - fs, - 'fs/promises': fs.promises, - gc, - ipc, - module: exports, - os, - path, - process, - stream, - test, - util, - url, - vm -} -// alias +export { createRequire, Module, builtins, isBuiltin } export const builtinModules = builtins -export function isBuiltin (name) { - name = name.replace(/^(socket|node):/, '') - - if (name in builtins) { - return true - } - - return false -} - -/** - * CommonJS module scope source wrapper. - * @type {string} - */ -export const COMMONJS_WRAPPER = CommonJSModuleScope - .toString() - .split(/'module code'/) - -/** - * The main entry source origin. - * @type {string} - */ -export const MAIN_SOURCE_ORIGIN = location.href - -/** - * Creates a `require` function from a source URL. - * @param {URL|string} sourcePath - * @return {function} - */ -export function createRequire (sourcePath) { - return Module.createRequire(sourcePath) -} - -export const scope = { - current: null, - previous: null -} - -/** - * A container for a loaded CommonJS module. All errors bubble - * to the "main" module and global object (if possible). - */ -export class Module extends EventTarget { - /** - * A reference to the currently scoped module. - * @type {Module?} - */ - static get current () { return scope.current } - static set current (module) { scope.current = module } - - /** - * A reference to the previously scoped module. - * @type {Module?} - */ - static get previous () { return scope.previous } - static set previous (module) { scope.previous = module } - - /** - * Module cache. - * @ignore - */ - static cache = Object.create(null) - - /** - * Custom module resolvers. - * @type {Array<ModuleResolver>} - */ - static resolvers = [] - - /** - * CommonJS module scope source wrapper. - * @ignore - */ - static wrapper = COMMONJS_WRAPPER - - /** - * Creates a `require` function from a source URL. - * @param {URL|string} sourcePath - * @return {function} - */ - static createRequire (sourcePath) { - if (!sourcePath) { - return this.main.createRequire() - } - - return this.from(sourcePath).createRequire() - } - - /** - * The main entry module, lazily created. - * @type {Module} - */ - static get main () { - if (MAIN_SOURCE_ORIGIN in this.cache) { - return this.cache[MAIN_SOURCE_ORIGIN] - } - - const main = this.cache[MAIN_SOURCE_ORIGIN] = new Module(MAIN_SOURCE_ORIGIN) - main.filename = main.id - main.loaded = true - Object.freeze(main) - Object.seal(main) - return main - } - - /** - * Wraps source in a CommonJS module scope. - */ - static wrap (source) { - const [head, tail] = this.wrapper - const body = String(source || '') - return [head, body, tail].join('\n') - } - - /** - * Creates a `Module` from source URL and optionally a parent module. - * @param {string|URL|Module} [sourcePath] - * @param {string|URL|Module} [parent] - */ - static from (sourcePath, parent = null) { - if (!sourcePath) { - return Module.main - } else if (sourcePath?.id) { - return this.from(sourcePath.id, parent) - } else if (sourcePath instanceof URL) { - return this.from(String(sourcePath), parent) - } - - if (!parent) { - parent = Module.current - } - - const id = String(parent - ? new URL(sourcePath, parent.id) - : new URL(sourcePath, MAIN_SOURCE_ORIGIN) - ) - - if (id in this.cache) { - return this.cache[id] - } - - const module = new Module(id, parent, sourcePath) - this.cache[module.id] = module - return module - } - - /** - * The module id, most likely a file name. - * @type {string} - */ - id = '' - - /** - * The parent module, if given. - * @type {Module?} - */ - parent = null - - /** - * `true` if the module did load successfully. - * @type {boolean} - */ - loaded = false - - /** - * The module's exports. - * @type {any} - */ - exports = {} - - /** - * The filename of the module. - * @type {string} - */ - filename = '' - - /** - * Modules children to this one, as in they were required in this - * module scope context. - * @type {Array<Module>} - */ - children = [] - - /** - * The original source URL to load this module. - * @type {string} - */ - sourcePath = '' - - /** - * `Module` class constructor. - * @ignore - */ - constructor (id, parent = null, sourcePath = null) { - super() - - this.id = new URL(id || '', parent?.id || MAIN_SOURCE_ORIGIN).toString() - this.parent = parent || null - this.sourcePath = sourcePath || id - - if (!scope.previous && id !== MAIN_SOURCE_ORIGIN) { - scope.previous = Module.main - } - - if (!scope.current) { - scope.current = this - } - - this.addEventListener('error', (event) => { - // @ts-ignore - const { error } = event - if (this.isMain) { - // bubble error to globalThis, if possible - if (typeof globalThis.dispatchEvent === 'function') { - // @ts-ignore - globalThis.dispatchEvent(new ErrorEvent('error', { error })) - } - } else { - // bubble errors to main module - Module.main.dispatchEvent(new ErrorEvent('error', { error })) - } - }) - } - - /** - * `true` if the module is the main module. - * @type {boolean} - */ - get isMain () { - return this.id === MAIN_SOURCE_ORIGIN - } - - /** - * `true` if the module was loaded by name, not file path. - * @type {boolean} - */ - get isNamed () { - return !this.isMain && !this.sourcePath?.startsWith('.') - } - - /** - * @type {URL} - */ - get url () { - return new URL(this.id) - } - - /** - * @type {string} - */ - get pathname () { - return new URL(this.id).pathname - } - - /** - * @type {string} - */ - get path () { - return path.dirname(this.pathname) - } - - /** - * Loads the module, synchronously returning `true` upon success, - * otherwise `false`. - * @return {boolean} - */ - load () { - const { isNamed, sourcePath } = this - const prefixes = (process.env.SOCKET_MODULE_PATH_PREFIX || '').split(':') - const urls = [] - - Module.previous = Module.current - Module.current = this - - // if module is named, and `prefixes` contains entries then - // build possible search paths of `<prefix>/<name>` starting - // from `.` up until `/` of the origin - if (isNamed) { - const name = sourcePath - for (const prefix of prefixes) { - let current = new URL('.', this.id).toString() - do { - const prefixURL = new URL(prefix, current) - urls.push(new URL(name, prefixURL + '/').toString()) - current = new URL('..', current).toString() - } while (new URL(current).pathname !== '/') - } - } else { - urls.push(this.id) - } - - for (const url of urls) { - if (loadPackage(this, url)) { - break - } - } - - if (this.loaded) { - Module.previous = this - Module.current = null - } - - return this.loaded - - function loadPackage (module, url) { - const hasTrailingSlash = url.endsWith('/') - const urls = [] - const extname = path.extname(url) - - if (extname && !hasTrailingSlash) { - urls.push(url) - } else { - if (hasTrailingSlash) { - urls.push( - new URL('./index.js', url).toString(), - new URL('./index.json', url).toString() - ) - } else { - urls.push( - url + '.js', - url + '.json', - new URL('./index.js', url + '/').toString(), - new URL('./index.json', url + '/').toString() - ) - } - } - - while (urls.length) { - const filename = urls.shift() - const response = request(filename) - - if (response.data) { - try { - evaluate(module, filename, response.data) - } catch (error) { - error.module = module - module.dispatchEvent(new ErrorEvent('error', { error })) - return false - } - } - - if (module.loaded) { - return true - } - } - - const response = request(path.join(url, 'package.json')) - if (response.data) { - try { - // @ts-ignore - const packageJSON = JSON.parse(response.data) - const filename = !packageJSON.exports - ? path.resolve('/', url, packageJSON.browser || packageJSON.main) - : ( - packageJSON.exports?.['.'] || - packageJSON.exports?.['./index.js'] || - packageJSON.exports?.['index.js'] - ) - - evaluate(module, filename, request(filename).data) - } catch (error) { - error.module = module - module.dispatchEvent(new ErrorEvent('error', { error })) - } - } - - return module.loaded - } - - function evaluate (module, filename, moduleSource) { - const { protocol } = path.Path.from(filename) - let dirname = path.dirname(filename) - - if (filename.startsWith(protocol) && !dirname.startsWith(protocol)) { - dirname = protocol + '//' + dirname - } - - if (path.extname(filename) === '.json') { - module.id = new URL(filename, module.parent.id).toString() - module.filename = filename - - try { - module.exports = JSON.parse(moduleSource) - } catch (error) { - error.module = module - module.dispatchEvent(new ErrorEvent('error', { error })) - return false - } finally { - module.loaded = true - Object.freeze(module) - Object.seal(module) - } - - if (module.parent) { - module.parent.children.push(module) - } - - module.dispatchEvent(new Event('load')) - return true - } - - try { - const source = Module.wrap(moduleSource) - // eslint-disable-next-line no-new-func - const define = new Function(`return ${source}`)() - - module.id = new URL(filename, module.parent.id).toString() - module.filename = filename - - // eslint-disable-next-line no-useless-call - const promise = define.call(null, - module.exports, - module.createRequire(), - module, - filename, - dirname, - process, - globalThis - ) - - promise.catch((error) => { - error.module = module - module.dispatchEvent(new ErrorEvent('error', { error })) - }) - - if (module.parent) { - module.parent.children.push(module) - } - - module.dispatchEvent(new Event('load')) - return true - } catch (error) { - error.module = module - module.dispatchEvent(new ErrorEvent('error', { error })) - return false - } finally { - module.loaded = true - Object.freeze(module) - Object.seal(module) - } - } - } - - /** - * Creates a require function for loaded CommonJS modules - * child to this module. - * @return {function(string): any} - */ - createRequire () { - const module = this - - Object.assign(require, { cache: Module.cache }) - Object.freeze(require) - Object.seal(require) - - return require - - function require (filename) { - const resolvers = Array.from(Module.resolvers) - const result = resolve(filename, resolvers) - - if (typeof result === 'string') { - const name = result.replace(/^(socket|node):/, '') - - if (name in builtins) { - return builtins[name] - } - - return module.require(name) - } - - if (result !== undefined) { - return result - } - - throw new ModuleNotFoundError( - `Cannot find module ${filename}`, - this.children - ) - } - - function resolve (filename, resolvers) { - return next(filename) - function next (specifier) { - if (resolvers.length === 0) return specifier - const resolver = resolvers.shift() - return resolver(specifier, module, next) - } - } - } - - /** - * Requires a module at `filename` that will be loaded as a child - * to this module. - * @param {string} filename - * @return {any} - */ - require (filename) { - // @ts-ignore - const module = Module.from(filename, this) - - if (!module.load()) { - throw new ModuleNotFoundError( - `Cannot find module ${filename}`, - this.children - ) - } - - return module.exports - } - - /** - * @ignore - */ - [Symbol.toStringTag] () { - return 'Module' - } -} +export default Module -// builtins should never be overloaded through this object, instead -// a custom resolver should be used -Object.freeze(Object.seal(builtins)) -// prevent misuse of the `Module` class -Object.freeze(Object.seal(Module)) +defineBuiltin('module', Module, false) diff --git a/api/network.js b/api/network.js index 4b5490f451..7b0cf0385d 100644 --- a/api/network.js +++ b/api/network.js @@ -1,17 +1,17 @@ /** - * @module Network + * @module network * - * Provides a higher level API over the stream-relay protocol. + * Provides a higher level API over the latica protocol. * * @see {@link https://socketsupply.co/guides/#p2p-guide} * */ -import sugar from './stream-relay/sugar.js' -import { Cache, Packet, sha256, Encryption, NAT } from './stream-relay/index.js' +import api from './latica/api.js' +import { Cache, Packet, sha256, Encryption, NAT } from './latica/index.js' import events from './events.js' import dgram from './dgram.js' -const network = sugar(dgram, events) +const network = options => api(options, events, dgram) export { network, Cache, sha256, Encryption, Packet, NAT } export default network diff --git a/api/node/index.js b/api/node/index.js index 85f75e4181..60e6fc96fc 100644 --- a/api/node/index.js +++ b/api/node/index.js @@ -1,9 +1,9 @@ -import sugar from '../stream-relay/sugar.js' -import { Cache, Packet, sha256, Encryption, NAT } from '../stream-relay/index.js' -import events from 'events' -import dgram from 'dgram' +import api from '../latica/api.js' +import { Cache, Packet, sha256, Encryption, NAT } from '../latica/index.js' +import events from 'node:events' +import dgram from 'node:dgram' -const network = sugar(dgram, events) +const network = options => api(options, events, dgram) export { network, Cache, sha256, Encryption, Packet, NAT } export default network diff --git a/api/notification.js b/api/notification.js index 4ab02a17dc..e0ea968d66 100644 --- a/api/notification.js +++ b/api/notification.js @@ -1,6 +1,6 @@ /* global CustomEvent, Event, ErrorEvent, EventTarget */ /** - * @module Notification + * @module notification * The Notification modules provides an API to configure and display * desktop and mobile notifications to the user. It also includes mechanisms * for request permissions to use notifications on the user's device. @@ -21,12 +21,6 @@ const NativeNotification = ( class NativeNotification extends EventTarget {} ) -/** - * Used to determine if notification beign created in a `ServiceWorker`. - * @ignore - */ -const isServiceWorkerGlobalScope = typeof globalThis.registration?.active === 'string' - /** * Default number of max actions a notification can perform. * @ignore @@ -263,7 +257,7 @@ export class NotificationOptions { * @param {boolean=} [options.silent = false] * @param {number[]=} [options.vibrate = []] */ - constructor (options = {}) { + constructor (options = {}, allowServiceWorkerGlobalScope = false) { if ('dir' in options) { // @ts-ignore if (!(options.dir in NotificationDirection)) { @@ -305,19 +299,21 @@ export class NotificationOptions { } } - if (this.#actions.length && !isServiceWorkerGlobalScope) { - throw new TypeError( - 'Failed to construct \'Notification\': Actions are only supported ' + - 'for persistent notifications shown using ' + - 'ServiceWorkerRegistration.showNotification().' - ) + if (allowServiceWorkerGlobalScope !== true) { + if (this.#actions.length && globalThis.isServiceWorkerScope) { + throw new TypeError( + 'Failed to construct \'Notification\': Actions are only supported ' + + 'for persistent notifications shown using ' + + 'ServiceWorkerRegistration.showNotification().' + ) + } } - if ('badge' in options && options.badge !== undefined) { + if ('badge' in options && options.badge) { this.#badge = String(new URL(String(options.badge), location.href)) } - if ('body' in options && options.body !== undefined) { + if ('body' in options && options.body) { this.#body = String(options.body) } @@ -325,11 +321,11 @@ export class NotificationOptions { this.#data = options.data } - if ('icon' in options && options.icon !== undefined) { + if ('icon' in options && options.icon) { this.#icon = String(new URL(String(options.icon), location.href)) } - if ('image' in options && options.image !== undefined) { + if ('image' in options && options.image) { this.#image = String(new URL(String(options.image), location.href)) } @@ -421,7 +417,7 @@ export class NotificationOptions { get dir () { return this.#dir } /** - * A string containing the URL of an icon to be displayed in the notification. + A string containing the URL of an icon to be displayed in the notification. * @type {string} */ get icon () { return this.#icon } @@ -479,6 +475,28 @@ export class NotificationOptions { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#vibration_patterns} */ get vibrate () { return this.#vibrate } + + /** + * @ignore + * @return {object} + */ + toJSON () { + return { + actions: this.#actions, + badge: this.#badge, + body: this.#body, + data: this.#data, + dir: this.#dir, + icon: this.#icon, + image: this.#image, + lang: this.#lang, + renotify: this.#renotify, + requireInteraction: this.#requireInteraction, + silent: this.#silent, + tag: this.#tag, + vibrate: this.#vibrate + } + } } /** @@ -558,9 +576,8 @@ export class Notification extends EventTarget { // any result state changes further updates const controller = new AbortController() // query for 'granted' status and return early - const query = await permissions.query({ - signal: controller.signal, - name: 'notifications' + const query = await permissions.query({ name: 'notifications' }, { + signal: controller.signal }) // if already granted, return early @@ -573,9 +590,8 @@ export class Notification extends EventTarget { // request permission and resolve the normalized `state.permission` value // when the query status changes - const request = await permissions.request({ + const request = await permissions.request({ name: 'notifications' }, { signal: controller.signal, - name: 'notifications', // macOS/iOS only options alert: Boolean(options?.alert !== false), // (defaults to `true`) @@ -611,6 +627,7 @@ export class Notification extends EventTarget { #title = null #id = null + #closed = false #proxy = null /** @@ -618,7 +635,7 @@ export class Notification extends EventTarget { * @param {string} title * @param {NotificationOptions=} [options] */ - constructor (title, options = {}) { + constructor (title, options = {}, existingState = null) { super() if (arguments.length === 0) { @@ -642,7 +659,7 @@ export class Notification extends EventTarget { } try { - this.#options = new NotificationOptions(options) + this.#options = new NotificationOptions(options, existingState !== null) } catch (err) { throw new TypeError( `Failed to construct 'Notification': ${err.message}` @@ -650,67 +667,120 @@ export class Notification extends EventTarget { } // @ts-ignore - this.#id = (rand64() & 0xFFFFn).toString() + this.#id = existingState?.id ?? (rand64() & 0xFFFFn).toString() + this.#timestamp = existingState?.timestamp ?? this.#timestamp - if (isLinux) { - const proxy = new NativeNotificationProxy(this) - const request = new Promise((resolve) => { - // @ts-ignore - proxy.addEventListener('show', () => resolve({})) - // @ts-ignore - proxy.addEventListener('error', (e) => resolve({ err: e.error })) - }) + const channel = new BroadcastChannel('socket.runtime.notification') - this.#proxy = proxy - this[Symbol.for('Notification.request')] = request - } else { - const request = ipc.request('notification.show', { - body: this.body, - icon: this.icon, - id: this.#id, - image: this.image, - lang: this.lang, - tag: this.tag || '', - title: this.title, - silent: this.silent + // if internal `existingState` is present, then this is just a view over the instance + if (existingState) { + channel.addEventListener('message', async (event) => { + if (event.data?.id === this.#id) { + if (!this.#closed) { + if (event.data.action === 'close') { + this.#closed = true + } + + this.dispatchEvent(new Event(event.data.action)) + } + } }) + } else { + if (globalThis.isServiceWorkerScope) { + throw new TypeError( + 'Failed to construct \'Notification\': Illegal constructor. ' + + 'Use ServiceWorkerRegistration.showNotification() instead.' + ) + } - this[Symbol.for('Notification.request')] = request - } + if (isLinux) { + const proxy = new NativeNotificationProxy(this) + const request = new Promise((resolve) => { + // @ts-ignore + proxy.addEventListener('show', () => resolve({})) + // @ts-ignore + proxy.addEventListener('error', (e) => resolve({ err: e.error })) + }) - state.pending.set(this.id, this) + this.#proxy = proxy + this[Symbol.for('socket.runtime.Notification.request')] = request + } else { + const request = ipc.request('notification.show', { + body: this.body, + icon: this.icon, + id: this.#id, + image: this.image, + lang: this.lang, + tag: this.tag || '', + title: this.title, + silent: this.silent + }) - const removeNotificationPresentedListener = hooks.onNotificationPresented((event) => { - if (event.detail.id === this.id) { - removeNotificationPresentedListener() - return this.dispatchEvent(new Event('show')) + this[Symbol.for('socket.runtime.Notification.request')] = request } - }) - const removeNotificationResponseListener = hooks.onNotificationResponse((event) => { - if (event.detail.id === this.id) { - const eventName = event.detail.action === 'dismiss' ? 'close' : 'click' - removeNotificationResponseListener() - this.dispatchEvent(new Event(eventName)) - if (eventName === 'click') { - queueMicrotask(() => this.dispatchEvent(new Event('close'))) + state.pending.set(this.id, this) + + const removeNotificationPresentedListener = hooks.onNotificationPresented((event) => { + if (event.detail.id === this.id) { + removeNotificationPresentedListener() + return this.dispatchEvent(new Event('show')) } - } - }) + }) - // propagate error to caller - this[Symbol.for('Notification.request')].then((result) => { - if (result?.err) { - // @ts-ignore - state.pending.delete(this.id, this) - removeNotificationPresentedListener() - removeNotificationResponseListener() - return this.dispatchEvent(new ErrorEvent('error', { - message: result.err.message, - error: result.err - })) - } - }) + const removeNotificationResponseListener = hooks.onNotificationResponse((event) => { + if (event.detail.id === this.id) { + this.#closed = true + const eventName = event.detail.action === 'dismiss' ? 'close' : 'click' + removeNotificationResponseListener() + this.dispatchEvent(new Event(eventName)) + + if (eventName === 'click') { + queueMicrotask(() => { + this.dispatchEvent(new Event('close')) + channel.postMessage({ id: this.id, action: 'close' }) + }) + } + + channel.postMessage({ id: this.id, action: eventName }) + } + }) + + // propagate error to caller + this[Symbol.for('socket.runtime.Notification.request')].then((result) => { + if (result?.err) { + // @ts-ignore + state.pending.delete(this.id, this) + removeNotificationPresentedListener() + removeNotificationResponseListener() + return this.dispatchEvent(new ErrorEvent('error', { + message: result.err.message, + error: result.err + })) + } + }) + + channel.addEventListener('message', async (event) => { + if (event.data?.id === this.#id) { + if (!this.#closed) { + if (event.data.action === 'close') { + removeNotificationPresentedListener() + removeNotificationResponseListener() + await this.close() + } + + this.dispatchEvent(new Event(event.data.action)) + } + } + }) + } + } + + /** + * @ignore + */ + get options () { + return this.#options } /** @@ -721,6 +791,14 @@ export class Notification extends EventTarget { return this.#id } + /** + * `true` if the notification was closed, otherwise `false`. + * @type {boolea} + */ + get closed () { + return this.#closed + } + /** * The click event is dispatched when the user clicks on * displayed notification. @@ -922,6 +1000,20 @@ export class Notification extends EventTarget { * Closes the notification programmatically. */ async close () { + if (this.#closed) { + return + } + + this.#closed = true + + if (globalThis.isServiceWorkerScope) { + return globalThis.postMessage({ + notificationclose: { + id: this.id + } + }) + } + if (isLinux) { if (this.#proxy) { return this.#proxy.close() diff --git a/api/npm/module.js b/api/npm/module.js new file mode 100644 index 0000000000..021983225e --- /dev/null +++ b/api/npm/module.js @@ -0,0 +1,91 @@ +import { DEFAULT_PACKAGE_PREFIX, Package } from '../commonjs/package.js' +import { Loader } from '../commonjs/loader.js' +import { isESMSource } from '../util.js' +import location from '../location.js' +import path from '../path.js' + +/** + * @typedef {{ + * package: Package + * origin: string, + * type: 'commonjs' | 'module', + * url: string + * }} ModuleResolution + */ + +/** + * Resolves an NPM module for a given `specifier` and an optional `origin`. + * @param {string|URL} specifier + * @param {string|URL=} [origin] + * @param {{ prefix?: string, type?: 'commonjs' | 'module' }} [options] + * @return {ModuleResolution|null} + */ +export async function resolve (specifier, origin = null, options = null) { + if (origin && typeof origin === 'object' && !(origin instanceof URL)) { + options = origin + origin = options.origin ?? null + } + + if (!origin) { + origin = location.origin + } + + if (!origin.endsWith('/')) { + origin += '/' + } + + // just use `specifier` to derive name and origin if it was a URL + if (specifier instanceof URL) { + origin = specifier.origin + specifier = specifier.pathname.slice(1) + specifier.search + } + + const prefix = options?.prefix ?? DEFAULT_PACKAGE_PREFIX + const name = Package.Name.from(specifier, { origin }) + const type = options?.type ?? 'module' // prefer 'module' in this context + const pkg = new Package(name.value, { + loader: { origin } + }) + + const pathname = name.pathname.replace(name.value, '.') || '.' + + try { + pkg.load({ type }) + + const url = pkg.type === type + ? pkg.resolve(pathname, { prefix, type }) + : pkg.resolve(pathname, { prefix }) + + const src = pkg.loader.load(url).text + + return { + package: pkg, + origin: pkg.origin, + type: isESMSource(src) ? 'module' : 'commonjs', + url + } + } catch (err) { + if (err?.code === 'MODULE_NOT_FOUND') { + const url = new URL(pathname, new URL(prefix + name.value + '/', origin)) + const loader = new Loader({ extensions: [path.extname(url.href)] }) + const status = loader.status(url) + // check for regular file + if (status.ok) { + return { + package: pkg, + origin: origin + prefix, + type: 'file', + url: url.href + } + } + + return null + } + + throw err + } +} + +export default { + resolve +} diff --git a/api/npm/service-worker.js b/api/npm/service-worker.js new file mode 100644 index 0000000000..5873bdfeb8 --- /dev/null +++ b/api/npm/service-worker.js @@ -0,0 +1,206 @@ +import { resolve } from './module.js' +import process from '../process.js' +import debug from '../service-worker/debug.js' +import mime from '../mime.js' +import path from '../path.js' +import http from '../http.js' +import util from '../util.js' + +const DEBUG_LABEL = ' <span\\sstyle="color:\\s#fb8817;"><b>npm</b></span>' + +/** + * @ignore + * @param {Request} + * @param {object} env + * @param {import('../service-worker/context.js').Context} ctx + * @return {Promise<Response|null>} + */ +export async function onRequest (request, env, ctx) { + // eslint-disable-next-line + void env, ctx; + if (process.env.SOCKET_RUNTIME_NPM_DEBUG) { + console.debug(request.url) + } + + const url = new URL(request.url) + const origin = url.origin.replace('npm://', 'socket://') + const referer = request.headers.get('referer') + let specifier = url.pathname.replace('/socket/npm/', '') + const importOrigins = url.searchParams.getAll('origin').concat(url.searchParams.getAll('origin[]')) + + if (typeof specifier === 'string') { + try { + specifier = (new URL(require.resolve(specifier, { type: 'module' }))).toString() + } catch {} + } + + debug(`${DEBUG_LABEL}: fetch: %s`, specifier) + + let resolved = null + let origins = [] + + if (referer && !referer.startsWith('blob:')) { + // @ts-ignore + if (URL.canParse(referer, origin)) { + const refererURL = new URL(referer, origin) + if (refererURL.href.endsWith('/')) { + importOrigins.push(refererURL.href) + } else { + importOrigins.push(new URL('./', refererURL).href) + } + } + } + + for (const value of importOrigins) { + if (value.startsWith('npm:')) { + origins.push(value) + } else if (value.startsWith('.')) { + origins.push(new URL(value, `socket://${url.host}${url.pathname}`).href.replace(/\/$/, '')) + } else if (value.startsWith('/')) { + origins.push(new URL(value, origin).href) + // @ts-ignore + } else if (URL.canParse(value)) { + origins.push(value) + // @ts-ignore + } else if (URL.canParse(`socket://${value}`)) { + origins.push(`socket://${value}`) + } + } + + origins.push(origin) + origins = Array.from(new Set(origins)) + + if (process.env.SOCKET_RUNTIME_NPM_DEBUG) { + console.debug('resolving: npm:%s (%o)', specifier, origins) + } + + while (origins.length && resolved === null) { + const potentialOrigins = [] + const origin = origins.shift() + + if (origin.startsWith('npm:')) { + const potentialSpecifier = new URL(origin).pathname + for (const potentialOrigin of origins) { + const resolution = await resolve(potentialSpecifier, potentialOrigin) + if (resolution) { + potentialOrigins.push(resolution.url) + break + } + } + } else { + potentialOrigins.push(origin) + } + + while (potentialOrigins.length && resolved === null) { + const importOrigin = new URL('./', potentialOrigins.shift()).href + resolved = await resolve(specifier, importOrigin) + } + } + + // not found + if (!resolved) { + debug(`${DEBUG_LABEL}: not found: %s`, specifier) + return + } + + const extname = path.extname(resolved.url) + const types = extname ? await mime.lookup(extname.slice(1)) : [] + + if (types.length || resolved.type === 'file') { + let redirect = true + for (const type of types) { + if (type.mime === 'text/javascript' || type.mime.endsWith('json')) { + redirect = false + break + } + } + + if (redirect) { + debug(`${DEBUG_LABEL}: resolve: %s (file): %s`, specifier, resolved.url) + return Response.redirect(resolved.url, 301) + } + } + + debug(`${DEBUG_LABEL}: resolve: %s (%s): %s`, specifier, resolved.type, resolved.url) + + if (resolved.type === 'module') { + const response = await fetch(resolved.url) + const text = await response.text() + const proxy = /^\s*(export\s*default)/gm.test(text) + ? ` + import module from '${resolved.url}' + export * from '${resolved.url}' + export default module + ` + : ` + import * as module from '${resolved.url}' + export * from '${resolved.url}' + export default module + ` + + const source = proxy + .trim() + .split('\n') + .map((line) => line.trim()) + .join('\n') + return new Response(source, { + headers: { + 'content-type': 'text/javascript' + } + }) + } + + if (resolved.type === 'commonjs') { + const proxy = ` + import { createRequire } from 'socket:module' + const headers = { 'Runtime-ServiceWorker-Fetch-Mode': 'ignore' } + const require = createRequire('${resolved.origin}', { headers }) + const exports = require('${resolved.url}') + export default exports?.default ?? exports ?? null + `.trim() + + const source = proxy + .trim() + .split('\n') + .map((line) => line.trim()) + .join('\n') + return new Response(source, { + headers: { + 'content-type': 'text/javascript' + } + }) + } +} + +/** + * Handles incoming 'npm://<module_name>/<pathspec...>' requests. + * @param {Request} request + * @param {object} env + * @param {import('../service-worker/context.js').Context} ctx + * @return {Response?} + */ +export default async function (request, env, ctx) { + if (request.method === 'OPTIONS') { + return new Response('OK', { status: 204 }) + } + + if (request.method !== 'GET') { + return new Response('Invalid HTTP method', { + status: http.BAD_REQUEST + }) + } + + try { + return await onRequest(request, env, ctx) + } catch (err) { + globalThis.reportError(err) + + if (process.env.SOCKET_RUNTIME_NPM_DEBUG) { + console.debug(err) + } + + return new Response(util.inspect(err), { + status: 500 + }) + } +} diff --git a/api/os.js b/api/os.js index cff6db835c..c1afc508d4 100644 --- a/api/os.js +++ b/api/os.js @@ -1,5 +1,5 @@ /** - * @module OS + * @module os * * This module provides normalized system information from all the major * operating systems. @@ -10,8 +10,12 @@ * ``` */ -import { toProperCase } from './util.js' import ipc, { primordials } from './ipc.js' +import { toProperCase } from './util.js' +import constants from './os/constants.js' +import { HOME } from './path/well-known.js' + +export { constants } const UNKNOWN = 'unknown' @@ -378,6 +382,14 @@ export function host () { return primordials['host-operating-system'] || 'unknown' } +/** + * Returns the home directory of the current user. + * @return {string} + */ +export function homedir () { + return globalThis.__args.env.HOME ?? HOME ?? '' +} + // eslint-disable-next-line import * as exports from './os.js' export default exports diff --git a/api/os/constants.js b/api/os/constants.js new file mode 100644 index 0000000000..335dec3606 --- /dev/null +++ b/api/os/constants.js @@ -0,0 +1,842 @@ +import { sendSync } from '../ipc.js' + +const constants = sendSync('os.constants', {}, { cache: true })?.data || {} + +/** + * @typedef {number} errno + * @typedef {number} signal + */ + +/** + * A container for all known "errno" constant values. + * Unsupported values have a default value of `0`. + */ +export const errno = Object.assign(Object.create(null), { + /** + * "Arg list too long" + * The number of bytes used for the argument and environment list of the + * new process exceeded the limit NCARGS (specified in ⟨sys/param.h⟩). + * @type {errno} + */ + E2BIG: constants.E2BIG ?? 0, + + /** + * "Permission denied" + * An attempt was made to access a file in a way forbidden by + * its file access permissions. + * @type {errno} + */ + EACCES: constants.EACCES ?? 0, + + /** + * "Address already in use" + * Only one usage of each address is normally permitted. + * @type {errno} + */ + EADDRINUSE: constants.EADDRINUSE ?? 0, + + /** + * "Cannot assign requested address" + * Normally results from an attempt to create a socket with an + * address not on this machine. + * @type {errno} + */ + EADDRNOTAVAIL: constants.EADDRNOTAVAIL ?? 0, + + /** + * "Address family not supported by protocol family" + * An address incompatible with the requested protocol was used. + * For example, you shouldn't necessarily expect to be able to use + * NS addresses with ARPA Internet protocols. + * @type {errno} + */ + EAFNOSUPPORT: constants.EAFNOSUPPORT ?? 0, + + /** + * "Resource temporarily unavailable" + * This is a temporary condition and later calls to the + * same routine may complete normally. + * @type {errno} + */ + EAGAIN: constants.EAGAIN ?? 0, + + /** + * "Operation already in progress" + * An operation was attempted on a non-blocking object that + * already had an operation in progress. + * @type {errno} + */ + EALREADY: constants.EALREADY ?? 0, + + /** + * "Bad file descriptor" + * A file descriptor argument was out of range, referred to no open file, + * or a read (write) request was made to a file that was only open + * for writing (reading). + * @type {errno} + */ + EBADF: constants.EBADF ?? 0, + + /** + * "Bad message" + * The message to be received is inapprorpiate for the operation + * being attempted. + * @type {errno} + */ + EBADMSG: constants.EBADMSG ?? 0, + + /** + * "Resource busy" + * An attempt to use a system resource which was in use at the time + * in a manner which would have conflicted with the request. + * @type {errno} + */ + EBUSY: constants.EBUSY ?? 0, + + /** + * "Operation canceled" + * The scheduled operation was canceled. + * @type {errno} + */ + ECANCELED: constants.ECANCELED ?? 0, + + /** + * "No child processes" + * A wait or waitpid function was executed by a process that had no existing + * or unwaited-for child processes. + * @type {errno} + */ + ECHILD: constants.ECHILD ?? 0, + + /** + * "Software caused connection abort" + * A connection abort was caused internal to your host machine. + * @type {errno} + */ + ECONNABORTED: constants.ECONNABORTED ?? 0, + + /** + * "Connection refused" + * No connection could be made because the target machine actively refused it. + * This usually results from trying to connect to a service that is inactive + * on the foreign host. + * @type {errno} + */ + ECONNREFUSED: constants.ECONNREFUSED ?? 0, + + /** + * "Connection reset by peer" + * A connection was forcibly closed by a peer. + * This normally results from a loss of the connection on the remote socket + * due to a timeout or a reboot. + * @type {errno} + */ + ECONNRESET: constants.ECONNRESET ?? 0, + + /** + * "Resource deadlock avoided" + * An attempt was made to lock a system resource that would have resulted in + * a deadlock situation. + * @type {errno} + */ + EDEADLK: constants.EDEADLK ?? 0, + + /** + * "Destination address required" + * A required address was omitted from an operation on a socket. + * @type {errno} + */ + EDESTADDRREQ: constants.EDESTADDRREQ ?? 0, + + /** + * "Numerical argument out of domain" + * A numerical input argument was outside the defined domain of the + * mathematical function. + * @type {errno} + */ + EDOM: constants.EDOM ?? 0, + + /** + * "Disc quota exceeded" + * A write to an ordinary file, the creation of a directory or symbolic link, + * or the creation of a directory entry failed because the user's quota of + * disk blocks was exhausted, or the allocation of an inode for a newly + * created file failed because the user's quota of inodes was exhausted. + * @type {errno} + */ + EDQUOT: constants.EDQUOT ?? 0, + + /** + * "File exists" + * An existing file was mentioned in an inappropriate context, for instance, + * as the new link name in a link function. + * @type {errno} + */ + EEXIST: constants.EEXIST ?? 0, + + /** + * "Bad address" + * The system detected an invalid address in attempting to use an + * argument of a call. + * @type {errno} + */ + EFAULT: constants.EFAULT ?? 0, + + /** + * "File too large" + * The size of a file exceeded the maximum. + * @type {errno} + */ + EFBIG: constants.EFBIG ?? 0, + + /** + * "No route to host" + * A socket operation was attempted to an unreachable host. + * @type {errno} + */ + EHOSTUNREACH: constants.EHOSTUNREACH ?? 0, + + /** + * "Identifier removed" + * An IPC identifier was removed while the current process was waiting on it. + * @type {errno} + */ + EIDRM: constants.EIDRM ?? 0, + + /** + * "Illegal byte sequence" + * While decoding a multibyte character the function came along an invalid + * or an incomplete sequence of bytes or the given wide character is invalid. + * @type {errno} + */ + EILSEQ: constants.EILSEQ ?? 0, + + /** + * "Operation now in progress" + * An operation that takes a long time to complete. + * @type {errno} + */ + EINPROGRESS: constants.EINPROGRESS ?? 0, + + /** + * "Interrupted function call" + * An asynchronous signal (such as SIGINT or SIGQUIT) was caught by the + * process during the execution of an interruptible function. If the signal + * handler performs a normal return, the interrupted function call will seem + * to have returned the error condition. + * @type {errno} + */ + EINTR: constants.EINTR ?? 0, + + /** + * "Invalid argument" + * Some invalid argument was supplied. + * (For example, specifying an undefined signal to a signal or kill function). + * @type {errno} + */ + EINVAL: constants.EINVAL ?? 0, + + /** + * "Input/output error" + * Some physical input or output error occurred. + * This error will not be reported until a subsequent operation on the same + * file descriptor and may be lost (over written) by any subsequent errors. + * @type {errno} + */ + EIO: constants.EIO ?? 0, + + /** + * "Socket is already connected" + * A connect or connectx request was made on an already connected socket; + * or, a sendto or sendmsg request on a connected socket specified a + * destination when already connected. + * @type {errno} + */ + EISCONN: constants.EISCONN ?? 0, + + /** + * "Is a directory" + * An attempt was made to open a directory with write mode specified. + * @type {errno} + */ + EISDIR: constants.EISDIR ?? 0, + + /** + * "Too many levels of symbolic links" + * A path name lookup involved more than 8 symbolic links. + * @type {errno} + */ + ELOOP: constants.ELOOP ?? 0, + + /** + * "Too many open files" + * @type {errno} + */ + EMFILE: constants.EMFILE ?? 0, + + /** + * "Too many links" + * Maximum allowable hard links to a single file + * has been exceeded (limit of 32767 hard links per file). + * @type {errno} + */ + EMLINK: constants.EMLINK ?? 0, + + /** + * "Message too long" + * A message sent on a socket was larger than the internal message + * buffer or some other network limit. + * @type {errno} + */ + EMSGSIZE: constants.EMSGSIZE ?? 0, + + /** + * "Reserved" + * This error is reserved for future use. + * @type {errno} + */ + EMULTIHOP: constants.EMULTIHOP ?? 0, + + /** + * "File name too long" + * A component of a path name exceeded 255 (MAXNAMELEN) characters, + * or an entire path name exceeded 1023 (MAXPATHLEN-1) characters. + * @type {errno} + */ + ENAMETOOLONG: constants.ENAMETOOLONG ?? 0, + + /** + * "Network is down" + * A socket operation encountered a dead network. + * @type {errno} + */ + ENETDOWN: constants.ENETDOWN ?? 0, + + /** + * "Network dropped connection on reset" + * The host you were connected to crashed and rebooted. + * @type {errno} + */ + ENETRESET: constants.ENETRESET ?? 0, + + /** + * "Network is unreachable" + * A socket operation was attempted to an unreachable network. + * @type {errno} + */ + ENETUNREACH: constants.ENETUNREACH ?? 0, + + /** + * "Too many open files in system" + * Maximum number of file descriptors allowable on the system has been reached + * and a requests for an open cannot be satisfied until at least one has + * been closed. + * @type {errno} + */ + ENFILE: constants.ENFILE ?? 0, + + /** + * "No buffer space available" + * An operation on a socket or pipe was not performed because the system + * lacked sufficient buffer space or because a queue was full. + * @type {errno} + */ + ENOBUFS: constants.ENOBUFS ?? 0, + + /** + * "No message available" + * No message was available to be received by the requested operation. + * @type {errno} + */ + ENODATA: constants.ENODATA ?? 0, + + /** + * "Operation not supported by device" + * An attempt was made to apply an inappropriate function to a device, + * for example, trying to read a write-only device such as a printer. + * @type {errno} + */ + ENODEV: constants.ENODEV ?? 0, + + /** + * "No such file or directory" + * A component of a specified pathname did not exist, + * or the pathname was an empty string. + * @type {errno} + */ + ENOENT: constants.ENOENT ?? 0, + + /** + * "Exec format error" + * A request was made to execute a file that, + * although it has the appropriate permissions, + * was not in the format required for an executable file. + * @type {errno} + */ + ENOEXEC: constants.ENOEXEC ?? 0, + + /** + * "No locks available" + * A system-imposed limit on the number of simultaneous + * file locks was reached. + * @type {errno} + */ + ENOLCK: constants.ENOLCK ?? 0, + + /** + * "Reserved" + * This error is reserved for future use. + * @type {errno} + */ + ENOLINK: constants.ENOLINK ?? 0, + + /** + * "Cannot allocate memory" + * The new process image required more memory than was allowed by the hardware + * or by system-imposed memory management constraints.A lack of swap space is + * normally temporary; however, a lack of core is not. + * Soft limits may be increased to their corresponding hard limits. + * @type {errno} + */ + ENOMEM: constants.ENOMEM ?? 0, + + /** + * "No message of desired type" + * An IPC message queue does not contain a message of the desired type, + * or a message catalog does not contain the requested message. + * @type {errno} + */ + ENOMSG: constants.ENOMSG ?? 0, + + /** + * "Protocol not available" + * A bad option or level was specified in a `getsockopt(2)` or + * `setsockopt(2)` call. + * @type {errno} + */ + ENOPROTOOPT: constants.ENOPROTOOPT ?? 0, + + /** + * "Device out of space" + * A write to an ordinary file, the creation of a directory or symbolic link, + * or the creation of a directory entry failed because no more disk blocks + * were available on the file system, or the allocation of an inode for a + * newly created file failed because no more inodes were available on + * the file system. + * @type {errno} + */ + ENOSPC: constants.ENOSPC ?? 0, + + /** + * "No STREAM resources" + * This error is reserved for future use. + * @type {errno} + */ + ENOSR: constants.ENOSR ?? 0, + + /** + * "Not a STREAM" + * This error is reserved for future use. + * @type {errno} + */ + ENOSTR: constants.ENOSTR ?? 0, + + /** + * "Function not implemented" + * Attempted a system call that is not available on this system. + * @type {errno} + */ + ENOSYS: constants.ENOSYS ?? 0, + + /** + * "Socket is not connected" + * An request to send or receive data was disallowed because the socket was + * not connected and (when sending on a datagram socket) no address was + * supplied. + * @type {errno} + */ + ENOTCONN: constants.ENOTCONN ?? 0, + + /** + * "Not a directory" + * A component of the specified pathname existed, but it was not a directory, + * when a directory was expected. + * @type {errno} + */ + ENOTDIR: constants.ENOTDIR ?? 0, + + /** + * "Directory not empty" + * A directory with entries other than ‘.’ and ‘..’ was supplied to a remove + * directory or rename call. + * @type {errno} + */ + ENOTEMPTY: constants.ENOTEMPTY ?? 0, + + /** + * "Socket operation on non-socket" + * Self-explanatory. + * @type {errno} + */ + ENOTSOCK: constants.ENOTSOCK ?? 0, + + /** + * "Not supported" + * The attempted operation is not supported for the type of object referenced. + * @type {errno} + */ + ENOTSUP: constants.ENOTSUP ?? 0, + + /** + * "Inappropriate ioctl for device" + * A control function (see `ioctl(2)`) was attempted for a file or special + * device for which the operation was inappropriate. + * @type {errno} + */ + ENOTTY: constants.ENOTTY ?? 0, + + /** + * "No such device or address" + * Input or output on a special file referred to a device that did not exist, + * or made a request beyond the limits of the device. + * This error may also occur when, for example, a tape drive is not online or + * no disk pack is loaded on a drive. + * @type {errno} + */ + ENXIO: constants.ENXIO ?? 0, + + /** + * "Operation not supported on socket" + * The attempted operation is not supported for the type of socket referenced; + * for example, trying to accept a connection on a datagram socket. + * @type {errno} + */ + EOPNOTSUPP: constants.EOPNOTSUPP ?? 0, + + /** + * "Value too large to be stored in data type" + * A numerical result of the function was too large to be stored + * in the caller provided space. + * @type {errno} + */ + EOVERFLOW: constants.EOVERFLOW ?? 0, + + /** + * "Operation not permitted" + * An attempt was made to perform an operation limited to processes with + * appropriate privileges or to the owner of a file or other resources. + * @type {errno} + */ + EPERM: constants.EPERM ?? 0, + + /** + * "Broken pipe" + * A write on a pipe, socket or FIFO for which there is no process to + * read the data. + * @type {errno} + */ + EPIPE: constants.EPIPE ?? 0, + + /** + * "Protocol error" + * Some protocol error occurred. + * This error is device-specific, but is generally not related to a + * hardware failure. + * @type {errno} + */ + EPROTO: constants.EPROTO ?? 0, + + /** + * "Protocol not supported" + * The protocol has not been configured into the system or + * no implementation for it exists. + * @type {errno} + */ + EPROTONOSUPPORT: constants.EPROTONOSUPPORT ?? 0, + + /** + * "Protocol wrong type for socket" + * A protocol was specified that does not support the semantics of the socket + * type requested. For example, you cannot use the ARPA Internet UDP protocol + * with type SOCK_STREAM. + * @type {errno} + */ + EPROTOTYPE: constants.EPROTOTYPE ?? 0, + + /** + * "Numerical result out of range" + * A numerical result of the function was too large to fit in the available + * space (perhaps exceeded precision). + * @type {errno} + */ + ERANGE: constants.ERANGE ?? 0, + + /** + * "Read-only file system" + * An attempt was made to modify a file or directory was made on a file + * system that was read-only at the time. + * @type {errno} + */ + EROFS: constants.EROFS ?? 0, + + /** + * "Illegal seek" + * An lseek function was issued on a socket, pipe or FIFO. + * @type {errno} + */ + ESPIPE: constants.ESPIPE ?? 0, + + /** + * "No such process" + * No process could be found corresponding to that specified + * by the given process ID. + * @type {errno} + */ + ESRCH: constants.ESRCH ?? 0, + + /** + * "Stale NFS file handle" + * An attempt was made to access an open file (on an NFS filesystem) + * which is now unavailable as referenced by the file descriptor. + * This may indicate the file was deleted on the NFS server or some other + * catastrophic event occurred. + * @type {errno} + */ + ESTALE: constants.ESTALE ?? 0, + + /** + * "STREAM ioctl() timeout" + * This error is reserved for future use. + * @type {errno} + */ + ETIME: constants.ETIME ?? 0, + + /** + * "Operation timed out" + * A connect, connectx or send request failed because the connected party did + * not properly respond after a period of time. + * (The timeout period is dependent on the communication protocol.) + * @type {errno} + */ + ETIMEDOUT: constants.ETIMEDOUT ?? 0, + + /** + * "Text file busy" + * The new process was a pure procedure (shared text) file which was open for + * writing by another process, or while the pure procedure file was being + * executed an open call requested write access. + * @type {errno} + */ + ETXTBSY: constants.ETXTBSY ?? 0, + + /** + * "Operation would block" + * (may be same value as EAGAIN) (POSIX.1-2001). + * @type {errno} + */ + EWOULDBLOCK: constants.EWOULDBLOCK ?? constants.EAGAIN ?? 0, + + /** + * "Improper link" + * A hard link to a file on another file system was attempted. + * @type {errno} + */ + EXDEV: constants.EXDEV ?? 0 +}) + +/** + * A container for all known "signal" constant values. + * Unsupported values have a default value of `0`. + */ +export const signal = Object.assign(Object.create(null), { + /** + * Terminal line hangup. + * @type {signal} + */ + SIGHUP: constants.SIGHUP ?? 0, + + /** + * Interrupt program. + * @type {signal} + */ + SIGINT: constants.SIGINT ?? 0, + + /** + * Quit program. + * @type {signal} + */ + SIGQUIT: constants.SIGQUIT ?? 0, + + /** + * Illegal instruction. + * @type {signal} + */ + SIGILL: constants.SIGILL ?? 0, + + /** + * Trace trap. + * @type {signal} + */ + SIGTRAP: constants.SIGTRAP ?? 0, + + /** + * Abort program. + * @type {signal} + */ + SIGABRT: constants.SIGABRT ?? 0, + + /** + * An alias to `SIGABRT` + * @type {signal} + */ + SIGIOT: constants.SIGIOT ?? constants.SIGABRT ?? 0, + + /** + * Bus error. + * @type {signal} + */ + SIGBUS: constants.SIGBUS ?? 0, + + /** + * Floating-point exception. + * @type {signal} + */ + SIGFPE: constants.SIGFPE ?? 0, + + /** + * Kill program. + * @type {signal} + */ + SIGKILL: constants.SIGKILL ?? 0, + + /** + * User defined signal 1. + * @type {signal} + */ + SIGUSR1: constants.SIGUSR1 ?? 0, + + /** + * Segmentation violation. + * @type {signal} + */ + SIGSEGV: constants.SIGSEGV ?? 0, + + /** + * User defined signal 2. + * @type {signal} + */ + SIGUSR2: constants.SIGUSR2 ?? 0, + + /** + * Write on a pipe with no reader. + * @type {signal} + */ + SIGPIPE: constants.SIGPIPE ?? 0, + + /** + * Real-time timer expired. + * @type {signal} + */ + SIGALRM: constants.SIGALRM ?? 0, + + /** + * Software termination signal. + * @type {signal} + */ + SIGTERM: constants.SIGTERM ?? 0, + + /** + * Child status has changed. + * @type {signal} + */ + SIGCHLD: constants.SIGCHLD ?? 0, + + /** + * Continue after stop. + * @type {signal} + */ + SIGCONT: constants.SIGCONT ?? 0, + + /** + * Stop signal (cannot be caught or ignored). + * @type {signal} + */ + SIGSTOP: constants.SIGSTOP ?? 0, + + /** + * Stop signal generated from keyboard. + * @type {signal} + */ + SIGTSTP: constants.SIGTSTP ?? 0, + + /** + * Background read attempted from control terminal. + * @type {signal} + */ + SIGTTIN: constants.SIGTTIN ?? 0, + + /** + * Background write attempted to control terminal. + * @type {signal} + */ + SIGTTOU: constants.SIGTTOU ?? 0, + + /** + * Urgent condition present on socket. + * @type {signal} + */ + SIGURG: constants.SIGURG ?? 0, + + /** + * CPU time limit exceeded (see `setrlimit(2)`) + * @type {signal} + */ + SIGXCPU: constants.SIGXCPU ?? 0, + + /** + * File size limit exceeded (see `setrlimit(2)`). + * @type {signal} + */ + SIGXFSZ: constants.SIGXFSZ ?? 0, + + /** + * Virtual time alarm (see `setitimer(2)`). + * @type {signal} + */ + SIGVTALRM: constants.SIGVTALRM ?? 0, + + /** + * Profiling timer alarm (see `setitimer(2)`). + * @type {signal} + */ + SIGPROF: constants.SIGPROF ?? 0, + + /** + * Window size change. + * @type {signal} + */ + SIGWINCH: constants.SIGWINCH ?? 0, + + /** + * I/O is possible on a descriptor (see `fcntl(2)`). + * @type {signal} + */ + SIGIO: constants.SIGIO ?? 0, + + /** + * Status request from keyboard. + * @type {signal} + */ + SIGINFO: constants.SIGINFO ?? 0, + + /** + * Non-existent system call invoked. + * @type {signal} + */ + SIGSYS: constants.SIGSYS ?? 0 +}) + +export default { + errno, + signal +} diff --git a/api/path.js b/api/path.js index 25d923072f..eddb29e1d3 100644 --- a/api/path.js +++ b/api/path.js @@ -1,18 +1,24 @@ import { primordials } from './ipc.js' import { + mounts, Path, posix, win32, // well known - RESOURCES, DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, + CONFIG, + MEDIA, MUSIC, - HOME + HOME, + DATA, + LOG, + TMP } from './path/index.js' const isWin32 = primordials.platform === 'win32' @@ -36,15 +42,21 @@ export { Path, posix, win32, + mounts, - RESOURCES, DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, + CONFIG, + MEDIA, MUSIC, - HOME + HOME, + DATA, + LOG, + TMP } export default isWin32 ? win32 : posix diff --git a/api/path/index.js b/api/path/index.js index 89748bc054..9e8bb2ed47 100644 --- a/api/path/index.js +++ b/api/path/index.js @@ -1,32 +1,44 @@ import { Path } from './path.js' import * as posix from './posix.js' import * as win32 from './win32.js' - +import * as mounts from './mounts.js' +import * as exports from './index.js' import { - RESOURCES, DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, + CONFIG, + MEDIA, MUSIC, - HOME + HOME, + DATA, + LOG, + TMP } from './well-known.js' -export * as default from './index.js' - export { + mounts, posix, win32, Path, - // well known - RESOURCES, + // well known paths DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, + CONFIG, + MEDIA, MUSIC, - HOME + HOME, + DATA, + LOG, + TMP } + +export default exports diff --git a/api/path/mounts.js b/api/path/mounts.js new file mode 100644 index 0000000000..b1c6ea436a --- /dev/null +++ b/api/path/mounts.js @@ -0,0 +1 @@ +export default {} diff --git a/api/path/path.js b/api/path/path.js index d3568adea8..6fb98fd874 100644 --- a/api/path/path.js +++ b/api/path/path.js @@ -1,26 +1,29 @@ /** - * @module Path + * @module path * * Example usage: * ```js * import { Path } from 'socket:path' * ``` */ +import { resolve as resolveURL, URL, URLPattern } from '../url.js' import location from '../location.js' -import { - resolve as resolveURL, - parse as parseURL, - URL, - URLPattern -} from '../url.js' const windowsDriveRegex = /^[a-z]:/i const windowsDriveAndSlashesRegex = /^([a-z]:(\\|\/\/))/i const windowsDriveInPathRegex = /^\/[a-z]:/i -function maybeURL (uri, baseURL) { +function maybeURL (uri, baseURL = undefined) { let url = null + if (typeof baseURL === 'string' && baseURL.startsWith('blob:')) { + baseURL = new URL(baseURL).pathname + } + + if (typeof uri === 'string' && uri.startsWith('blob:')) { + uri = new URL(uri).pathname + } + try { baseURL = new URL(baseURL) } catch {} @@ -48,6 +51,10 @@ export function resolve (options, ...components) { component = component.replace(/\/$/g, '') } + if (component.startsWith('blob:')) { + component = maybeURL(component) + } + resolved = resolveURL(resolved + '/', component) if (resolved.length > 1) { @@ -158,30 +165,28 @@ export function join (options, ...components) { const { sep } = options const queries = [] const resolved = [] - let protocol = null + const isAbsolute = ( + URL.canParse(components[0]) || + components[0].trim().startsWith(sep) + ) - const isAbsolute = components[0].trim().startsWith(sep) + let url = null while (components.length) { - let component = String(components.shift() || '') - const url = parseURL(component) || component + const component = components.shift() - if (url.protocol) { - if (!protocol) { - protocol = url.protocol - } - - component = url.pathname - } - - const parts = component.split(sep).filter(Boolean) - while (parts.length) { - queries.push(parts.shift()) + if (!url && URL.canParse(component)) { + url = new URL(component) + queries.push(...url.pathname.split('/')) + } else { + queries.push(...String(component).split(sep)) } } for (const query of queries) { - if (query === '..' && resolved.length > 1 && resolved[0] !== '..') { + if (!query) { + continue + } else if (query === '..' && resolved.length > 1 && resolved[0] !== '..') { resolved.pop() } else if (query !== '.') { if (query.startsWith(sep)) { @@ -196,9 +201,15 @@ export function join (options, ...components) { const joined = resolved.join(sep) - return isAbsolute - ? sep + joined - : joined + if (url) { + return new URL(joined, url.origin).href + } + + if (isAbsolute) { + return sep + joined + } + + return joined } /** @@ -208,6 +219,12 @@ export function join (options, ...components) { * @return {string} */ export function dirname (options, path) { + if (typeof path !== 'string') { + throw Object.assign(new Error(`The "path" argument must be of type string. Received: ${path}`), { + code: 'ERR_INVALID_ARG_TYPE' + }) + } + if (windowsDriveInPathRegex.test(path)) { path = path.slice(1) } @@ -397,14 +414,14 @@ export class Path { if (cwd) { cwd = cwd.replace(/\\/g, '/') - cwd = new URL(`file://${cwd.replace('file://', '')}`) + cwd = maybeURL(`file://${cwd.replace('file://', '')}`) } else if (pathname.startsWith('..')) { pathname = pathname.slice(2) cwd = 'file:///..' } else if (isRelative) { - cwd = new URL('file:///.') + cwd = maybeURL('file:///.') } else { - cwd = new URL(`file://${Path.cwd()}`) + cwd = maybeURL(`file://${Path.cwd()}`) } if (cwd === 'socket:/') { @@ -420,7 +437,7 @@ export class Path { this.#hasProtocol = Boolean(this.pattern.protocol) } catch {} - this.url = new URL(pathname, cwd) + this.url = maybeURL(pathname, cwd) const [drive] = ( pathname.match(windowsDriveRegex) || @@ -435,6 +452,10 @@ export class Path { } get pathname () { + if (!this.url) { + return null + } + let { pathname } = this.url if (this.#leadingDot || this.isRelative) { @@ -449,11 +470,11 @@ export class Path { } get protocol () { - return this.url.protocol + return this.url?.protocol } get href () { - return this.url.href + return this.url?.href } /** @@ -632,7 +653,7 @@ export class Path { * @return {URL} */ toURL () { - return new URL(this.href.replace(/\\/g, '/')) + return maybeURL(this.href.replace(/\\/g, '/')) } /** diff --git a/api/path/posix.js b/api/path/posix.js index 196413b92a..19aee7dd6a 100644 --- a/api/path/posix.js +++ b/api/path/posix.js @@ -1,16 +1,22 @@ +import * as mounts from './mounts.js' import * as win32 from './win32.js' import location from '../location.js' import { Path } from './path.js' import url from '../url.js' - import { - RESOURCES, DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, - MUSIC + CONFIG, + MEDIA, + MUSIC, + HOME, + DATA, + LOG, + TMP } from './well-known.js' import * as exports from './posix.js' @@ -18,16 +24,24 @@ import * as exports from './posix.js' /** @typedef {import('./path.js').PathComponent} PathComponent */ export { + mounts, win32, Path, - RESOURCES, + // well known paths DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, - MUSIC + CONFIG, + MEDIA, + MUSIC, + HOME, + DATA, + LOG, + TMP } export default exports @@ -75,10 +89,10 @@ export function dirname (path) { /** * Computes base name of path. * @param {PathComponent} path - * @param {string} suffix + * @param {string=} [suffix] * @return {string} */ -export function basename (path, suffix) { +export function basename (path, suffix = null) { return Path.basename({ sep }, path).replace(suffix || '', '') } diff --git a/api/path/well-known.js b/api/path/well-known.js index 0998863801..e3434a7882 100644 --- a/api/path/well-known.js +++ b/api/path/well-known.js @@ -1,55 +1,85 @@ import ipc from '../ipc.js' -const paths = ipc.sendSync('os.paths') +const paths = ipc.sendSync('os.paths')?.data ?? {} /** * Well known path to the user's "Downloads" folder. * @type {?string} */ -export const DOWNLOADS = paths.data.downloads || null +export const DOWNLOADS = paths.downloads || null /** * Well known path to the user's "Documents" folder. * @type {?string} */ -export const DOCUMENTS = paths.data.documents || null +export const DOCUMENTS = paths.documents || null /** * Well known path to the user's "Pictures" folder. * @type {?string} */ -export const PICTURES = paths.data.pictures || null +export const PICTURES = paths.pictures || null /** * Well known path to the user's "Desktop" folder. * @type {?string} */ -export const DESKTOP = paths.data.desktop || null +export const DESKTOP = paths.desktop || null /** * Well known path to the user's "Videos" folder. * @type {?string} */ -export const VIDEOS = paths.data.videos || null +export const VIDEOS = paths.videos || null /** * Well known path to the user's "Music" folder. * @type {?string} */ -export const MUSIC = paths.data.music || null +export const MUSIC = paths.music || null /** * Well known path to the application's "resources" folder. * @type {?string} */ -export const RESOURCES = paths.data.resources || null +export const RESOURCES = paths.resources || null + +/** + * Well known path to the application's "config" folder. + * @type {?string} + */ +export const CONFIG = paths.config || null + +/** + * Well known path to the application's public "media" folder. + * @type {?string} + */ +export const MEDIA = paths.media || null + +/** + * Well known path to the application's "data" folder. + * @type {?string} + */ +export const DATA = paths.data || null + +/** + * Well known path to the application's "log" folder. + * @type {?string} + */ +export const LOG = paths.log || null + +/** + * Well known path to the application's "tmp" folder. + * @type {?string} + */ +export const TMP = paths.tmp || null /** * Well known path to the application's "home" folder. * This may be the user's HOME directory or the application container sandbox. * @type {?string} */ -export const HOME = paths.data.home || null +export const HOME = paths.home || null export default { DOWNLOADS, @@ -58,6 +88,11 @@ export default { PICTURES, DESKTOP, VIDEOS, + CONFIG, + MEDIA, MUSIC, - HOME + HOME, + DATA, + LOG, + TMP } diff --git a/api/path/win32.js b/api/path/win32.js index 1ceee65bc2..7c514fca8e 100644 --- a/api/path/win32.js +++ b/api/path/win32.js @@ -1,16 +1,22 @@ +import * as mounts from './mounts.js' import * as posix from './posix.js' import location from '../location.js' import { Path } from './path.js' import url from '../url.js' - import { - RESOURCES, DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, - MUSIC + CONFIG, + MEDIA, + MUSIC, + HOME, + DATA, + LOG, + TMP } from './well-known.js' import * as exports from './win32.js' @@ -18,16 +24,24 @@ import * as exports from './win32.js' /** @typedef {import('./path.js').PathComponent} PathComponent */ export { + mounts, posix, Path, - RESOURCES, + // well known paths DOWNLOADS, DOCUMENTS, + RESOURCES, PICTURES, DESKTOP, VIDEOS, - MUSIC + CONFIG, + MEDIA, + MUSIC, + HOME, + DATA, + LOG, + TMP } export default exports @@ -74,10 +88,10 @@ export function dirname (path) { /** * Computes base name of path. * @param {PathComponent} path - * @param {string} suffix + * @param {string=} [suffix] * @return {string} */ -export function basename (path, suffix) { +export function basename (path, suffix = null) { return Path.basename({ sep }, path).replace(suffix || '', '') } diff --git a/api/process.js b/api/process.js index 77de01822e..57d866731f 100644 --- a/api/process.js +++ b/api/process.js @@ -1,5 +1,5 @@ /** - * @module Process + * @module process * * Example usage: * ```js @@ -8,20 +8,158 @@ */ import { primordials, send } from './ipc.js' import { EventEmitter } from './events.js' +import signal from './process/signal.js' +import tty from './tty.js' import os from './os.js' let didEmitExitEvent = false +let cwd = primordials.cwd + +export class ProcessEnvironmentEvent extends Event { + key + value + + constructor (type, key, value) { + super(type) + this.key = key + this.value = value ?? process.env[key] ?? undefined + } +} + +export class ProcessEnvironment extends EventTarget { + get [Symbol.toStringTag] () { + return 'ProcessEnvironment' + } +} + +export const env = Object.defineProperties(new ProcessEnvironment(), { + proxy: { + configurable: false, + enumerable: false, + writable: false, + value: new Proxy({}, { + get (_, property) { + if (Reflect.has(env, property)) { + return Reflect.get(env, property) + } + + return Reflect.get(globalThis.__args.env, property) + }, + + set (_, property, value) { + if (Reflect.get(env, property) !== value) { + env.dispatchEvent(new ProcessEnvironmentEvent('set', property, value)) + env.dispatchEvent(new ProcessEnvironmentEvent('change', property, value)) + } + return Reflect.set(env, property, value) + }, + + deleteProperty (_, property) { + if (Reflect.has(env, property)) { + // @ts-ignore + env.dispatchEvent(new ProcessEnvironmentEvent('delete', property)) + env.dispatchEvent(new ProcessEnvironmentEvent('change', property)) + } + return Reflect.deleteProperty(env, property) + }, + + getOwnPropertyDescriptor (_, property) { + if (Reflect.has(globalThis.__args.env, property)) { + return { + configurable: true, + enumerable: true, + writable: true, + value: globalThis.__args.env[property] + } + } + }, + + has (_, property) { + return ( + Reflect.has(env, property) || + Reflect.has(globalThis.__args.env, property) + ) + }, + + ownKeys (_) { + const keys = [] + keys.push(...Reflect.ownKeys(env)) + keys.push(...Reflect.ownKeys(globalThis.__args.env)) + return Array.from(new Set(keys)) + } + }) + } +}) class Process extends EventEmitter { - arch = primordials.arch - argv = globalThis.__args?.argv ?? [] - argv0 = globalThis.__args?.argv?.[0] ?? '' - cwd = () => primordials.cwd - env = { ...(globalThis.__args?.env ?? {}) } - exit = exit - homedir = homedir - platform = primordials.platform - version = primordials.version + // @ts-ignore + stdin = new tty.ReadStream(0) + // @ts-ignore + stdout = new tty.WriteStream(1) + // @ts-ignore + stderr = new tty.WriteStream(2) + + get version () { + return primordials.version.short + } + + get platform () { + return primordials.platform + } + + get env () { + return env.proxy + } + + get arch () { + return primordials.arch + } + + get argv () { + return globalThis.__args?.argv ?? [] + } + + get argv0 () { + return this.argv[0] ?? '' + } + + get execArgv () { + return [] + } + + get versions () { + return { + socket: this.version + } + } + + uptime () { + return os.uptime() + } + + cwd () { + return cwd + } + + exit (code) { + return exit(code) + } + + nextTick (callback, ...args) { + return nextTick(callback, ...args) + } + + hrtime (time = [0, 0]) { + return hrtime(time) + } + + memoryUsage () { + return memoryUsage + } + + chdir (dir) { + cwd = dir + } } const isNode = Boolean(globalThis.process?.versions?.node) @@ -33,29 +171,53 @@ if (!isNode) { EventEmitter.call(process) } +if (!isNode) { + signal.channel.addEventListener('message', (event) => { + if (event.data.signal) { + const code = event.data.signal + const name = signal.getName(code) + const message = signal.getMessage(code) + process.emit(name, name, code, message) + } + }) + + globalThis.addEventListener('signal', (event) => { + // @ts-ignore + if (event.detail.signal) { + // @ts-ignore + const code = event.detail.signal + const name = signal.getName(code) + const message = signal.getMessage(code) + process.emit(name, name, code, message) + } + }) +} + export default process /** * Adds callback to the 'nextTick' queue. * @param {Function} callback */ -export function nextTick (callback) { - if (typeof process.nextTick === 'function' && process.nextTick !== nextTick) { - process.nextTick(callback) - } else if (typeof globalThis.setImmediate === 'function') { - globalThis.setImmediate(callback) +export function nextTick (callback, ...args) { + if (isNode && typeof process.nextTick === 'function' && process.nextTick !== nextTick) { + process.nextTick(callback, ...args) } else if (typeof globalThis.queueMicrotask === 'function') { globalThis.queueMicrotask(() => { try { - callback() + // eslint-disable-next-line + callback(...args) } catch (err) { setTimeout(() => { throw err }) } }) + } else if (typeof globalThis.setImmediate === 'function') { + globalThis.setImmediate(callback, ...args) } else if (typeof globalThis.setTimeout === 'function') { - globalThis.setTimeout(callback) + globalThis.setTimeout(callback, ...args) } else if (typeof globalThis.requestAnimationFrame === 'function') { - globalThis.requestAnimationFrame(callback) + // eslint-disable-next-line + globalThis.requestAnimationFrame(() => callback(...args)) } else { throw new TypeError('\'process.nextTick\' is not supported in environment.') } @@ -65,13 +227,6 @@ if (typeof process.nextTick !== 'function') { process.nextTick = nextTick } -/** - * @returns {string} The home directory of the current user. - */ -export function homedir () { - return globalThis.__args.env.HOME ?? '' -} - /** * Computed high resolution time as a `BigInt`. * @param {Array<number>?} [time] @@ -98,6 +253,8 @@ if (typeof process.hrtime !== 'function') { process.hrtime = hrtime } +process.hrtime.bigint = hrtime.bigint + /** * @param {number=} [code=0] - The exit code. Default: 0. */ @@ -128,3 +285,5 @@ memoryUsage.rss = function rss () { const rusage = os.rusage() return rusage.ru_maxrss } + +process.memoryUsage.rss = memoryUsage.rss diff --git a/api/process/signal.js b/api/process/signal.js new file mode 100644 index 0000000000..90ae0b40e6 --- /dev/null +++ b/api/process/signal.js @@ -0,0 +1,229 @@ +/** + * @module signal + */ +import { signal as constants } from '../os/constants.js' +import { SignalEvent } from '../internal/events.js' +import os from '../os.js' + +/** + * @typedef {import('./os/constants.js').signal} signal + */ + +export { constants } + +export const channel = new BroadcastChannel('socket.runtime.signal') + +export const SIGHUP = constants.SIGHUP +export const SIGINT = constants.SIGINT +export const SIGQUIT = constants.SIGQUIT +export const SIGILL = constants.SIGILL +export const SIGTRAP = constants.SIGTRAP +export const SIGABRT = constants.SIGABRT +export const SIGIOT = constants.SIGIOT +export const SIGBUS = constants.SIGBUS +export const SIGFPE = constants.SIGFPE +export const SIGKILL = constants.SIGKILL +export const SIGUSR1 = constants.SIGUSR1 +export const SIGSEGV = constants.SIGSEGV +export const SIGUSR2 = constants.SIGUSR2 +export const SIGPIPE = constants.SIGPIPE +export const SIGALRM = constants.SIGALRM +export const SIGTERM = constants.SIGTERM +export const SIGCHLD = constants.SIGCHLD +export const SIGCONT = constants.SIGCONT +export const SIGSTOP = constants.SIGSTOP +export const SIGTSTP = constants.SIGTSTP +export const SIGTTIN = constants.SIGTTIN +export const SIGTTOU = constants.SIGTTOU +export const SIGURG = constants.SIGURG +export const SIGXCPU = constants.SIGXCPU +export const SIGXFSZ = constants.SIGXFSZ +export const SIGVTALRM = constants.SIGVTALRM +export const SIGPROF = constants.SIGPROF +export const SIGWINCH = constants.SIGWINCH +export const SIGIO = constants.SIGIO +export const SIGINFO = constants.SIGINFO +export const SIGSYS = constants.SIGSYS + +export const strings = { + [SIGHUP]: 'Terminal line hangup', + [SIGINT]: 'Interrupt program', + [SIGQUIT]: 'Quit program', + [SIGILL]: 'Illegal instruction', + [SIGTRAP]: 'Trace trap', + [SIGABRT]: 'Abort program', + [SIGIOT]: 'Abort program', + [SIGBUS]: 'Bus error', + [SIGFPE]: 'Floating-point exception', + [SIGKILL]: 'Kill program', + [SIGUSR1]: 'User defined signal 1', + [SIGSEGV]: 'Segmentation violation', + [SIGUSR2]: 'User defined signal 2', + [SIGPIPE]: 'Write on a pipe with no reader', + [SIGALRM]: 'Real-time timer expired', + [SIGTERM]: 'Software termination signal', + [SIGCHLD]: 'Child status has changed', + [SIGCONT]: 'Continue after stop', + [SIGSTOP]: 'Stop signal', + [SIGTSTP]: 'Stop signal generated from keyboard', + [SIGTTIN]: ' Background read attempted from control terminal', + [SIGTTOU]: 'Background write attempted to control terminal', + [SIGURG]: 'Urgent condition present on socket', + [SIGXCPU]: 'Urgent condition present on socket', + [SIGXFSZ]: 'File size limit exceeded', + [SIGVTALRM]: 'Virtual time alarm', + [SIGPROF]: 'Profiling timer alarm', + [SIGWINCH]: 'Window size change', + [SIGIO]: 'I/O is possible on a descriptor', + [SIGINFO]: 'Status request from keyboard', + [SIGSYS]: 'Non-existent system call invoked' +} + +/** + * Converts an `signal` code to its corresponding string message. + * @param {import('./os/constants.js').signal} {code} + * @return {string} + */ +export function toString (code) { + return strings[code] ?? '' +} + +/** + * Gets the code for a given 'signal' name. + * @param {string|number} name + * @return {signal} + */ +export function getCode (name) { + if (typeof name !== 'string') { + name = name.toString() + } + + name = name.toUpperCase() + for (const key in constants) { + if (name === key) { + return constants[key] + } + } + + return 0 +} + +/** + * Gets the name for a given 'signal' code + * @return {string} + * @param {string|number} code + */ +export function getName (code) { + if (typeof code === 'string') { + code = getCode(code) + } + + for (const key in constants) { + const value = constants[key] + if (value === code) { + return key + } + } + + return '' +} + +/** + * Gets the message for a 'signal' code. + * @param {number|string} code + * @return {string} + */ +export function getMessage (code) { + if (typeof code === 'string') { + code = getCode(code) + } + + return toString(code) +} + +/** + * Add a signal event listener. + * @param {string|number} signal + * @param {function(SignalEvent)} callback + * @param {{ once?: boolean }=} [options] + */ +export function addEventListener (signalName, callback, options = null) { + const name = getName(signalName) + globalThis.addEventListener(name, callback, options) +} + +/** + * Remove a signal event listener. + * @param {string|number} signal + * @param {function(SignalEvent)} callback + * @param {{ once?: boolean }=} [options] + */ +export function removeEventListener (signalName, callback, options = null) { + const name = getName(signalName) + return globalThis.removeEventListener(name, callback, options) +} + +if (!/android|ios/i.test(os.platform())) { + channel.addEventListener('message', (event) => { + onSignal(event.data.signal) + }) + + globalThis.addEventListener?.('signal', (event) => { + onSignal(event.detail.signal) + channel.postMessage(event.detail) + }) +} + +function onSignal (code) { + const name = getName(code) + const message = getMessage(code) + globalThis.dispatchEvent(new SignalEvent(name, { + code, + message + })) +} + +export default { + addEventListener, + removeEventListener, + constants, + channel, + strings, + toString, + getName, + getCode, + getMessage, + + // constants + SIGHUP, + SIGINT, + SIGQUIT, + SIGILL, + SIGTRAP, + SIGABRT, + SIGIOT, + SIGBUS, + SIGFPE, + SIGKILL, + SIGUSR1, + SIGSEGV, + SIGUSR2, + SIGPIPE, + SIGALRM, + SIGTERM, + SIGCHLD, + SIGCONT, + SIGSTOP, + SIGTSTP, + SIGTTIN, + SIGTTOU, + SIGURG, + SIGXCPU, + SIGXFSZ, + SIGVTALRM, + SIGPROF, + SIGWINCH, + SIGIO, + SIGINFO, + SIGSYS +} diff --git a/api/querystring.js b/api/querystring.js new file mode 100644 index 0000000000..954224f605 --- /dev/null +++ b/api/querystring.js @@ -0,0 +1,429 @@ +/* eslint-disable */ +// The MIT License (MIT) +// +// Copyright (c) 2015 Mathias Rasmussen +// +// 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. +// +// Query String Utilities - https://github.com/mathiasvr/querystring + +import { Buffer } from './buffer.js' + +const HEX_TABLE = new Array(256) + +for (var i = 0; i < 256; ++i) { + HEX_TABLE[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase() +} + + +// a safe fast alternative to decodeURIComponent +export function unescapeBuffer (s, decodeSpaces) { + var out = new Buffer(s.length); + var state = 0; + var n, m, hexchar; + + for (var inIndex = 0, outIndex = 0; inIndex <= s.length; inIndex++) { + var c = inIndex < s.length ? s.charCodeAt(inIndex) : NaN; + switch (state) { + case 0: // Any character + switch (c) { + case 37: // '%' + n = 0; + m = 0; + state = 1; + break; + case 43: // '+' + if (decodeSpaces) + c = 32; // ' ' + // falls through + default: + out[outIndex++] = c; + break; + } + break; + + case 1: // First hex digit + hexchar = c; + if (c >= 48/*0*/ && c <= 57/*9*/) { + n = c - 48/*0*/; + } else if (c >= 65/*A*/ && c <= 70/*F*/) { + n = c - 65/*A*/ + 10; + } else if (c >= 97/*a*/ && c <= 102/*f*/) { + n = c - 97/*a*/ + 10; + } else { + out[outIndex++] = 37/*%*/; + out[outIndex++] = c; + state = 0; + break; + } + state = 2; + break; + + case 2: // Second hex digit + state = 0; + if (c >= 48/*0*/ && c <= 57/*9*/) { + m = c - 48/*0*/; + } else if (c >= 65/*A*/ && c <= 70/*F*/) { + m = c - 65/*A*/ + 10; + } else if (c >= 97/*a*/ && c <= 102/*f*/) { + m = c - 97/*a*/ + 10; + } else { + out[outIndex++] = 37/*%*/; + out[outIndex++] = hexchar; + out[outIndex++] = c; + break; + } + out[outIndex++] = 16 * n + m; + break; + } + } + + return out.slice(0, outIndex - 1); +} + +export function unescape (s, decodeSpaces) { + try { + return decodeURIComponent(s); + } catch (e) { + return unescapeBuffer(s, decodeSpaces).toString(); + } +} + +export function escape (str) { + // replaces encodeURIComponent + // http://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 + if (typeof str !== 'string') + str += ''; + var out = ''; + var lastPos = 0; + + for (var i = 0; i < str.length; ++i) { + var c = str.charCodeAt(i); + + // These characters do not need escaping (in order): + // ! - . _ ~ + // ' ( ) * + // digits + // alpha (uppercase) + // alpha (lowercase) + if (c === 0x21 || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E || + (c >= 0x27 && c <= 0x2A) || + (c >= 0x30 && c <= 0x39) || + (c >= 0x41 && c <= 0x5A) || + (c >= 0x61 && c <= 0x7A)) { + continue; + } + + if (i - lastPos > 0) + out += str.slice(lastPos, i); + + // Other ASCII characters + if (c < 0x80) { + lastPos = i + 1; + out += HEX_TABLE[c]; + continue; + } + + // Multi-byte characters ... + if (c < 0x800) { + lastPos = i + 1; + out += HEX_TABLE[0xC0 | (c >> 6)] + HEX_TABLE[0x80 | (c & 0x3F)]; + continue; + } + if (c < 0xD800 || c >= 0xE000) { + lastPos = i + 1; + out += HEX_TABLE[0xE0 | (c >> 12)] + + HEX_TABLE[0x80 | ((c >> 6) & 0x3F)] + + HEX_TABLE[0x80 | (c & 0x3F)]; + continue; + } + // Surrogate pair + ++i; + var c2; + if (i < str.length) + c2 = str.charCodeAt(i) & 0x3FF; + else + throw new URIError('URI malformed'); + lastPos = i + 1; + c = 0x10000 + (((c & 0x3FF) << 10) | c2); + out += HEX_TABLE[0xF0 | (c >> 18)] + + HEX_TABLE[0x80 | ((c >> 12) & 0x3F)] + + HEX_TABLE[0x80 | ((c >> 6) & 0x3F)] + + HEX_TABLE[0x80 | (c & 0x3F)]; + } + if (lastPos === 0) + return str; + if (lastPos < str.length) + return out + str.slice(lastPos); + + return out; +} + +function stringifyPrimitive (v) { + if (typeof v === 'string') + return v; + if (typeof v === 'number' && isFinite(v)) + return '' + v; + if (typeof v === 'boolean') + return v ? 'true' : 'false'; + return ''; +} + +export function stringify (obj, sep, eq, options) { + sep = sep || '&'; + eq = eq || '='; + + var encode = escape; + if (options && typeof options.encodeURIComponent === 'function') { + encode = options.encodeURIComponent; + } + + if (obj !== null && typeof obj === 'object') { + var keys = Object.keys(obj); + var len = keys.length; + var flast = len - 1; + var fields = ''; + for (var i = 0; i < len; ++i) { + var k = keys[i]; + var v = obj[k]; + var ks = encode(stringifyPrimitive(k)) + eq; + + if (Array.isArray(v)) { + var vlen = v.length; + var vlast = vlen - 1; + for (var j = 0; j < vlen; ++j) { + fields += ks + encode(stringifyPrimitive(v[j])); + if (j < vlast) + fields += sep; + } + if (vlen && i < flast) + fields += sep; + } else { + fields += ks + encode(stringifyPrimitive(v)); + if (i < flast) + fields += sep; + } + } + return fields; + } + return ''; +} + +// Parse a key/val string. +export function parse (qs, sep, eq, options) { + sep = sep || '&'; + eq = eq || '='; + + var obj = {} + + if (typeof qs !== 'string' || qs.length === 0) { + return obj; + } + + if (typeof sep !== 'string') + sep += ''; + + var eqLen = eq.length; + var sepLen = sep.length; + + var maxKeys = 1000; + if (options && typeof options.maxKeys === 'number') { + maxKeys = options.maxKeys; + } + + var pairs = Infinity; + if (maxKeys > 0) + pairs = maxKeys; + + var decode = unescape; + if (options && typeof options.decodeURIComponent === 'function') { + decode = options.decodeURIComponent; + } + var customDecode = (decode !== unescape); + + var keys = []; + var lastPos = 0; + var sepIdx = 0; + var eqIdx = 0; + var key = ''; + var value = ''; + var keyEncoded = customDecode; + var valEncoded = customDecode; + var encodeCheck = 0; + for (var i = 0; i < qs.length; ++i) { + var code = qs.charCodeAt(i); + + // Try matching key/value pair separator (e.g. '&') + if (code === sep.charCodeAt(sepIdx)) { + if (++sepIdx === sepLen) { + // Key/value pair separator match! + var end = i - sepIdx + 1; + if (eqIdx < eqLen) { + // If we didn't find the key/value separator, treat the substring as + // part of the key instead of the value + if (lastPos < end) + key += qs.slice(lastPos, end); + } else if (lastPos < end) + value += qs.slice(lastPos, end); + if (keyEncoded) + key = decodeStr(key, decode); + if (valEncoded) + value = decodeStr(value, decode); + // Use a key array lookup instead of using hasOwnProperty(), which is + // slower + if (keys.indexOf(key) === -1) { + obj[key] = value; + keys[keys.length] = key; + } else { + var curValue = obj[key]; + // `instanceof Array` is used instead of Array.isArray() because it + // is ~15-20% faster with v8 4.7 and is safe to use because we are + // using it with values being created within this function + if (curValue instanceof Array) + curValue[curValue.length] = value; + else + obj[key] = [curValue, value]; + } + if (--pairs === 0) + break; + keyEncoded = valEncoded = customDecode; + encodeCheck = 0; + key = value = ''; + lastPos = i + 1; + sepIdx = eqIdx = 0; + } + continue; + } else { + sepIdx = 0; + if (!valEncoded) { + // Try to match an (valid) encoded byte (once) to minimize unnecessary + // calls to string decoding functions + if (code === 37/*%*/) { + encodeCheck = 1; + } else if (encodeCheck > 0 && + ((code >= 48/*0*/ && code <= 57/*9*/) || + (code >= 65/*A*/ && code <= 70/*Z*/) || + (code >= 97/*a*/ && code <= 102/*z*/))) { + if (++encodeCheck === 3) + valEncoded = true; + } else { + encodeCheck = 0; + } + } + } + + // Try matching key/value separator (e.g. '=') if we haven't already + if (eqIdx < eqLen) { + if (code === eq.charCodeAt(eqIdx)) { + if (++eqIdx === eqLen) { + // Key/value separator match! + var end = i - eqIdx + 1; + if (lastPos < end) + key += qs.slice(lastPos, end); + encodeCheck = 0; + lastPos = i + 1; + } + continue; + } else { + eqIdx = 0; + if (!keyEncoded) { + // Try to match an (valid) encoded byte once to minimize unnecessary + // calls to string decoding functions + if (code === 37/*%*/) { + encodeCheck = 1; + } else if (encodeCheck > 0 && + ((code >= 48/*0*/ && code <= 57/*9*/) || + (code >= 65/*A*/ && code <= 70/*Z*/) || + (code >= 97/*a*/ && code <= 102/*z*/))) { + if (++encodeCheck === 3) + keyEncoded = true; + } else { + encodeCheck = 0; + } + } + } + } + + if (code === 43/*+*/) { + if (eqIdx < eqLen) { + if (i - lastPos > 0) + key += qs.slice(lastPos, i); + key += '%20'; + keyEncoded = true; + } else { + if (i - lastPos > 0) + value += qs.slice(lastPos, i); + value += '%20'; + valEncoded = true; + } + lastPos = i + 1; + } + } + + // Check if we have leftover key or value data + if (pairs > 0 && (lastPos < qs.length || eqIdx > 0)) { + if (lastPos < qs.length) { + if (eqIdx < eqLen) + key += qs.slice(lastPos); + else if (sepIdx < sepLen) + value += qs.slice(lastPos); + } + if (keyEncoded) + key = decodeStr(key, decode); + if (valEncoded) + value = decodeStr(value, decode); + // Use a key array lookup instead of using hasOwnProperty(), which is + // slower + if (keys.indexOf(key) === -1) { + obj[key] = value; + keys[keys.length] = key; + } else { + var curValue = obj[key]; + // `instanceof Array` is used instead of Array.isArray() because it + // is ~15-20% faster with v8 4.7 and is safe to use because we are + // using it with values being created within this function + if (curValue instanceof Array) + curValue[curValue.length] = value; + else + obj[key] = [curValue, value]; + } + } + + return obj; +} + +export const decode = parse +export const encode = stringify + +export default { + decode, + encode, + parse, + stringify, + escape, + unescape +} + +function decodeStr(s, decoder) { + try { + return decoder(s); + } catch (e) { + return unescape(s, true); + } +} diff --git a/api/service-worker.js b/api/service-worker.js new file mode 100644 index 0000000000..ce00a3f976 --- /dev/null +++ b/api/service-worker.js @@ -0,0 +1,25 @@ +import { ExtendableEvent, FetchEvent } from './service-worker/events.js' +import { Environment } from './service-worker/env.js' +import { Context } from './service-worker/context.js' + +/** + * A reference to the opened environment. This value is an instance of an + * `Environment` if the scope is a ServiceWorker scope. + * @type {Environment|null} + */ +export const env = Environment.instance + +export { + ExtendableEvent, + FetchEvent, + Environment, + Context +} + +export default { + ExtendableEvent, + FetchEvent, + Environment, + Context, + env +} diff --git a/api/service-worker/background.svg b/api/service-worker/background.svg new file mode 100644 index 0000000000..c5fbbe4c12 --- /dev/null +++ b/api/service-worker/background.svg @@ -0,0 +1,580 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 27.6.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 1179 873" style="enable-background:new 0 0 1179 873;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#FFFFFF;} + .st1{fill:#D1D1D1;} + .st2{fill:#505355;} + .st3{fill:#0E5598;} + .st4{fill:url(#SVGID_1_);} + .st5{opacity:0.59;fill:#003C77;enable-background:new ;} + .st6{fill:#BCBCBC;} + .st7{opacity:0.59;fill:#636363;enable-background:new ;} + .st8{opacity:0.65;fill:#AAAAAA;enable-background:new ;} + .st9{fill:url(#SVGID_00000075125474166681529780000011613403109940336805_);} + .st10{fill:none;stroke:#505355;stroke-linecap:round;stroke-linejoin:round;} + .st11{fill:none;stroke:#505355;stroke-linejoin:round;} +</style> +<path class="st0" d="M569,55.7c-1.6-1.8-3.4-3.5-5.4-4.9c-0.4-0.3-0.8-0.6-1.3-0.6c-0.8-0.1-1.6,0.5-2,1.1c-0.4,0.7-0.4,1.5-0.3,2.3 + c0.1,2,0.3,4,0.6,6c0.1,0.8,0.3,1.5,0.1,2.3s-0.9,1.4-1.7,1.4c-0.4,0-0.9-0.2-1.2,0.1c-0.1,0.1-0.2,0.3-0.2,0.5 + c-0.2,2.2-2.6,3.3-3.5,5.4c-1.4,3.2-1.1,11.2-0.9,14.5c0.2,2.1,0.9,5.1,1.4,6.3c2.2,5.7,2.7,12.1,7.4,17.4 + c-1.7,2.2,8.2-10.2,6.5-7.9c-0.3,0.4,0.9,0.9,1.1,1.4c0.1,0.3,1,1.3,1.2,1.5c3.3,1.9,4.4,1.6,8.3,1.7c1.5,0,6.5-1.3,7.8-2.1 + c1.1-0.8,1.5-2,0.9-3.3c-0.8-0.8-0.6-1.1-1.7-1.3c-0.6-0.1-1.3,0-1.9-0.1c-2.7-0.5-3.7-3.8-2.8-6.2c0.9-2.4,3.1-4.1,5-5.9 + s3.7-4.1,3.5-6.6c-0.1-1-0.4-1.9-0.9-2.7c-0.8-1.6-1.8-3.1-3.1-4.4c-1-1-2-1.9-2.6-3.2c-0.8-1.6-0.6-3.4-1-5.2 + c-1-4-5.7-6.9-10.1-6.1c-1.9,0.3-3.8,1.5-3.9,3.3c-0.1,0.7,0.2,1.5-0.1,2.2s-1.5,1.1-1.8,0.4"/> +<path class="st1" d="M617.2,56.2c0.1-0.7-0.4-1.1-0.9-1.2c-0.2-1.2-0.4-2.4-0.7-3.5c-0.7-2.6-1.7-5-3.1-7.3l0,0 + c-0.2-0.7-0.5-1.2-0.9-1.8c0.1,0,0.2,0.1,0.3,0.1c-1.2-1.4-2.1-3-3.1-4.5c-1.1-1.2-2.4-2.3-3.7-3.2c-3.7-4.1-8.7-6.6-13.9-8.4 + c-0.2,0.1-0.4,0.1-0.6,0.1c-2.3,0-4.7-0.1-7-0.2c-2,0-4.2-0.2-6.2,0.4c-1.8,0.5-3.3,1.6-4.9,2.3c-1.5,0.6-3.1,1-4.2,2.2 + c-0.2,0.4-0.4,0.8-0.8,1.1c-2.3,1.5-4.4,3.3-6.3,5.3c-0.5,1.9-1.4,3.7-2.3,5.5c-0.3,0.6-0.6,1.1-0.8,1.7c-0.1,0.6-0.2,1.3-0.3,1.9 + c-1.7,5.3-1.5,11,0.6,16.2c0.1,0.3,0.3,0.5,0.5,0.6l0,0c0,0,0,0,0,0.1l0,0c0,0.1,0.1,0.2,0.1,0.2c0.5,1,2.1,0.3,1.8-0.8 + c-0.3-1-0.5-1.9-0.7-2.9c-0.2-2.8-0.1-5.5,0.3-8.2c0.3,0,0.7-0.1,1-0.1c1.7,0.5,3.3,1.2,4.8,2.1c0.1,0.2,0.2,0.4,0.3,0.6 + c0.6,0.1,1.2,0.4,1.5,1c1,2.2,1.2,4.4,0.9,6.7c-0.1,0.4-0.1,0.9-0.2,1.3c1.3-0.8,2.8-1.4,4.1-2.1c0.1-0.1,0.2-0.1,0.4-0.2 + c0,0,0-0.1,0.1-0.1c0.2,0,0.3-0.1,0.4-0.3c0.6-0.7,1.2-1.4,1.8-2.1c0.2-0.7,0.7-1.3,1.4-1.5c1-0.3,2.2,0.4,2.5,1.4 + c0.2,0.8,0.1,1.6-0.1,2.4c0.3,0.3,0.6,0.6,0.9,0.9c0.2,0.1,0.3,0.2,0.5,0.4c0.7,0.6,1.5,1.2,2.4,1.7c1.2,1.8,2.5,3.5,4.9,3.9 + c1,0.2,1.8,0,2.5-0.4c1.6,0.8,3.3,1.3,5.1,1.5c2.1,0.6,4.1,0.7,6.2,0.4c0.3,0.3,0.9,0.5,1.4,0c0.2-0.1,0.3-0.3,0.5-0.5 + c2.4-0.7,4.8-1.8,6.9-3.1c0.2,0.2,0.4,0.3,0.7,0.3c1.1-0.1,1.7-1,1.8-1.8c1.1-0.5,2.1-1,2.8-1.8c1-1.1,1-2.7,0.9-4.1 + C616.9,57.6,617.1,56.9,617.2,56.2z M578,55.8c0.1-0.1,0.1-0.1,0.2-0.2c0.3,0.8,0.7,1.5,1,2.2C578.7,57.2,578.3,56.5,578,55.8z"/> +<path class="st2" d="M183.2,474.2c-1.1,0-2.2-0.3-3.2-0.8c-2.3-1.2-4-4.1-5-8.2l-64.1-221.4c-1.6-6.5,0.5-11.8,6.1-15L422.4,52 + c0.9-0.6,1.8-0.9,2.9-1.1c1-0.1,2.1,0,3,0.3c2.2,0.9,3.9,3.3,5,7l64.6,223.7c2.3,7.3-0.6,12.9-8.8,17L186.4,473.6l-0.1,0.1 + C185.3,474,184.2,474.2,183.2,474.2z M426.3,52c-1.2,0.1-2.3,0.4-3.3,1L117.5,229.8c-3.6,2.1-7.4,6.1-5.5,13.6l64.2,221.4 + c0.9,3.8,2.4,6.3,4.4,7.4c0.8,0.4,1.7,0.6,2.6,0.7c0.9,0,1.8-0.1,2.6-0.5l302.7-174.8c7.8-3.8,10.4-8.8,8.2-15.5L432.1,58.5 + c-1-3.3-2.4-5.4-4.2-6.2C427.3,52.1,426.8,52,426.3,52z"/> +<path class="st2" d="M182.4,474.2c-7.9,0-12.3-2.2-13.4-6.7l-65.2-223.7c-1.6-6.5,0.5-11.8,6.1-15L415.4,52 + c4.6-2.6,11.5-1.1,11.8-1.1l-0.3,1.2c-0.1,0-6.7-1.4-10.9,1L110.5,229.8c-3.6,2.1-7.4,6.1-5.5,13.6l65.2,223.8 + c0.9,3.9,4.9,5.8,12.2,5.8V474.2z"/> +<path class="st2" d="M403.1,615.9c-3.7,0-7.4-0.9-10.7-2.7l-204-117.7c-3.4-2-5.1-4.6-4.7-7.2v-12h1.2v12.1c-0.3,2.1,1.2,4.4,4.1,6 + l204,117.8c5.2,3,13.2,3.4,17.7,0.8l302.9-174.8c2.4-1.4,2.9-2.8,2.7-5.3v-9.7h1.2v9.6c0.2,2.7-0.3,4.6-3.3,6.4L411.4,614 + C408.8,615.3,406,615.9,403.1,615.9z"/> +<path class="st2" d="M322.2,541c-1.8,0-3.6-0.4-5.2-1.3l-104.2-60.2c-0.7-0.3-1.2-0.8-1.7-1.4c-0.4-0.6-0.7-1.3-0.8-2 + c0-0.6,0.2-1.1,0.5-1.6s0.8-0.9,1.3-1.1l280.6-162c1.5-0.7,3.1-1,4.7-1c1.6,0.1,3.2,0.5,4.6,1.3l104.2,60.2c0.7,0.3,1.2,0.8,1.7,1.4 + c0.4,0.6,0.7,1.3,0.8,2c0,0.6-0.2,1.1-0.5,1.6s-0.8,0.9-1.3,1.1L326.3,540C325.1,540.7,323.7,541,322.2,541z M496.9,311.7 + c-1.2,0-2.4,0.2-3.5,0.8l-280.6,162c-0.3,0.1-0.6,0.4-0.8,0.7s-0.3,0.6-0.4,1c0,0.8,0.7,1.6,1.8,2.3l104.2,60.1 + c1.2,0.7,2.6,1.1,4,1.1c1.4,0.1,2.8-0.2,4.1-0.8l280.6-162c0.3-0.1,0.6-0.4,0.8-0.7c0.2-0.3,0.3-0.6,0.4-1c-0.1-0.5-0.3-1-0.6-1.4 + s-0.7-0.7-1.2-0.9l-104.2-60.2C500.1,312,498.5,311.6,496.9,311.7L496.9,311.7z"/> +<path class="st2" d="M526,397.8c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9s0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9s-0.4,0.5-0.7,0.6l-12.7,7.3C527.3,397.7,526.6,397.8,526,397.8z M525.7,380.3 + c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.3-0.4,0.5s0.2,0.5,0.7,0.7l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5c0.6,0,1.1-0.1,1.7-0.3 + L540,389c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.6-0.8l-12-7C527.1,380.4,526.4,380.3,525.7,380.3L525.7,380.3z"/> +<path class="st2" d="M505.7,409.5c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.8,0.8 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9s-0.4,0.5-0.7,0.6l-12.7,7.3C507.1,409.4,506.4,409.5,505.7,409.5z M505.5,392 + c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.3-0.4,0.5s0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5c0.6,0,1.2-0.1,1.7-0.3 + l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C506.8,392.2,506.1,392,505.5,392L505.5,392z"/> +<path class="st2" d="M485.5,421.2c-0.9,0-1.8-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C486.9,421.1,486.2,421.2,485.5,421.2z + M485.2,403.7c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.2-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C486.6,403.8,485.9,403.7,485.2,403.7z"/> +<path class="st2" d="M465.2,432.9c-0.9,0-1.8-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.3,0.6l12.1,7c0.3,0.2,0.6,0.4,0.8,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C466.6,432.7,465.9,432.9,465.2,432.9z + M465,415.4c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C466.3,415.5,465.7,415.3,465,415.4z"/> +<path class="st3" d="M444.2,443.7c-0.8,0-1.7-0.2-2.4-0.6l-11.5-6.6c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1 + c0-0.3,0.1-0.6,0.3-0.8c0.2-0.3,0.4-0.5,0.7-0.6l12.1-7c0.7-0.3,1.4-0.5,2.2-0.5c0.7,0,1.5,0.2,2.1,0.6l11.5,6.6 + c0.3,0.2,0.6,0.4,0.8,0.7c0.2,0.3,0.3,0.7,0.4,1c0,0.3-0.1,0.6-0.3,0.8s-0.4,0.5-0.7,0.6l-12.1,7 + C445.6,443.5,444.9,443.7,444.2,443.7z M444,427c-0.5,0-0.9,0.1-1.3,0.3l-12.1,7c-0.1,0.1-0.3,0.2-0.3,0.4c0,0.2,0.2,0.5,0.6,0.7 + l11.5,6.6c0.5,0.3,1,0.4,1.6,0.4c0.5,0,1.1-0.1,1.6-0.3l12.1-7c0.1-0.1,0.3-0.2,0.3-0.4c0-0.2-0.2-0.5-0.6-0.7l-11.5-6.7 + C445.3,427.1,444.6,427,444,427L444,427z"/> +<path class="st2" d="M424.8,456.2c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C426.1,456.1,425.5,456.2,424.8,456.2z + M424.5,438.7c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5s0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.3,0.4-0.5s-0.2-0.5-0.7-0.7l-12.1-7C425.9,438.9,425.2,438.7,424.5,438.7z"/> +<path class="st2" d="M404.5,467.9c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.3,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C405.9,467.8,405.2,467.9,404.5,467.9z + M404.3,450.4c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5s-0.2-0.5-0.7-0.7l-12.1-7C405.6,450.5,405,450.4,404.3,450.4z"/> +<path class="st2" d="M384.4,479.6c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.3,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C385.8,479.5,385.1,479.6,384.4,479.6z + M384.1,462.1c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.7l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.2-7C385.4,462.2,384.7,462.1,384.1,462.1 + L384.1,462.1z"/> +<path class="st2" d="M364.1,491.3c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C365.5,491.1,364.8,491.3,364.1,491.3z + M363.8,473.8c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.6,0.7l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C365.2,473.9,364.5,473.7,363.8,473.8z"/> +<path class="st2" d="M343.8,503c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,6.9 + c0.3,0.2,0.6,0.4,0.8,0.8c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3 + C345.2,502.8,344.5,503,343.8,503z M343.6,485.4c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.3-0.4,0.5s0.2,0.5,0.7,0.7l12.1,7 + c0.5,0.3,1.1,0.4,1.6,0.5c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.6-0.8l-12.1-7 + C344.9,485.6,344.3,485.4,343.6,485.4z"/> +<path class="st2" d="M516.8,380.9c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.4c0.8,0,1.6,0.3,2.2,0.7l12.1,7 + c0.3,0.2,0.6,0.4,0.9,0.7c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9s-0.4,0.5-0.7,0.6l-12.7,7.3 + C518.2,380.8,517.5,380.9,516.8,380.9z M516.6,363.4c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5 + c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5s-0.2-0.5-0.7-0.7 + l-12.1-7C517.9,363.5,517.2,363.3,516.6,363.4z"/> +<path class="st2" d="M496.6,392.6c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.3,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + s0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9s-0.4,0.5-0.7,0.6l-12.7,7.3C498,392.4,497.3,392.6,496.6,392.6z M496.3,375 + c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C497.7,375.2,497,375,496.3,375z"/> +<path class="st2" d="M476.3,404.2c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C477.7,404.1,477,404.3,476.3,404.2z + M476.1,386.7c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.6,0.7l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C477.4,386.9,476.8,386.7,476.1,386.7 + L476.1,386.7z"/> +<path class="st3" d="M456.1,415.9c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9s-0.4,0.5-0.7,0.6l-12.7,7.3C457.5,415.8,456.8,415.9,456.1,415.9z M455.9,398.4 + c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.3-0.4,0.5s0.2,0.5,0.7,0.7l12.1,7c0.5,0.3,1.1,0.5,1.6,0.5s1.1-0.1,1.7-0.3 + l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.6-0.8l-12.1-7C457.2,398.6,456.5,398.4,455.9,398.4z"/> +<path class="st2" d="M412.4,395.2c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.8,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C413.8,395,413.1,395.2,412.4,395.2z + M412.2,377.7c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.3-0.4,0.5s0.2,0.5,0.7,0.7l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12-7C413.6,377.8,412.9,377.7,412.2,377.7z"/> +<path class="st2" d="M392.1,406.9c-0.9,0-1.7-0.2-2.5-0.6l-12-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.8,0.8 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C393.6,406.7,392.9,406.9,392.1,406.9z + M391.9,389.3c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.3-0.4,0.5s0.2,0.5,0.7,0.8l12.1,6.9c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C393.2,389.5,392.6,389.3,391.9,389.3z"/> +<path class="st2" d="M390.2,430.8c-0.9,0-1.8-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C391.6,430.6,390.9,430.8,390.2,430.8z + M390,413.2c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.2-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.7-0.8l-12.1-7C391.3,413.4,390.7,413.2,390,413.2 + L390,413.2z"/> +<path class="st2" d="M351.7,430.2c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.2,0.6l12.1,7c0.3,0.2,0.6,0.4,0.8,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9s-0.4,0.5-0.7,0.6l-12.7,7.3C353.1,430.1,352.4,430.3,351.7,430.2z M351.4,412.7 + c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.2,0.4-0.5c0-0.2-0.2-0.5-0.6-0.8l-12.1-7C352.7,412.9,352.1,412.7,351.4,412.7z"/> +<path class="st2" d="M331.4,441.9c-0.9,0-1.7-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.3,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C332.8,441.8,332.1,441.9,331.4,441.9z + M331.2,424.4c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.3,0.4-0.5s-0.2-0.5-0.7-0.8l-12.1-7C332.5,424.6,331.9,424.4,331.2,424.4z"/> +<path class="st2" d="M311.2,453.6c-0.9,0-1.8-0.2-2.5-0.6l-12.1-7c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l12.7-7.3c0.7-0.3,1.5-0.5,2.3-0.5s1.6,0.2,2.3,0.6l12.1,7c0.3,0.2,0.6,0.4,0.9,0.7 + c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9c-0.2,0.3-0.4,0.5-0.7,0.6l-12.7,7.3C312.6,453.4,311.9,453.6,311.2,453.6z M311,436 + c-0.5,0-1,0.1-1.4,0.3l-12.7,7.3c-0.1,0.1-0.4,0.2-0.4,0.5c0,0.2,0.2,0.5,0.7,0.8l12.1,7c0.5,0.3,1.1,0.4,1.6,0.5 + c0.6,0,1.1-0.1,1.7-0.3l12.7-7.3c0.1-0.1,0.4-0.3,0.4-0.5s-0.2-0.5-0.7-0.7l-12.1-7C312.3,436.2,311.7,436,311,436z"/> +<path class="st3" d="M423.8,479c-0.9,0-1.7-0.2-2.4-0.6l-11.8-6.8c-0.3-0.2-0.6-0.4-0.8-0.7c-0.2-0.3-0.3-0.7-0.4-1.1 + c0-0.3,0.1-0.6,0.3-0.9c0.2-0.3,0.4-0.5,0.7-0.6l93.7-54.1c0.7-0.3,1.5-0.5,2.3-0.5s1.5,0.2,2.2,0.6l11.8,6.8 + c0.3,0.2,0.6,0.4,0.8,0.7c0.2,0.3,0.3,0.7,0.4,1.1c0,0.3-0.1,0.6-0.3,0.9s-0.4,0.5-0.7,0.6l-93.7,54.1 + C425.2,478.9,424.5,479.1,423.8,479z M504.9,415c-0.5,0-0.9,0.1-1.4,0.3l-93.7,54.1c-0.1,0.1-0.4,0.2-0.4,0.4c0,0.2,0.2,0.5,0.6,0.7 + l11.8,6.8c0.5,0.3,1,0.4,1.6,0.5c0.6,0,1.1-0.1,1.6-0.3l93.7-54.1c0.1-0.1,0.4-0.2,0.4-0.4s-0.2-0.5-0.6-0.7l-11.8-6.8 + C506.3,415.2,505.6,415,504.9,415L504.9,415z"/> +<path class="st2" d="M494,546.9c-1.5,0-2.9-0.3-4.2-1l-78.5-45.3c-0.5-0.2-1-0.6-1.4-1.1c-0.3-0.5-0.6-1.1-0.6-1.6s0.2-0.9,0.4-1.3 + c0.3-0.4,0.6-0.7,1.1-0.9L529.7,427c1.2-0.5,2.5-0.8,3.8-0.8s2.6,0.4,3.7,1l78.5,45.3c0.5,0.2,1,0.6,1.4,1.1 + c0.3,0.5,0.6,1.1,0.6,1.7c0,0.5-0.2,0.9-0.4,1.3s-0.6,0.7-1.1,0.9l-118.9,68.7C496.3,546.7,495.2,547,494,546.9z M533.1,427.3 + c-0.9,0-1.8,0.2-2.7,0.6l-118.9,68.7c-0.2,0.1-0.5,0.3-0.6,0.5c-0.2,0.2-0.3,0.5-0.3,0.7c0,0.6,0.5,1.2,1.4,1.7l78.5,45.3 + c1,0.5,2,0.8,3.1,0.9c1.1,0,2.2-0.2,3.2-0.6l118.9-68.7c0.2-0.1,0.4-0.3,0.6-0.5s0.2-0.5,0.3-0.7c0-0.6-0.5-1.2-1.4-1.7l-78.5-45.3 + C535.6,427.7,534.4,427.3,533.1,427.3L533.1,427.3z"/> +<path class="st2" d="M403.1,603.8c-3.7,0-7.4-0.9-10.7-2.7l-204-117.7c-3-1.8-4.8-4.1-4.8-6.5c0.1-1.1,0.4-2.1,1-3s1.4-1.6,2.4-2.1 + l302.9-174.9c4.9-2.8,13.4-2.4,18.9,0.8l204,117.8c3,1.8,4.8,4.1,4.8,6.5c-0.1,1.1-0.4,2.1-1,3s-1.5,1.6-2.4,2.1l-111.6,64.5 + c-0.3,0.2-0.6,0.4-0.8,0.7c-0.2,0.3-0.3,0.7-0.3,1l-0.2,2.2c0,0.9-0.2,1.7-0.7,2.4c-0.4,0.7-1,1.4-1.8,1.8L523,542.9 + c-0.3,0.2-0.6,0.3-0.9,0.3c-0.3,0-0.7,0-1-0.1s-0.6-0.2-0.9-0.5c-0.3-0.2-0.5-0.5-0.6-0.8c-0.2-0.3-0.3-0.7-0.3-1.1 + c-0.1-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.1-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1s-0.3,0.1-0.4,0.1l-106.4,61.4 + C408.9,603.3,406,603.9,403.1,603.8z M498.2,296.3c-2.7-0.1-5.3,0.5-7.7,1.7L187.7,472.8c-0.8,0.4-1.5,0.9-2,1.6s-0.8,1.6-0.9,2.4 + c0,1.9,1.5,3.9,4.2,5.4L393,600c5.2,3,13.2,3.4,17.7,0.8l106.4-61.4c0.3-0.1,0.5-0.2,0.8-0.3c0.3,0,0.6,0,0.9,0.1 + c0.3,0.1,0.5,0.2,0.8,0.4c0.2,0.2,0.4,0.4,0.6,0.7c0,0.1,0.1,0.2,0.1,0.3s0,0.1,0,0.2c0,0.2,0.1,0.5,0.2,0.7 + c0.1,0.2,0.3,0.4,0.5,0.5s0.4,0.2,0.7,0.2c0.2,0,0.5-0.1,0.7-0.2l75.8-43.2c0.6-0.3,1-0.8,1.4-1.4c0.3-0.6,0.5-1.2,0.5-1.9l0.2-2.3 + c0-0.6,0.2-1.1,0.5-1.6s0.7-0.9,1.2-1.2L713.5,426c0.8-0.4,1.5-0.9,2-1.6s0.8-1.6,0.9-2.4c0-1.9-1.5-3.9-4.2-5.5l-204-117.8 + C505.2,297.1,501.7,296.2,498.2,296.3L498.2,296.3z"/> +<path class="st2" d="M778.8,654.3c-3.5,0.1-6.9-0.7-10-2.3l-77.1-44.5c-2.7-1.5-4.1-3.7-4.1-6.1c0-2.8,2.1-5.7,5.7-7.8L867.6,493 + c6.7-3.9,17-4.3,22.8-0.9l77.1,44.5c2.7,1.5,4.2,3.7,4.2,6.1c0,2.8-2.1,5.7-5.8,7.8L791.7,651C787.8,653.2,783.3,654.3,778.8,654.3z + M880.5,491c-4.3,0-8.5,1-12.3,3.1L694,594.7c-3.3,1.9-5.1,4.3-5.1,6.7c0,1.9,1.2,3.7,3.5,5l77.1,44.5c5.5,3.2,15.2,2.8,21.6-0.9 + l174.2-100.6c3.3-1.9,5.1-4.3,5.1-6.7c0-1.9-1.2-3.7-3.5-5l-77.1-44.5C886.9,491.7,883.7,490.9,880.5,491L880.5,491z"/> +<path class="st2" d="M778.8,664.9c-3.5,0.1-6.9-0.7-10-2.3l-77.1-44.5c-3-1.7-4.4-4.1-4.1-6.8v-10h1.2v10.1c-0.3,2.2,1,4.2,3.5,5.7 + l77.1,44.5c5.5,3.2,15.2,2.8,21.6-0.9l174.2-100.6c3.4-2,5.2-4.5,5.1-6.9v-10.4h1.2v10.3c0.1,2.9-2,5.8-5.8,8l-174,100.5 + C787.8,663.8,783.3,664.9,778.8,664.9z"/> +<path class="st2" d="M936.4,527.1c-0.5,0.3-1.1,0.4-1.7,0.4c-0.6,0-1.2-0.1-1.7-0.4c-0.1,0-0.2-0.1-0.3-0.2 + c-0.1-0.1-0.2-0.2-0.2-0.3c-0.1-0.1-0.1-0.3-0.1-0.4c0-0.1,0-0.3,0.1-0.4c0.1-0.1,0.1-0.3,0.2-0.4c0.1-0.1,0.2-0.2,0.4-0.2 + c0.5-0.3,1.1-0.4,1.7-0.4c0.6,0,1.2,0.1,1.7,0.4c0.1,0,0.2,0.1,0.3,0.2c0.1,0.1,0.2,0.2,0.2,0.3s0.1,0.3,0.1,0.4 + c0,0.1,0,0.3-0.1,0.4c0,0.1-0.1,0.3-0.2,0.4S936.5,527.1,936.4,527.1z"/> +<path class="st2" d="M926.9,523.6L908,512.7c-0.6-0.4-0.5-1,0.2-1.4c0.4-0.2,0.8-0.3,1.2-0.3s0.8,0.1,1.2,0.2l18.9,10.9 + c0.6,0.4,0.5,1-0.2,1.4c-0.4,0.2-0.8,0.3-1.2,0.3C927.7,523.8,927.3,523.8,926.9,523.6z"/> +<path class="st3" d="M221.4,703.2l-16.3-9.4v-32.9l16.3,9.4V703.2z"/> +<path class="st3" d="M221.4,670.1l-16.3-9.4l83-47.9l16.3,9.4L221.4,670.1z"/> +<path class="st2" d="M254.7,689.9c-0.1,0-0.2,0-0.3-0.1l-83-48c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3s0-0.2,0.1-0.3 + s0.1-0.2,0.2-0.2l83-47.9c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0.1l83,47.9c0.1,0,0.2,0.1,0.2,0.2c0.1,0.1,0.1,0.2,0.1,0.3 + s0,0.2-0.1,0.3s-0.1,0.2-0.2,0.2l-83,47.9C254.9,689.9,254.9,689.9,254.7,689.9z M173,641.4l81.8,47.2l81.8-47.2l-81.8-47.2 + L173,641.4z"/> +<path class="st2" d="M229.4,771.2l-57.9-33.4c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-95.8h1.2v95.4l57.6,33.3 + L229.4,771.2z"/> +<path class="st2" d="M338.3,641.4h-1.2v29.5h1.2V641.4z"/> +<path class="st2" d="M255.4,688.3h-1.3v46.4h1.3V688.3z"/> +<path class="st3" d="M348.9,705.9l-11.9-6.8l60.3-34.8l11.9,6.8L348.9,705.9z"/> +<path class="st2" d="M373.1,720.5c-0.1,0-0.2,0-0.3-0.1l-60.3-34.8c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3 + s0-0.2,0.1-0.3s0.1-0.2,0.2-0.2l60.3-34.8c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0.1l60.3,34.9c0.1,0.1,0.2,0.2,0.3,0.4 + c0,0.2,0,0.3-0.1,0.5c-0.1,0.1-0.1,0.2-0.2,0.2l-60.3,34.8C373.3,720.5,373.2,720.5,373.1,720.5z M314,685.1l59.1,34.2l59.1-34.2 + L373.1,651L314,685.1z"/> +<path class="st2" d="M313.4,685.1h-1.2v28.1h1.2V685.1z"/> +<path class="st2" d="M351,776.1l-0.6,1.1l22.4,12.9l0.6-1.1L351,776.1z"/> +<path class="st2" d="M373.4,790.1l-0.6-1.1l60-34.7V685h1.2v69.7c0,0.1,0,0.2-0.1,0.3s-0.1,0.2-0.2,0.2L373.4,790.1z"/> +<path class="st2" d="M373.7,719.9h-1.2v69.7h1.2V719.9z"/> +<path class="st2" d="M249.3,760.1H248v54.8h1.2v-54.8H249.3z"/> +<path class="st2" d="M259.7,766.1h-1.2v56h1.2V766.1z"/> +<path class="st2" d="M320.1,717.7l-71.9,41.6l0.6,1.1l71.9-41.6L320.1,717.7z"/> +<path class="st2" d="M330.9,724L259,765.6l0.6,1.1l71.9-41.6L330.9,724z"/> +<path class="st3" d="M278.3,777.8c-0.1,0-0.2,0-0.3-0.1l-48.7-28.5c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3 + s0-0.2,0.1-0.3s0.1-0.2,0.2-0.2l72.5-41.8c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0.1l48.7,28.5c0.1,0.1,0.2,0.1,0.2,0.2 + c0.1,0.1,0.1,0.2,0.1,0.3s0,0.2-0.1,0.3s-0.1,0.2-0.2,0.2l-72.4,41.8C278.5,777.8,278.4,777.8,278.3,777.8z M230.7,748.7l47.5,27.8 + l71.2-41.1L302,707.6L230.7,748.7z"/> +<path class="st3" d="M277.9,833.5l-48.7-28.6c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-55.7h1.2v55.4l48.4,28.3 + L277.9,833.5z"/> +<path class="st3" d="M278.6,833.5l-0.6-1.1l72.1-41.7v-55.4h1.2V791c0,0.1,0,0.2-0.1,0.3s-0.1,0.2-0.2,0.2L278.6,833.5z"/> +<path class="st3" d="M278.9,777.2h-1.2v55.7h1.2V777.2z"/> +<path class="st2" d="M388.2,751.1c-0.1,0-0.2,0-0.3-0.1s-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-11.4c0-0.1,0-0.2,0.1-0.3 + s0.1-0.2,0.2-0.2l32.8-18.9c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1c0.1,0.1,0.2,0.1,0.2,0.2c0.1,0.1,0.1,0.2,0.1,0.3v11.3 + c0,0.1,0,0.2-0.1,0.3s-0.1,0.2-0.2,0.2l-32.8,19C388.5,751.1,388.4,751.1,388.2,751.1z M388.9,739.5v9.9l31.6-18.3v-9.9L388.9,739.5 + z"/> +<path class="st2" d="M809.1,240.4c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-3-3.1c-0.7-1.3-1.1-2.7-1.2-4.1c-0.1-0.8,0.1-1.6,0.5-2.2 + c0.5-0.6,1.1-1.1,1.9-1.2c0.4-0.1,0.8,0,1.2,0.1s0.8,0.3,1.1,0.6c1.2,0.8,2.2,1.8,3,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6,0,1.2-0.3,1.7s-0.6,1-1.1,1.3C810,240.3,809.6,240.4,809.1,240.4z M805.7,230.4c-0.2,0-0.5,0.1-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9s-0.2,0.7-0.2,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.3,0.6,0.5,1,0.5 + c0.4,0.1,0.8,0,1.1-0.2s0.6-0.5,0.7-0.9c0.1-0.4,0.2-0.8,0.1-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C806.7,230.6,806.2,230.4,805.7,230.4z"/> +<path class="st3" d="M823.4,248.7c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-2.9-3.1s-1.1-2.7-1.2-4.1c-0.1-0.6,0-1.2,0.2-1.7 + c0.3-0.5,0.7-1,1.2-1.3s1.1-0.4,1.7-0.4c0.6,0,1.2,0.3,1.6,0.7c1.2,0.8,2.2,1.8,3,3.1s1.1,2.7,1.2,4.1c0.1,0.6-0.1,1.2-0.3,1.7 + c-0.3,0.5-0.6,1-1.1,1.3C824.4,248.6,823.9,248.7,823.4,248.7z M820,238.8c-0.2,0-0.5,0.1-0.7,0.2c-0.3,0.2-0.5,0.5-0.7,0.9 + c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,1.2,0.4,2.4,1.1,3.5s1.5,2,2.5,2.6c0.3,0.2,0.6,0.4,1,0.4s0.7,0,1.1-0.1c0.3-0.2,0.5-0.5,0.7-0.9 + c0.1-0.3,0.2-0.7,0.1-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6C821,238.9,820.5,238.7,820,238.8L820,238.8z"/> +<path class="st3" d="M839.9,253.6c0,2.4-1.7,3.5-3.8,2.2c-1.1-0.7-2.1-1.7-2.7-2.9c-0.7-1.2-1.1-2.5-1.1-3.8c0-2.4,1.7-3.4,3.9-2.2 + c1.1,0.7,2.1,1.7,2.7,2.9C839.4,251,839.8,252.3,839.9,253.6z"/> +<path class="st2" d="M936.2,263.3H935v13.6h1.2V263.3z"/> +<path class="st2" d="M943.3,259.2h-1.2v13.6h1.2V259.2z"/> +<path class="st3" d="M1000.1,237.4L957.5,262v-6.3l42.6-24.6V237.4z"/> +<path class="st2" d="M809.1,272.6c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-3-3.1c-0.7-1.3-1.1-2.7-1.2-4.1c-0.1-0.6,0-1.2,0.2-1.7 + c0.3-0.5,0.7-1,1.2-1.3s1.1-0.4,1.7-0.4c0.6,0,1.2,0.3,1.6,0.7c1.2,0.8,2.2,1.8,3,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6,0,1.2-0.3,1.7s-0.6,1-1.1,1.3C810.1,272.5,809.6,272.6,809.1,272.6z M805.7,262.6c-0.2,0-0.5,0.1-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9s-0.2,0.7-0.2,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.2,0.6,0.4,1,0.4s0.7,0,1.1-0.1 + c0.3-0.2,0.5-0.5,0.7-0.9c0.1-0.3,0.2-0.7,0.2-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C806.7,262.8,806.2,262.6,805.7,262.6z"/> +<path class="st3" d="M825.6,277.5c0,2.4-1.7,3.5-3.9,2.2c-1.1-0.7-2-1.7-2.7-2.9c-0.7-1.2-1.1-2.5-1.1-3.8c0-2.4,1.7-3.5,3.8-2.2 + c1.1,0.7,2.1,1.7,2.7,2.9C825.1,274.8,825.5,276.1,825.6,277.5z"/> +<path class="st3" d="M837.8,289.1c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-3-3.1c-0.7-1.3-1.1-2.7-1.2-4.1c-0.1-0.6,0-1.2,0.2-1.7 + c0.3-0.5,0.7-1,1.2-1.3s1.1-0.4,1.7-0.4c0.6,0,1.2,0.3,1.6,0.7c1.2,0.8,2.2,1.8,2.9,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6,0,1.2-0.3,1.7s-0.6,1-1.1,1.3C838.7,289,838.2,289.1,837.8,289.1z M834.3,279.1c-0.2,0-0.5,0-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.2,0.6,0.4,1,0.4 + s0.7,0,1.1-0.1c0.3-0.2,0.5-0.5,0.7-0.9c0.1-0.3,0.2-0.7,0.2-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C835.3,279.3,834.8,279.2,834.3,279.1z"/> +<path class="st2" d="M936.2,296.7H935v12.4h1.2V296.7z"/> +<path class="st2" d="M943.3,292.6h-1.2V305h1.2V292.6z"/> +<path class="st2" d="M957.5,294.8c-0.1,0-0.2,0-0.3-0.1s-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-6.3c0-0.1,0-0.2,0.1-0.3 + c0.1-0.1,0.1-0.2,0.2-0.2l42.6-24.6c0.1,0,0.1-0.1,0.2-0.1c0.1,0,0.2,0,0.2,0c0.1,0,0.1,0.1,0.2,0.1c0.1,0,0.1,0.1,0.2,0.2 + c0.1,0.1,0.1,0.2,0.1,0.3v6.3c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2l-42.6,24.6C957.7,294.8,957.6,294.8,957.5,294.8z + M958.1,288.2v4.9l41.4-23.9v-4.9L958.1,288.2z"/> +<path class="st2" d="M809.1,305.3c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-3-3.1s-1.1-2.7-1.2-4.1c-0.1-0.8,0.1-1.6,0.5-2.2 + c0.5-0.6,1.1-1.1,1.9-1.2c0.4-0.1,0.8,0,1.2,0.1s0.8,0.3,1.1,0.6c1.2,0.8,2.2,1.8,3,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6-0.1,1.2-0.3,1.7c-0.3,0.5-0.6,1-1.1,1.3C810,305.1,809.6,305.2,809.1,305.3z M805.7,295.3c-0.2,0-0.5,0.1-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9c-0.1,0.3-0.2,0.7-0.2,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.2,0.6,0.4,1,0.4 + s0.7,0,1.1-0.1c0.3-0.2,0.5-0.5,0.7-0.9c0.1-0.3,0.2-0.7,0.2-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C806.7,295.5,806.2,295.3,805.7,295.3L805.7,295.3z"/> +<path class="st3" d="M825.6,310.1c0,2.4-1.7,3.5-3.9,2.2c-1.1-0.7-2-1.7-2.7-2.9c-0.7-1.2-1.1-2.5-1.1-3.8c0-2.4,1.7-3.4,3.8-2.2 + c1.1,0.7,2.1,1.7,2.7,2.9C825.1,307.5,825.5,308.8,825.6,310.1z"/> +<path class="st3" d="M837.8,321.8c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-3-3.1s-1.1-2.7-1.2-4.1c-0.1-0.6,0-1.2,0.2-1.7 + c0.3-0.5,0.7-1,1.2-1.3s1.1-0.4,1.7-0.4c0.6,0.1,1.2,0.3,1.6,0.7c1.2,0.8,2.2,1.8,2.9,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6,0,1.2-0.3,1.7s-0.6,1-1.1,1.3C838.7,321.7,838.2,321.8,837.8,321.8z M834.3,311.9c-0.2,0-0.5,0.1-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9c-0.1,0.3-0.2,0.7-0.2,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.3,0.8,0.5,1.2,0.5 + c0.5,0,0.9-0.2,1.2-0.5c0.2-0.2,0.4-0.5,0.4-0.8c0.1-0.3,0.1-0.6,0-0.9c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C835.3,312.1,834.8,311.9,834.3,311.9z"/> +<path class="st2" d="M936.2,329.5H935v12.4h1.2V329.5z"/> +<path class="st2" d="M943.3,325.4h-1.2v12.4h1.2V325.4z"/> +<path class="st3" d="M1000.1,302.3l-42.6,24.6v-6.3l42.6-24.6V302.3z"/> +<path class="st2" d="M809.1,338c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-3-3.1c-0.7-1.3-1.1-2.7-1.2-4.1c-0.1-0.8,0.1-1.6,0.5-2.2 + c0.5-0.6,1.1-1.1,1.9-1.2c0.4-0.1,0.8,0,1.2,0.1s0.8,0.3,1.1,0.6c1.2,0.8,2.2,1.8,3,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6-0.1,1.2-0.3,1.7c-0.3,0.5-0.6,1-1.1,1.3C810,337.9,809.6,338,809.1,338z M805.7,328.1c-0.2,0-0.5,0.1-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9s-0.2,0.7-0.2,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.2,0.6,0.4,1,0.4s0.7,0,1.1-0.1 + c0.3-0.2,0.5-0.5,0.7-0.9c0.1-0.3,0.2-0.7,0.2-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C806.7,328.2,806.2,328.1,805.7,328.1z"/> +<path class="st3" d="M823.4,346.2c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-2.9-3.1c-0.7-1.3-1.1-2.7-1.2-4.1c-0.1-0.6,0-1.2,0.2-1.7 + c0.3-0.5,0.7-1,1.2-1.3s1.1-0.4,1.7-0.4c0.6,0,1.2,0.3,1.6,0.6c1.2,0.8,2.2,1.8,3,3.1s1.1,2.7,1.2,4.1c0.1,0.6-0.1,1.2-0.3,1.7 + c-0.3,0.5-0.6,1-1.1,1.3C824.4,346.1,823.9,346.2,823.4,346.2z M820,336.3c-0.2,0-0.5,0.1-0.7,0.2c-0.3,0.2-0.5,0.5-0.7,0.9 + c-0.1,0.3-0.2,0.7-0.2,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.2,0.7,0.4,1,0.4c0.4,0,0.7,0,1.1-0.1 + c0.3-0.2,0.5-0.5,0.7-0.9c0.1-0.3,0.2-0.7,0.2-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C821,336.5,820.5,336.3,820,336.3z"/> +<path class="st3" d="M839.9,351.2c0,2.4-1.7,3.5-3.8,2.2c-1.1-0.7-2.1-1.7-2.7-2.9c-0.7-1.2-1.1-2.5-1.1-3.8c0-2.4,1.7-3.5,3.9-2.2 + c1.1,0.7,2,1.7,2.7,2.9C839.4,348.5,839.8,349.8,839.9,351.2z"/> +<path class="st2" d="M936.2,362.2H935v12.4h1.2V362.2z"/> +<path class="st2" d="M943.3,358.1h-1.2v12.4h1.2V358.1z"/> +<path class="st2" d="M957.5,360.2c-0.1,0-0.2,0-0.3-0.1s-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-6.3c0-0.1,0-0.2,0.1-0.3 + c0.1-0.1,0.1-0.2,0.2-0.2l42.6-24.6c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0.1c0.1,0,0.2,0.1,0.2,0.2s0.1,0.2,0.1,0.3v6.3 + c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2l-42.6,24.7C957.7,360.2,957.6,360.2,957.5,360.2z M958.1,353.6v4.9l41.4-23.9v-4.9 + L958.1,353.6z"/> +<path class="st3" d="M811.3,367.3c0,2.4-1.7,3.5-3.9,2.2c-1.1-0.7-2.1-1.7-2.7-2.9c-0.7-1.2-1.1-2.5-1.1-3.8c0-2.4,1.7-3.5,3.9-2.2 + c1.1,0.7,2.1,1.7,2.7,2.9C810.8,364.7,811.2,366,811.3,367.3z"/> +<path class="st3" d="M823.4,379c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-2.9-3.1c-0.7-1.3-1.1-2.7-1.2-4.1c-0.1-0.6,0-1.2,0.2-1.7 + c0.3-0.5,0.7-1,1.2-1.3s1.1-0.4,1.7-0.4c0.6,0,1.2,0.3,1.6,0.7c1.2,0.8,2.2,1.8,3,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6-0.1,1.2-0.3,1.7c-0.3,0.5-0.6,1-1.1,1.3C824.4,378.8,823.9,379,823.4,379z M820,369c-0.2,0-0.5,0.1-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.2,0.6,0.4,1,0.4 + s0.7,0,1.1-0.1c0.3-0.2,0.5-0.5,0.7-0.9c0.1-0.3,0.2-0.7,0.1-1.1c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C821,369.2,820.5,369,820,369z"/> +<path class="st2" d="M837.8,387.2c-0.7,0-1.4-0.2-2-0.6c-1.2-0.8-2.2-1.8-3-3.1c-0.7-1.3-1.1-2.7-1.2-4.1c-0.1-0.6,0-1.2,0.2-1.7 + c0.3-0.5,0.7-1,1.2-1.3s1.1-0.4,1.7-0.4c0.6,0,1.2,0.3,1.6,0.7c1.2,0.8,2.2,1.8,2.9,3.1c0.7,1.3,1.1,2.7,1.2,4.1 + c0.1,0.6,0,1.2-0.3,1.7s-0.6,1-1.1,1.3C838.7,387.1,838.2,387.2,837.8,387.2z M834.3,377.3c-0.2,0-0.5,0.1-0.7,0.2 + c-0.3,0.2-0.5,0.5-0.7,0.9s-0.2,0.7-0.2,1.1c0.1,1.2,0.4,2.4,1.1,3.5c0.6,1.1,1.5,2,2.5,2.6c0.3,0.3,0.8,0.5,1.2,0.5 + c0.5,0,0.9-0.2,1.2-0.5c0.2-0.2,0.4-0.5,0.4-0.8c0.1-0.3,0.1-0.6,0-0.9c-0.1-1.2-0.4-2.4-1.1-3.5c-0.6-1.1-1.5-2-2.5-2.6 + C835.3,377.5,834.8,377.3,834.3,377.3z"/> +<path class="st2" d="M936.2,394.9H935v12.4h1.2V394.9z"/> +<path class="st2" d="M943.3,390.8h-1.2v12.4h1.2V390.8z"/> +<path class="st3" d="M1000.1,367.7l-42.6,24.6V386l42.6-24.6V367.7z"/> +<path class="st2" d="M794.9,210.9l-0.6-1l109.4-63.2c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0.1l108.8,62.8l-0.6,1.1L904,148 + L794.9,210.9z"/> +<path class="st0" d="M532.1,248.8v-20.3l6.1,0.7l16.3,3.4l12.2,2l6.1,0.7l18.3-1.4l12.9-4.1l13.6-6.1l8.8-4.1v3.4l2,13.5l7.5,37.2 + l4.1,27.1l0.7,9.5v11.5l0.7,10.1l-0.7,7.4v14.9l0.7,15.6l0.7,10.1v4.7l0.7,7.4v5.4l-7.5,2.7l-7.5,3.4l-6.1,3.4h-8.8l-6.1-2 + l-3.4-14.2l-2-14.9l-1.4-16.9l-0.7-8.8l1.4-25.3v-11.9v-4.1l-2.7-8.1l-2-9.5l-3.4-12.2l-4.1-12.2l-0.7-6.8l-4.7,15.6l-2.7,10.1 + l-0.7,10.8l-3.4,12.2l-4.1,28.4l-2.7,15.6l-3.4,6.8v3.4l-0.7,8.8l-2,8.8l-0.7,12.9l-1.4,12.9l-0.7,14.9v4.1v11.5v4.7l-1.4,0.7h-4.7 + l-10.2,2l-6.1,1.4l-4.7-2l-8.1-4.1l-1.4-2v-6.8l-1.4-24.4v-27.7l2.7-14.9l3.4-12.2l3.4-9.5v-7.4V305v-18.3L532.1,248.8L532.1,248.8z + "/> +<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="537.2522" y1="541.7285" x2="642.6112" y2="541.7285" gradientTransform="matrix(1 0 0 -1 0 873)"> + <stop offset="0" style="stop-color:#D9D9D9"/> + <stop offset="0.9558" style="stop-color:#D9D9D9;stop-opacity:0"/> +</linearGradient> +<path class="st4" d="M641.5,374.5c0.2-3.2-0.7-19.8-0.6-23.1c0.6-13.2-0.7-28.5-0.1-36.6c0.3-11.6-2.8-32.3-6.5-49.9 + c-1.4-9.4-8-46.5-8-46.5S615,223.9,611,226c-5.8,3.1-8.1,4.2-14.2,6.1c-0.4,0.1-8.3,1.6-8.7,1.8c-2.4,0-7.5,2-16.4,0.8 + c-9-1.2-14.8,4.3-19.1,25.4l-4.6,76.4c-0.5,1.5-0.9,3-1.2,4.6c-1.8,8.3-3.1,16.7-3.6,25.2c-2.7,11.9-4.6,21.3-5.3,32.3 + c-0.4,5.9-0.6,13.2-0.5,19.1c0.2,10.7,4.7,27.4,8.4,26.4c4.8-1.3,10.3-1.6,13.8-1.6c2.8,0-0.4-1.7,1.5-13.3c0.5-2.8-1-4.7-0.7-7.8 + c0.6-6.1,1.4-14.8,1.6-20.3c1.1-15.4,1.9-22.7,4.6-38.4c-0.8-3.9,2.5-7.5,3.7-11.9c1.5-5.5,4.8-36.5,4.8-36.7 + c5.4-24.6,9.9-46.1,9.9-46.4c0.3-1.1,1.2-1,0.7-3.1c0.1-0.1,0.4-2.7,0.4-2.8c0.5-0.5,3.9-6.5,4.7-7.2c0,1.4,5.1,3.7,7.6,7.6 + c8.1,12.4,7.9,27.8,10.2,35.8c1.1,4.3,6.7,10.3,7,18.5c1.1,32.5,2.4,45.8,2.1,51.5c-1.9,20.3-13.6,12.5-16.9-0.1 + c-2-7.6,0.7,4.8,1.2,12.6c0.4,5.5,1.3,8.4,2.4,14.7c0,0.7,1.5,6.1,1.5,6.9c0.2,4.5,1.5,4.1,5.1,5.2c6.6,1.9,12.4,0.4,15.7-2.5 + c6.7-3.3,15.9-7.8,15.9-7.8S641.8,385.7,641.5,374.5z"/> +<path class="st3" d="M656.2,155.4c-1-2.8-2.3-5.4-3.9-7.9c-0.5-1.9-1.2-3.8-2.6-4.9c-1.7-6.3-5.9-11.2-9.7-16.4 + c-2.7-3.6-5.4-7.7-8.5-11.2c-0.9-1.3-1.6-2.7-2.3-4.1c-0.6-2.1-1.2-4.1-1.9-6.2c-0.4-2.9-0.6-5.9-1.5-8.6c-0.1-0.2-0.1-0.3-0.3-0.5 + c-0.1-0.1-0.3-0.2-0.4-0.3c-1.5-6.2-2.6-13.3-5.6-19.1c0.2-2.2-3.1-5.3-4.1-6.5c-0.8-1-2.1-3.1-3.4-3.4c-1.1-0.2-2.1,0.7-2.5,1.6 + c-0.2,0.6-0.3,1.3-0.3,2c-0.2,1.4-1,2.2-1.9,3.3c-1.5,1.7-2.9,3.5-4.4,5.2l-0.1,0.1c-0.5,0.2-1,0.5-1.4,0.9 + c-0.3,0.1-0.5,0.2-0.7,0.5c-0.2,0.2-0.2,0.5-0.2,0.8c-0.2,0.4-0.3,0.8-0.4,1.2c-0.6,3.1-1.7,5.6-4.1,7.8c-0.2,0.2-0.3,0.4-0.3,0.7 + c-0.2,0.1-0.4,0.3-0.5,0.6c-0.1,0.2-0.1,0.5-0.1,0.8c0.5,3.8,0.3,7.7-0.6,11.4c-0.4,0.4-0.7,0.3-1.2,0.5h-0.1l-3.4-2.8 + c-1.3-1.1-1.3-1-2.1-0.1c-0.7,0.3-1.8,1.2-2.5,1.6l-1.5,1c-0.3-0.1-0.9,0.4-1.2,0.5c-2.7,0.5-4.5,0.6-7.2,0.2 + c-2.9-0.2-2.8-1.1-4.7-2.2c-0.1-0.2-0.9-0.9-0.9-1.1c-0.1-1-1.5-0.9-2.3,0l-2.5,2.9c-0.8,1.2-1.8,2.2-2,4.1 + c-0.8,0.1-0.6-0.7-0.8-0.4c-2.4-2.4-3.7-4.3-4.5-7.6c-1.8-7.5-6.4-14.5-5.1-22.5c0-0.2,0-0.5-0.1-0.7c0.5-3.5,1.2-6.8,2-8.5 + c1.2-2.7-8.9,7.6-16.1,14.9c-0.6,0.2-1.5,1.7-2.4,3.1l-11.8,12.3c-1.4,0.8-2.5,1.9-3.2,3.4l-0.1,0.1c-5.1,7-8.4,15.2-9.8,23.7 + c-0.3,0.3-0.5,0.6-0.7,0.9c-0.2,0.4-0.3,0.7-0.3,1.1c-0.3,7.9-0.7,15.8-1,23.7c0,0.2,0,0.5,0,0.7c0,0.1,0,0.2,0,0.4 + c-0.2,6.6,0.3,13.1,1.5,19.6c0.1,0.4,0.2,0.7,0.4,1c0,0.1,0,0.2,0.1,0.4c0.8,3.6,1.5,7.2,2.3,10.8c0.1,0.4,0.2,0.7,0.4,1 + c0.6,4.2,1.4,8.3,4.1,11.7c2.4,2.9,5.7,4.6,9.2,5.7v0.9c-0.1,0.3-0.1,0.5-0.1,0.8l0.1,1.4c0,0.2-0.1,0.3-0.1,0.5 + c-0.5,5.6-1.7,11.6-0.8,17.2c0.4,2.5,1.8,4.9,4.3,5.8c2.3,0.9,5.1,0.7,7.6,0.7l4.5,1c0.2,0.3,0.5,0.5,0.8,0.6s0.6,0.2,1,0.2l2.4,0.2 + l0.3,0.1c5.8,1.2,11.6,2.1,17.5,2.9c0.2,0.1,0.3,0.1,0.5,0.1c7.6,1.3,15.4,1.4,23,0.2c0.1,0,0.2,0,0.3-0.1c4.9-0.3,9.7-1.6,14.1-3.7 + c0.7-0.1,1.4-0.4,2-0.8l0.1-0.1l12.9-6.1l0.1-0.1h0.3c0.4,0,0.8-0.2,1.2-0.4s0.6-0.5,0.8-0.9l0.2-0.1l3-1.5c0.2-0.1,0.4-0.3,0.5-0.5 + c-1.5,0.3-1.8,0.3-4,0.3c-0.1,0-1,0-3.9-0.3c-2-0.2-5.3,0.7-7.1-0.6c-0.5-0.3-1-0.5-1.6-0.6c-2-0.6-3.5-1.8-5.6-1.7 + c-4.7,0.2-7.5-2.1-11.3-2.9c-1.8-0.4-3.1-1.3-5.1-1.8c-1.3-0.3-3.6,0.8-4.5-0.5l-0.2-0.2c-0.4-0.5-0.7-1.2-0.8-1.9 + c-1.2-4.9,0-7.2,0.7-12.6c0.2-0.2,0.3-0.5,0.4-0.8c0.3-0.5,0.3-0.9,0.6-1.3c1-0.3,1.9-0.7,3-1c0.4-0.1,0.8-0.1,1.2-0.2 + c0.2,0.1,0.4,0.2,0.6,0.2c0.8,0.1,1.5,0.5,2.4,0.5c0.1,0,0.4,0,0.4,0c0.4,0.1,0.9,0,1.3-0.1c5-0.5,9.3-2.9,14.4-2.4h0.1l0.7,0.1 + c1.2,0.3,2.4,0.6,3.6,0.7c0.8,0.4,1.6,0.5,2.5,0.4l0,0c0.9,0.2,1.8,0.3,2.7,0.3c0.3,0,0.6-0.1,0.8-0.4c0.2-0.2,0.4-0.5,0.4-0.8 + l0.1-0.1c1.8-2.7,3.5-5.5,5-8.3c0.1-0.2,0.2-0.4,0.2-0.6s-0.1-0.4-0.2-0.6l0,0c-0.4-1.6-0.9-3.2-1.3-4.8c0.1-0.3,0.1-0.6,0-0.8 + c-0.3-0.8-0.5-1.6-0.8-2.4c-0.6-2.7-1-5.4-1.2-8.2c-0.2-1.7-0.4-3.5-0.6-5.2c0.1-0.1,0.2-0.3,0.2-0.5s0-0.4,0-0.5l-0.9-5 + c0-0.2-0.1-0.4-0.2-0.6c0.4-4.2,0.8-8.4,1.2-12.6c2,3.7,4.8,6.5,7,9.9c1.3,2.1,2.5,4.3,3.9,6.3c0.4,1,1.1,1.9,2,2.5 + c0,0.2-0.2,0.5,0,0.7c3.2,3.5,1.5,5.5,7.2,8.6c0.4,0.2,0.9,0.4,1.4,0.5l1.7,1.9c0.2,0.2,0.5,0.3,0.7,0.4s0.6,0,0.8-0.2l0.6-0.4 + c0.1,0,0.2-0.1,0.3-0.1c0.1-0.1,0.2-0.1,0.3-0.2l0.2-0.1l0.2-0.2c0.1-0.1,0.2-0.2,0.3-0.4c0.4-0.6,0.7-1.3,1-2.1l1.3-3.7l0.4-0.5 + c1.4-1.8,2.5-3.2,3.2-5c0.3-0.2,0.6-0.4,1-0.6c0.2,0.1,0.3,0.1,0.5,0.1l4.4,0.2l0.1,0.1c0.9,0.4,1.3-0.2,1.4-1.2 + c0-0.5,0.5-0.8,0.5-1.3C657.1,157.5,656.7,156.4,656.2,155.4z"/> +<path class="st5" d="M655.1,152.6c-1-1.6-2.2-3.9-3.1-5.5c-2.6-5.7-5.4-11.2-8.6-16.6c-0.7-1.2-3.9-5.5-4.8-6.5 + c-7.1-7.9-9.3-12.9-12.6-23.1c-0.1-5.1-1.4-8.2-2.9-13.2c-0.4-2.1-1.1-4.2-2.1-6.1c-0.3-0.6-0.6-2.9-1.1-3.4 + c0.2-2.8-7.8-12.4-7.8-12.4s-3,0.2-3.1,1.8c-0.5,0-1.2,3.5-1.5,3.9c-3.4,4.6-5.2,6.7-7.7,8.3c-0.7,0.5,0.5,1.9-0.5,4 + c-1.2,2.5-4.8,5.8-4.6,6.8c1.4,2.9-0.8,10.4-1.1,12.6c-1.4,0.1-5.3-3.5-5.3-3.5s-5.3,3.9-1.2,9.7c0.8,1.3,1.4,2.7,2,4.1 + c-0.2-0.5,0.4,1.1,0.5,1.2c0.3,0.8,0.6,1.6,0.9,2.4c1.1,3.2,2,6.4,2.8,9.6c1.6,6.3,2.6,12.7,3.6,19.1c2.1,14.2,6.5,47,6.5,47 + l11.3,1.7l5.4-10.2c-1.6-5.6-2.6-11.4-3.2-17.2c-1-9.6-0.9-21.7-1-30.3c0.9-0.7,2.8-4.4,3.3-5.3l0,0c0.1-0.2,0.1-0.2,1.8,0.7 + c5,2.8,9.5,6.4,13.5,10.5c2.6,4.4,0,10.3,4.1,14.7c3.4,3.7,7.6,3.3,6.8,8.7c2.4-3.8,3.3-4.8,6-6.1c1.9-0.9,5.2,2.1,5-0.4 + c0.1-0.4,0.1-0.8,0-1.2C656.2,157.3,656.4,154.7,655.1,152.6z"/> +<path class="st6" d="M572,480c-0.1-0.2-0.1-0.6-0.2-0.7c-0.5-3-1.2-4.3-2.4-6.7c0-0.1-6-12.7-9.7-16.2c-0.4-1.2-1.3-2.2-1.9-3.3 + c0-0.6-0.1-1.2-0.2-1.7c-0.2-2.4-2.3-7.8-2.2-9.2c0-0.9-3.7-0.3-6,0.4c-2.3,0.7-3.6,1.2-6.2,2.2l-3.5,0.6c-1-0.4-2-0.9-3-1.5 + c-2.3-1.3-4.1-1.8-6.8-3.4c-4.1-2.1-4-1.9-4.5,0.2c-0.8,4-0.3,13.1-0.3,13.1s4.5,6.4,5.3,10.1c-0.2,0.3-0.2,0.7-0.2,1 + c1.1,10.5,8.7,19.8,19.3,21.9c5.2,1,10.6,0.4,15.5-1.8c2-0.4,4-1.1,5.8-2.2C571.8,482.3,572.5,481,572,480z"/> +<path class="st6" d="M686,426.8c0-0.1,0-0.2,0-0.3c0.1-1.1,0-2.1-0.4-3.1c-0.3-1-0.9-1.9-1.6-2.7h-0.1l-0.1-0.1c0.1-0.3,0-0.5,0-0.8 + c-0.1-0.2-0.3-0.4-0.5-0.6c-1.5-1-3.1-1.9-4.7-2.8c-0.1-0.1-0.3-0.2-0.4-0.3c-1.4-0.8-2.8-1.4-4.4-1.9c-5.5-2.4-11-4.8-16.5-7.2 + l-2.6-1.6l-0.1-0.1l-8.9-6.5c-0.1-0.1-0.3-0.2-0.5-0.2c-0.3-0.2-0.6-0.4-0.9-0.5c-0.5-0.5-1.2-0.9-1.9-1.1c-1.9-0.4-4.4,1.4-6,2.2 + c-0.2,0.1-0.3,0.2-0.5,0.4c-0.8,0.2-1.6,0.5-2.4,0.8c-4,1.7-7.9,3.6-11.8,5.5c-0.4,0.2-0.7,0.4-1,0.7c-2.8,0.4-5.3,0.2-8.2,0.2 + c-0.1,0-5-1.2-5.1-1.2c-0.8,2.2-1.5,8.3,0.5,11c0.9,1.3,2.4,1.5,3.7,2.2c3.9,2.1,9.3,3.9,14,3.1c3.5,1.3,6.4,2.6,9.9,3.7 + c5.9,3.1,12.7,6.6,19.5,7.9c5.7,1.1,11.3,0.3,17-0.3c0.3,0,0.5-0.1,0.8-0.1h0.1c4.4-0.3,8.8-0.8,12-3.9c0.3,0,0.6,0,0.8-0.2 + s0.4-0.4,0.4-0.7c0-0.2,0.1-0.5,0.1-0.7C686.2,427.3,686.1,427,686,426.8z"/> +<path class="st7" d="M569.7,473.4c-1.5-4.1-3.5-8-5.8-11.7c-1.5-2.4-3.4-4.2-5-6.6c-0.7-1.9-1.1-4.3-1.8-6.1 + c-0.7-2.2-1.3-4.5-1.6-6.8c-1.4-0.2-2.9-0.2-4.4,0c-2.6,0.4-4.6,0.9-5.9,1.7c-5.4,3.3-11.3-1.7-11.2-0.2s9.6,4.7,10.8,8 + c2.9,8.1,11.8,13.9,13.3,16.6c8,14.2-0.9,19-0.9,19c1.9-0.3,3.7-0.7,5.5-1.2c3.2-1.2,6.4-2.6,9.4-4.3 + C572,480,571.1,477.5,569.7,473.4z"/> +<path class="st7" d="M682,417.9c-3.9-2.4-8.1-4.3-12.4-5.9l-0.8-0.3c-12.6-5.7-14.1-6.4-21.6-12.3c-0.1-0.4-1.6-0.7-1.8-1.1 + c-1.1-1.9-3.3-2.2-5.2-1.3l-11.2,5.1c-2.1,1.4-4.5,2.5-7.9,4.6c-0.6,0.4,3.7-0.2,10.4,1.2c7.9,1.8,9,5.6,16.7,8 + c0.8,0.3,6.7,1.1,8.5,1.5l14.2,4c4.3,1.2,5.9,3.6,7.5,6.3c2,3.2-1.1,4.9,0.9,4.6c1.8-0.3,4-1.2,4.8-1.9c0.1-0.1,2.1-1.6,2.1-1.7 + c0.2-0.5,0.2-2.1,0.1-2.7C686.1,422,684.4,419.9,682,417.9z"/> +<path class="st8" d="M612.7,194.5c-1.1-0.2-3.5-0.8-3.5-0.2c0,3.3-3.2,3.2-4.7,4.5c-1.6,1.4-0.1,2.2,0.2,2.4c0.2,0.1-0.1-0.8,0.4-1 + c1.2-0.4,1.4-0.4,2.8-0.8c0.7-0.1,1.4-0.3,2.1-0.6c0.2-0.1,1.1,0,1.3-0.1c0.2,0,0.3,0,0.5-0.1c0.1-0.1,0.3-0.2,0.4-0.3 + c0.6-0.9,1.2-1.9,1.7-2.9C614.4,194.6,613.8,194.5,612.7,194.5z"/> +<path class="st5" d="M585.2,193.1c-0.1-0.6-0.4-1.2-0.7-1.7c-0.5-0.6-1.1-1.3-1.7-1.8c-1.2-1.1-2.2-1.5-3.7-2.2 + c-9.9-4.2-17.3-8.8-25.5-13.5c-2.1-1.1-6.5-3.5-8.8-4.1c0.3-7.7,1.1-13.4,2.1-21.5c0.1-7.7-1.2-17.6-2.4-26.9 + c-0.2-2-0.9-0.9-1.7,1.2c-5.8,14.4-1.6,31.5-7.8,45.9c-0.5,1.1,2.9,1.5,3.6,2c-3.4,1.4-2.9,1.9-2.9,2c-0.2,0.3-0.2,0.7-0.2,1.1 + c0,0.4,0.1,0.7,0.3,1s0.5,0.6,0.8,0.8s0.7,0.3,1.1,0.3h0.1c8.4,2.4,17,4.7,25,8.4c3.7,1.8,7.4,3.9,10.9,6.2c1.9,1.2,3.1,4,5.3,4 + c3.7-0.1,0.8,3.7,0.8,3.7s3-3.2,4.1-3.1C585.4,194.9,585.3,195.3,585.2,193.1z"/> +<path class="st6" d="M671.3,179.7c1.2-2.1,0.9-1.7,0.6-4c-0.3-0.9-0.7-1.8-1.2-2.6c-0.6-2.1-0.5-2.1-1.5,0c-0.4,0.9-1.2,2.2-1.6,3.1 + c-0.6,1.1-1.5,2-2.4,2.7c4.4-0.3,0.3,5.5,0.3,5.5s3.1,3.7-1.9,5.5c2.4,0.2,2.5,1.3,0.6,2.4c-0.6,0.8,1.8-0.4,1.9-0.6 + c0.9-0.8,1.7-1.7,2.5-2.7c0.5-0.7,1.7-1.7,1.4-2.3c0.8-1.3,1.4-2.7,1.9-4.2c0.1-0.4,0.1-0.9,0-1.4 + C671.8,180.5,671.6,180,671.3,179.7z"/> +<path class="st6" d="M662.2,162.9c-2.4-0.7-9-4-12.5-2.2c-0.9,0.4-3,2.7-3.4,4c-0.1,0.4,0.8-1.3,2.9-1.2c5.8,0.2,3.4,3.9,6.3,2.9 + c1.2-0.4,3.1-0.8,3.6-1.6C660.7,165.1,662.6,163,662.2,162.9z"/> +<linearGradient id="SVGID_00000084528855015831264900000011375017582849876888_" gradientUnits="userSpaceOnUse" x1="579.9214" y1="792.3315" x2="609.1272" y2="792.3315" gradientTransform="matrix(1 0 0 -1 0 873)"> + <stop offset="0" style="stop-color:#D9D9D9"/> + <stop offset="0.9558" style="stop-color:#D9D9D9;stop-opacity:0"/> +</linearGradient> +<path style="fill:url(#SVGID_00000084528855015831264900000011375017582849876888_);" d="M597.3,70.2c-1.7-0.2-7.2-2.6-7.2-2.6 + s-3.5,1.2-7.9-2.7c-3.8-3.5-1.1,4.6,2,7.3c5.3,4.4,5.4,4.7,3.5,10c-1.9,5.2-8.3,6.3-7.7,10.3s2.1,4.8,5.1,4.8 + c4.4,0,14.6-12.4,14.6-12.4s0.2-2.4,0.4-4.4s1.6-0.4,3-1.7c1.4-1.4,2.7-3,3.8-4.7c1.3-2.2,2.1-4.6,2.3-7.1 + C609.1,66.9,600.3,70.5,597.3,70.2z"/> +<path class="st10" d="M566.1,58.2c0.9-0.4,0,0.3,0.4,0.7c0,0-0.2-1.5-0.2-1.6c-0.1-0.4-0.3-0.8-0.4-1.2c-0.3-1.6-2.3-4.7-4.6-3.5 + c-1.1,0.5-0.7,3.3-0.7,4.3c0,2.4,0.3,11.4,2.5,12.4"/> +<path class="st10" d="M565.2,72.2c0,7.1,1.3,12.4,8.3,19.1c1.2,1.1,4.1,3.1,5.4,4c1.5,1.1,3.5,2.3,5.3,2.3"/> +<path class="st0" d="M645.6,259.2c-0.1,0-0.3,0-0.4-0.1l-59.8-34.5c-0.1-0.1-0.2-0.2-0.3-0.3s-0.1-0.2-0.1-0.4v-77.5 + c0-0.1,0-0.3,0.1-0.4c0.1-0.1,0.2-0.2,0.3-0.3s0.2-0.1,0.4-0.1s0.3,0,0.4,0.1l59.8,34.5c0.1,0.1,0.2,0.2,0.3,0.3s0.1,0.2,0.1,0.4 + v77.5c0,0.1,0,0.3-0.1,0.4s-0.2,0.2-0.3,0.3C645.8,259.1,645.7,259.2,645.6,259.2z"/> +<path class="st2" d="M645.6,259.2c-0.1,0-0.3,0-0.4-0.1l-59.8-34.5c-0.1-0.1-0.2-0.2-0.3-0.3s-0.1-0.2-0.1-0.4v-77.5 + c0-0.1,0-0.3,0.1-0.4c0.1-0.1,0.2-0.2,0.3-0.3s0.2-0.1,0.4-0.1s0.3,0,0.4,0.1l59.8,34.5c0.1,0.1,0.2,0.2,0.3,0.3s0.1,0.2,0.1,0.4 + v77.5c0,0.1,0,0.3-0.1,0.4s-0.2,0.2-0.3,0.3C645.8,259.1,645.7,259.2,645.6,259.2z M586.5,223.5l58.3,33.6v-75.8l-58.3-33.6V223.5z" + /> +<path class="st10" d="M612.9,65.1c-0.9,0.7-3.6,1.6-3.7,2.7c-0.1,1.4,0,2.3-1.2,3.8c-2.7,3.3-4.1,6.9-7.1,7.6"/> +<path class="st10" d="M590.5,68.4L590.5,68.4"/> +<path class="st10" d="M538.3,172.1c2.2-1.1,5.2-1.2,7.6-1.2"/> +<path class="st10" d="M540.4,175c0.9-1.9,4.2-2.9,5.9-3.8"/> +<path class="st10" d="M518.9,198.2c0.7,1.9,9.3,4.6,11.5,5.4c1.9,0.7,3.6,2.3,5.5,3c7.9,2.9,16.8,4.5,25,6.5 + c3.7,0.9,12.3,0.6,15.2,2.4"/> +<path class="st0" d="M636.3,206.9c-5-1.9-5.9-1.2-8.4-1.2c-3.1,0-4.7,0-8.1,0c-0.3,0.1-2.4,0-2.8,0c-2.2-0.3-2.4-0.2-5-0.6 + c-2.4-0.4-4.8-1.4-6.8-2.8v-0.6c-0.1-0.6,4,1.4,8.1,1.9c1.8,0.2,7.3-0.5,9.3-1.5c1.3-0.6,0.6-3.5,0.6-3.5s-7.9,0.1-8.7,0 + c-1.1-0.1-9.4-2.4-10.6-2.5c-1.8-0.5-5.5-1.9-6.2-1.9c-3.4,0.4-2.9,0.3-8.7,1c-1.3-0.5-3.1-0.4-4.7-0.1c-4.4,0.7-6.1,8.4-5.3,14.5 + c0.1,1,0.7,3.4,1.6,3.5c3.2,0.3,7.3,1.9,10.5,2.7c0.2,0.1,2.2,0.4,2.4,0.5l2.7,0.6c0.3,0.2,0.7,0.4,1.1,0.6c0.4,0.1,0.7,0.2,1.1,0.3 + c0.3,0.3,0.6,0.4,1,0.5l2.5,0.4c0.1,0.1,0.2,0.1,0.3,0.1c1.9,0.8,2.9,0.8,5.1,1.4c0.5,0.2,2,1,2.5,0.9c2.2,0.3,2.9,0.3,4.4,0 + c1.2,0.3,7.8,0.2,10.3-0.6c1-0.3,5.2-1.7,6.2-2c0.1,0-0.1-1.1,0-1.1c0.7-0.1-1.6-0.4-0.9-0.6c1.4-0.5,6.6-1,7.1-2.2 + c0.5-1-0.5-0.8,0-1.6c1.2-0.3,2.8-1.2,2.8-1.2s0.4,0.2-0.9-1.9c-0.2-0.3-1.3-0.2-0.9-0.3C639.1,208.8,636.7,207.1,636.3,206.9z"/> +<path class="st10" d="M600.8,79.2c-1.2,0,0.2,2.3-1.4,5.6c-2.8,5.8-12.4,12.5-15,12.5"/> +<path class="st10" d="M566.2,54.1c2.5,1.2,3.2,3.9,2.6,6.7c-0.1,0.3-0.7,3.2-0.4,3.4s2.5-5,2.1-2.2c0,0.3,0,0.6,0,0.9"/> +<path class="st10" d="M560.9,65.1c-5,0-4.3-8.7-4.3-11.9c0-10.5,6.2-20.4,15.1-24.8"/> +<path class="st10" d="M569.9,27.7c1.1,1.1,3.3,0.7,4.8,0.7"/> +<path class="st10" d="M575.3,26.4c0.4,0-1.4-0.3-1.1,0c0.4,0.4,2.5,0.3,1.9,0.2C575.2,26.4,574.4,26.3,575.3,26.4z"/> +<path class="st10" d="M578.1,24.5c3.1,2.1,9.6,1.4,13.5,2.7c14.1,4.9,27.1,19.9,25.2,35"/> +<path class="st10" d="M572.6,61c0.2-0.4-0.5,0.8-0.8,1.1c-0.6,0.7,2.2-3,1.7-2.2C572.8,60.8,572.3,61.7,572.6,61z"/> +<path class="st10" d="M574.9,57.5c0,3.8,1.5,0.6,2.1-1.1c0.1-0.3,0.1-0.9,0.4-0.7c0.7,0.4,1.3,3.1,1.9,3.9"/> +<path class="st10" d="M579.2,59c-1.6,3.1,7.8,9.5,10.3,10.1"/> +<path class="st10" d="M589.6,68.9c0-0.3-0.2-1.9,0-2.1s0.2,0.5,0.4,0.7c0.2,0.3,0.4,0.6,0.7,0.8c0.3,0.2,0.6,0.4,1,0.5 + c2.3,0.6,4.2,1.3,6.7,1.3c3.5,0,14.4-3.1,18.1-7.9"/> +<path class="st10" d="M558.6,64.3c0,3.1-1.2,6.3-0.6,9.4c0.3,1.5,0.5,3.2,1,4.7c0.8,2.7,2.9,4.5,4.5,6.6c2.8,3.7,7,8.1,12,8.1"/> +<path class="st10" d="M557.6,64.6l-0.3,0.6"/> +<path class="st10" d="M557.9,64.9c-1.8,0.9-3.1,2.5-3.8,4.4"/> +<path class="st10" d="M554.4,68.8c-2.3,4.6-2.2,12.6-1.2,17.6c0.7,3.7,2.8,7.2,3.7,10.8c0.6,2.3,3.1,9.5,5.1,10.6"/> +<path class="st10" d="M562.5,108.1c0-2.8,5.1-7.6,7.2-9.2c0.6-0.5,1.8-0.3,2.2-0.9c0.9-1.2,1.7-3.9,3.2-4.7"/> +<path class="st10" d="M585.6,97.4c2.5,1.2,5.4,4.8,7.9,5.9c0.9,0.4,1.2-2.2,1.3-2.8c0.6-2.8,1.5-6.5,0.1-9.2"/> +<path class="st10" d="M555.1,66.8c-2.3,0.8-4.4,3.3-6,5l-7.8,7.8c-4.4,4.4-8.3,9.5-12.7,13.9c-2.8,2.8-6.4,5-8.5,8.5 + c-2.3,3.8-4.2,7.7-6.1,11.6"/> +<path class="st10" d="M514.5,112.9c-3.1,6.2-3.9,12.8-4.7,19.6c-0.4,3.3,0.2,6.8-0.1,10.1c-1.1,9.6,0.1,19.8,1.2,29.3 + c0.7,6.3,3.1,12.4,4.2,18.6c0.4,2,0.9,4.2,2.2,5.6c0.4,0.5,0.7,1,1.3,1.6c0.3,0.3,0.4,0.8,0.7,1"/> +<path class="st10" d="M612.5,65.9c2.3,3.6,6.1,6.1,7.3,10.6c1.3,4.7,3.1,9.3,4.4,14.1c1.6,6.3,2.9,12.6,4.9,18.9 + c1.1,3.4,3.6,6.5,5.5,9.5"/> +<path class="st10" d="M634.3,118.5c3.8,6,8.9,10.8,12.4,17c2.2,4,4.1,8,6,12.2c1.4,3,5.4,7.7,4,11.3"/> +<path class="st10" d="M615.1,97.7c1.1,10.4,1.4,20.9,1,31.4c-0.3,8,1.1,16.4,0,24.2c-0.3,2.1,0.5,4.9,0.8,7"/> +<path class="st10" d="M616.8,142.5c0.4,3.3,4.6,6.6,6.6,9.2c2.1,2.8,3.4,6,5.1,9c0.8,1.4,2.1,2.4,2.8,3.8c0.7,1.3,1.1,3.4,2.4,4.4 + c2.3,1.9,5.2,2.6,6.9,5.2"/> +<path class="st10" d="M569,100.3c2.8,5.5,16.7,4.8,19.3-0.5"/> +<path class="st10" d="M566.8,103c6.7,6.3,18.6,7,24-1.3"/> +<path class="st10" d="M545.3,121.8c0,5.5,0.8,10.9,1.6,16.4c1.5,10.6-1.6,21.3-1.6,31.6"/> +<path class="st10" d="M546.1,170.8c4.4,0.5,7.5,3.3,11.3,5.3c5.8,3,11.7,6.1,17.5,9.2c4.4,2.3,8.7,2.9,10.6,7.9"/> +<path class="st10" d="M656.5,159.5c-1.6-1-4.6-2.8-6.6-2.2c-3.8,1.1-8.8,12.4-9.2,16"/> +<path class="st10" d="M656.5,159.3c0.1,0.1,0.1,0.9,0.2,1"/> +<path class="st11" d="M585.5,194.7c-6.6,0-6.7,8.8-6.7,13.5c0,1.7,0.3,5.5,2.6,5.5"/> +<path class="st11" d="M585.3,194.7c1.6-4.1-6.5,0-7.3,0.8c-3.1,3.1-2.8,23.2,3.3,18.2"/> +<path class="st11" d="M578.6,214.3c-0.3,0.4-0.7,0.8-1.2,1s-1,0.3-1.6,0.3"/> +<path class="st11" d="M585.1,194.8c1.4,0.1,3.2,0.8,4.6,0.6c3.5-0.5,7.7-1.8,10.9-0.6c8,3,13,4.2,17.8,3.6c2.6-0.3,4.6-0.4,5.3,0.6 + c0.6,1-0.6,3.5-3.8,4.1c-1.4,0.3-2.8,0.5-4.2,0.6"/> +<path class="st11" d="M616.7,203.7c-7.1,0.5-9.8-1.6-10.8-1.9c-0.2-0.1-0.8-0.3-0.9-0.1c-0.1,0.2,0.4,0.8,1,1.2"/> +<path class="st11" d="M605.6,202.5c1.7,1.3,3.8,1.6,5.5,2.2c8.3,3,20.9-1.1,26.4,3"/> +<path class="st11" d="M581.2,213.8c2.4-0.3,5.9,0.5,8.2,1.3c2.2,0.8,3.8,1.8,6,2.5c3.3,1.1,6.7,1.2,9.6,1.9"/> +<path class="st11" d="M604.5,219.5c6.5,2.8,8.9,1.6,15.9,1.9c3.6,0.1,7-2,9-2.3c1.9-0.3,2.2-2-0.8-1.7"/> +<path class="st11" d="M530.5,229.1c1.8,0.9,4.5,0.2,6.4,0.6c4.7,0.9,9.4,2.3,14.3,3c5.8,0.7,11.6,2.2,17.4,3 + c12.8,1.6,22.4,0,31.7-3.2"/> +<path class="st11" d="M527,202.9c-0.1,0.7-0.1,1.4-0.1,2v6c0,5.9-3.1,14.8,3.7,18.2"/> +<path class="st11" d="M643.8,172.1c0-1,0.9-1.6,1.1-2.5c0.6-3,2.8-8.2,6.1-9.3c1.9-0.6,5.3,0.1,6.7,0.9"/> +<path class="st11" d="M657.5,161.2c1.5,0.8,3.7,1.2,5.2,2.1"/> +<path class="st11" d="M667.6,176.6c-1.6,3.2-7,5.3-9.9,7.1c-1,0.6-3.8,1.9-4.2,2.8"/> +<path class="st11" d="M670.3,172.2c1.7,1.2,2.5,3.6,2.1,5.8c-0.5,2.6-6.7,7-9.1,8c-2.4,1-5,2.6-7.5,3.2c-1.9,0.5-3.1-1.7-2.3-2.8"/> +<path class="st11" d="M671.6,179.8c0.9,1,0.5,2.5,0,3.8c-1.2,3.1-5.7,5.5-8.5,6.9c-2.6,1.3-12.7,4.5-8.9-1.5"/> +<path class="st11" d="M670.8,184.6c0.9,0.9-1.3,3.7-1.9,4.4c-1,1.1-2,2.1-3.1,3c-2.1,1.6-5.2,1.9-7.6,2.8c-1.6,0.6-4.5-0.2-3.1-2.4" + /> +<path class="st11" d="M650.5,169.1c-0.4-0.2-1.1-0.2-1.4-0.5"/> +<path class="st11" d="M637.5,207.7c0.2,0.1,0.3,0.3,0.5,0.5c0.1,0.2,0.2,0.4,0.2,0.7c0,0.2,0,0.5-0.1,0.7s-0.2,0.4-0.3,0.6l-6.2,0.2 + c-3.7,0.1-8.4,1.9-12.2,1.5c-1.7-0.2-3.4-0.2-5.1-0.5c-1.4-0.2-2.7-0.4-4.1-0.7"/> +<path class="st11" d="M638.2,210c0.7,0.2,2.2,0.8,1.1,2.2c-0.9,1.2-4.6,0.5-8.8,1.2c-4,0.8-5.1,1.1-8.1,1.4 + c-4.3,0.4-8.5,0.2-12.7-0.6"/> +<path class="st11" d="M634.2,213.1c5.1,0.2,3,2.7-1.6,3.1c-1.9,0.2-2.6,1.6-11,1.9c-3.1,0.1-11.1-0.3-12.6-0.8"/> +<path class="st11" d="M532.4,229.5c-0.3,2.2,0.8,4.8,0.6,7c-0.4,5.5-0.2,11.3-0.4,16.8c-0.5,10.6,0,21.1-0.5,31.6 + c-0.4,8.1,2.5,17.2,1.6,25.2c-0.2,2.3,0.3,4.6,0.1,6.9c-0.6,6.9-0.6,13.9,0,20.8"/> +<path class="st11" d="M592.8,252.8c-1.2,3.2-4.8,5.6-5.7,8.8c-1.4,4.9-2.4,9.8-3.5,14.8c-0.8,3.6-3,7.1-3.4,10.9 + c-0.3,3.1,0,6.1-0.6,9.2c-1.1,6.1-3,11.9-4.1,18.1c-1.4,7.6-1.8,15.2-2.6,22.7c-0.5,4.8-0.9,9.4-2.3,14.1c-0.8,2.7-2.6,4.7-3.5,7.4" + /> +<path class="st11" d="M533.6,335.3c0,4.1-1,8.2-2.7,11.9c-1.5,3-1.4,7-2.2,10.2c-1.1,4.5-3.5,8.9-4.1,13.7 + c-1.5,10.3-0.5,20.6-0.5,30.9c0,7.7,0.7,15.3,0.7,23.1c0,2.5,1.1,5.3,0.5,7.8c-0.2,1.3-0.4,2.6-0.5,3.9v-1.5"/> +<path class="st11" d="M567.2,358.2c-0.9,1.8-0.4,4.1-0.7,6c-0.6,3.8-0.8,8.1-1.7,11.9c-1.4,5.8-1.1,12-1.7,17.9 + c-0.9,8.8-1.2,17.7-2.1,26.5c-0.1,1.5,0.1,3,0.4,4.4c0.6,3.2,0,7.9-0.4,11c-0.2,1.2,0.5,3.8,0,4.9"/> +<path class="st11" d="M524.6,437.3c0.5-1.1,1.9,1.4,2.9,1.7c3.6,1,6.5,4.1,10.2,5.3c4.5,1.5,7.4-1.2,11.5-2c2.3-0.5,4.6-0.8,6.9-0.9 + c1.4,0,5,0.3,5-1.1"/> +<path class="st11" d="M587.7,261.2c0,4.5,2.4,9.2,3.4,13.5c1.8,7.3,4.6,14.7,5.8,22.2c1,5.7,4,10.5,4,16.4"/> +<path class="st11" d="M600.7,311.3c0,19.8-2.3,40.1,0.2,59.9c1,7.8,2.1,15.9,3.6,23.5c0.7,3.7,2,7.6,2,11.4"/> +<path class="st11" d="M630.7,249.8c2.6,15.5,6.4,33.8,9,49.2c1,5.9,0.5,12.2,0.5,18.3"/> +<path class="st11" d="M640.2,317c0,3.7-0.5,6.3,0,9.3c1.1,6.5,0.2,13.3,0.2,19.9c0,7.9,0.7,15.6,0.7,23.5c0,3.3-0.2,6.7,0.3,9.9 + c0.3,1.7-0.4,3.6,0,5.2c0.8,3.4,0.8,8.1,0.8,11.6"/> +<path class="st11" d="M606.4,405.1c3.3,1.6,10.3,2.7,13.9,2c2.3-0.5,4.2-1.9,6.2-2.9c5.4-2.7,11-5,16.3-7.7"/> +<path class="st11" d="M607.7,405.1c-1.2,2.4-1.8,11.1,1.5,12.2"/> +<path class="st11" d="M533.6,334.8c0,4.8,1.7,8.4,1.7,13.3"/> +<path class="st11" d="M525.8,438.3c0.1-0.1-0.7,3.8-0.9,4.8c-0.6,3.2,0,6.9,0,10.1"/> +<path class="st11" d="M524.9,451.2c0.3,0.6-0.3,1.6,0.3,2.2c1.2,1.2,4.8,6.5,4.8,8.2"/> +<path class="st11" d="M529.7,461c0.8,0.8,1.1,6.4,1.4,7.9c0.3,1.7,1.2,3.3,1.9,4.8c1,2.5,3.4,4.5,5,6.5c6.7,8.9,21.6,7.8,30.5,3.4 + c1.3-0.7,2.4-1.7,3.7-2.3"/> +<path class="st11" d="M555.3,441.4c0.3,0.2,0.5,1.9,0.6,2.4c1,2.7,1.7,5.5,2.1,8.4c0.5,3.4,3.9,6.3,5.7,9c2.9,4.3,5.1,8.9,6.6,13.9 + c0.6,2.2,2.6,4.9,1.3,6.8"/> +<path class="st11" d="M608.7,417c3.3,2.3,7.3,3.7,11.1,5c2.7,1,5.7-0.5,8.4,0.6c8,3.3,16,7.4,24.4,10.1c5.3,1.7,11.2,0.7,16.5,0.6 + c3.1,0,6.7,0.1,9.7-0.9"/> +<path class="st11" d="M642.5,396.6c0.2-0.3,0.2,0.4,0.8,0.6c1,0.4,1.9,1.2,2.9,1.7c4.2,2.1,7.5,6.4,11.9,8c4.1,1.5,8,4,12,5.7 + c4.7,1.9,9.2,3,13.2,6.3c1.7,1.4,4.9,8.5,2.6,10.2c-0.9,0.6-1.7,1.2-2.4,2c-0.6,0.7-5.6,1.4-6.7,2"/> +<path class="st0" d="M646.5,181.6c-0.1,0-0.3,0-0.4-0.1l-60-34.7c-0.2-0.1-0.3-0.2-0.4-0.4c-0.1-0.2,0-0.4,0.1-0.6 + c0.1-0.1,0.2-0.2,0.3-0.3l54.7-32c0.1-0.1,0.2-0.1,0.4-0.1c0.1,0,0.3,0,0.4,0.1l60,34.7c0.1,0.1,0.2,0.2,0.3,0.3s0.1,0.2,0.1,0.4 + c0,0.1,0,0.3-0.1,0.4c-0.1,0.1-0.2,0.2-0.3,0.3l-54.7,32C646.7,181.6,646.6,181.6,646.5,181.6z"/> +<path class="st0" d="M645.7,259.6c-0.1,0-0.3,0-0.4-0.1c-0.1-0.1-0.2-0.2-0.3-0.3c-0.1-0.1-0.1-0.2-0.1-0.4v-77.9 + c0-0.1,0-0.3,0.1-0.4c0.1-0.1,0.2-0.2,0.3-0.3l54.9-31.9c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0s0.2,0.1,0.3,0.1 + c0.1,0.1,0.1,0.1,0.2,0.2c0.1,0.1,0.1,0.2,0.1,0.4v77.9c0,0.1,0,0.3-0.1,0.4c-0.1,0.1-0.2,0.2-0.3,0.3l-55,32 + C645.9,259.5,645.8,259.6,645.7,259.6z"/> +<path class="st2" d="M645.7,259.6c-0.1,0-0.3,0-0.4-0.1c-0.1-0.1-0.2-0.2-0.3-0.3c-0.1-0.1-0.1-0.2-0.1-0.4v-77.9 + c0-0.1,0-0.3,0.1-0.4c0.1-0.1,0.2-0.2,0.3-0.3l54.9-31.9c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0s0.2,0.1,0.3,0.1 + c0.1,0.1,0.1,0.1,0.2,0.2c0.1,0.1,0.1,0.2,0.1,0.4v77.9c0,0.1,0,0.3-0.1,0.4c-0.1,0.1-0.2,0.2-0.3,0.3l-55,32 + C645.9,259.5,645.8,259.6,645.7,259.6z M646.4,181.3v76.1l53.4-31v-76.2L646.4,181.3z"/> +<path class="st2" d="M645.9,181.6c-0.1,0-0.3,0-0.4-0.1l-60-34.7c-0.2-0.1-0.3-0.2-0.4-0.4c-0.1-0.2,0-0.4,0.1-0.6 + c0.1-0.1,0.2-0.2,0.3-0.3l54.7-32c0.1-0.1,0.2-0.1,0.4-0.1c0.1,0,0.3,0,0.4,0.1l60,34.7c0.1,0.1,0.2,0.2,0.3,0.3s0.1,0.2,0.1,0.4 + c0,0.1,0,0.3-0.1,0.4c-0.1,0.1-0.2,0.2-0.3,0.3l-54.7,32C646.1,181.6,646,181.6,645.9,181.6z M587.3,146.1l58.5,33.8l53.4-31.1 + l-58.5-33.9L587.3,146.1z"/> +<path class="st3" d="M608,133.8l59.1,35.2l11.2-6.5l-59.1-35.2L608,133.8z"/> +<path class="st3" d="M666.8,168.4l0.9,24.2l11.2-6.5l-0.9-24L666.8,168.4z"/> +<path class="st2" d="M903.3,305.9c-0.1,0-0.2,0-0.3-0.1L794.3,243c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-32.1 + c0-0.1,0-0.2,0.1-0.3s0.1-0.2,0.2-0.2c0.1-0.1,0.2-0.1,0.3-0.1s0.2,0,0.3,0.1l108.7,62.8c0.1,0.1,0.2,0.1,0.2,0.2 + c0.1,0.1,0.1,0.2,0.1,0.3v32.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2C903.5,305.8,903.4,305.9,903.3,305.9z M795.2,242.2 + l107.5,62.1v-30.7l-107.5-62.1V242.2z"/> +<path class="st2" d="M903.6,305.8l-0.6-1.1l109.1-63V211l-108.5,62.6l-0.6-1.1l109.4-63.2c0.1,0,0.2-0.1,0.3-0.1s0.2,0,0.3,0.1 + s0.2,0.1,0.2,0.2c0.1,0.1,0.1,0.2,0.1,0.3v32.1c0,0.1,0,0.2-0.1,0.3s-0.1,0.2-0.2,0.2L903.6,305.8z"/> +<path class="st3" d="M913.1,276.7h-1.2v13.6h1.2V276.7z"/> +<path class="st3" d="M920.8,272.2h-1.2v13.6h1.2V272.2z"/> +<path class="st2" d="M928.5,267.8h-1.2v13.6h1.2V267.8z"/> +<path class="st2" d="M903.3,338.1c-0.1,0-0.2,0-0.3-0.1l-108.7-62.8c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-32.1h1.2 + v31.7l107.5,62.1v-31.1h1.2v32.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2C903.5,338.1,903.4,338.1,903.3,338.1z"/> +<path class="st2" d="M903.6,337.9l-0.6-1.1l109.1-63V242h1.2v32.2c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2L903.6,337.9z"/> +<path class="st3" d="M913.1,310.1h-1.2v12.4h1.2V310.1z"/> +<path class="st2" d="M920.8,305.6h-1.2V318h1.2V305.6z"/> +<path class="st2" d="M928.5,301.2h-1.2v12.4h1.2V301.2z"/> +<path class="st2" d="M903.3,370.8c-0.1,0-0.2,0-0.3-0.1l-108.7-62.8c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-32.1h1.2 + v31.8l107.5,62.1V338h1.2v32.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2C903.5,370.8,903.4,370.8,903.3,370.8z"/> +<path class="st2" d="M903.6,370.7l-0.6-1.1l109.1-63v-31.8h1.2V307c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2L903.6,370.7z"/> +<path class="st3" d="M913.1,342.8h-1.2v12.4h1.2V342.8z"/> +<path class="st3" d="M920.8,338.4h-1.2v12.4h1.2V338.4z"/> +<path class="st3" d="M928.5,333.9h-1.2v12.4h1.2V333.9z"/> +<path class="st2" d="M903.3,403.5c-0.1,0-0.2,0-0.3-0.1l-108.7-62.8c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3V308h1.2 + v31.8l107.5,62.1v-31.1h1.2v32.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2C903.5,403.5,903.4,403.5,903.3,403.5z"/> +<path class="st2" d="M903.6,403.4l-0.6-1.1l109.1-63v-31.7h1.2v32.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2L903.6,403.4z"/> +<path class="st3" d="M913.1,375.5h-1.2v12.4h1.2V375.5z"/> +<path class="st2" d="M920.8,371.1h-1.2v12.4h1.2V371.1z"/> +<path class="st2" d="M928.5,366.6h-1.2V379h1.2V366.6z"/> +<path class="st2" d="M903.3,436.2c-0.1,0-0.2,0-0.3-0.1l-108.7-62.8c-0.1-0.1-0.2-0.1-0.2-0.2c-0.1-0.1-0.1-0.2-0.1-0.3v-32.2h1.2 + v31.8l107.5,62.1v-31.1h1.2v32.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2C903.5,436.2,903.4,436.2,903.3,436.2z"/> +<path class="st2" d="M903.6,436.1L903,435l109.1-63v-31.7h1.2v32.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.2-0.2,0.2L903.6,436.1z"/> +<path class="st3" d="M913.1,408.2h-1.2v12.4h1.2V408.2z"/> +<path class="st2" d="M920.8,403.8h-1.2v12.4h1.2V403.8z"/> +<path class="st2" d="M928.5,399.3h-1.2v12.4h1.2V399.3z"/> +</svg> diff --git a/api/service-worker/clients.js b/api/service-worker/clients.js new file mode 100644 index 0000000000..a5e60ab5cc --- /dev/null +++ b/api/service-worker/clients.js @@ -0,0 +1,242 @@ +import application from '../application.js' +import state from './state.js' + +const MAX_WINDOWS = 32 +const CLIENT_GET_TIMEOUT = 100 +const CLIENT_MATCH_ALL_TIMEOUT = 50 + +function normalizeURL (url) { + if (typeof url !== 'string') { + url = url.toString() + } + + if (!URL.canParse(url) && !url.startsWith('/') && !url.startsWith('.')) { + url = `./${url}` + } + + return new URL(url, globalThis.location.origin) +} + +function getOrigin () { + if (globalThis.location.origin.startsWith('blob:')) { + return new URL(globalThis.location.origin).pathname + } + + return globalThis.location.origin +} + +export class Client { + #id = null + #url = null + #type = null + #frameType = null + + constructor (options) { + this.#id = options?.id ?? null + this.#url = options?.url ?? null + this.#type = options?.type ?? null + this.#frameType = options?.frameType ?? null + } + + get id () { + return this.#id + } + + get url () { + return this.#url + } + + get type () { + return this.#type ?? 'none' + } + + get frameType () { + return this.#frameType ?? 'none' + } + + postMessage (message, optionsOrTransferables = null) { + globalThis.postMessage({ + from: 'serviceWorker', + registration: { id: state.id }, + client: { + id: this.#id, + type: this.#type, + frameType: this.#frameType + }, + message + }, optionsOrTransferables) + } +} + +export class WindowClient extends Client { + #url = null + #window = null + #focused = false + #ancestorOrigins = [] + #visibilityState = 'prerender' + + constructor (options) { + super({ + ...options, + id: options?.window?.id + }) + + this.#url = options?.url ?? null + this.#window = options?.window ?? null + + state.channel.addEventListener('message', (event) => { + if (event.data?.client?.id === this.id) { + if ('focused' in event.data.client) { + this.#focused = Boolean(event.data.client.focused) + } + + if ('visibilityState' in event.data.client) { + this.#visibilityState = event.data.client.visibilityState + } + } + }) + } + + get url () { + return this.#url + } + + get focused () { + return this.#focused + } + + get ancestorOrigins () { + return this.#ancestorOrigins + } + + get visibilityState () { + return this.#visibilityState + } + + async focus () { + state.channel.postMessage({ + client: { + id: this.id, + focus: true + } + }) + + return this + } + + async navigate (url) { + const origin = getOrigin() + + url = normalizeURL(url) + + if (!url.toString().startsWith(origin)) { + throw new TypeError('WindowClient cannot navigate outside of origin') + } + + await this.#window.navigate(url.pathname + url.search) + this.#url = url.pathname + url.search + return this + } +} + +export class Clients { + async get (id) { + state.channel.postMessage({ + clients: { + get: { id } + } + }) + + const result = await new Promise((resolve) => { + const timeout = setTimeout(onTimeout, CLIENT_GET_TIMEOUT) + + state.channel.addEventListener('message', onMessage) + + function onMessage (event) { + if (event.data?.clients?.get?.result?.client?.id === id) { + clearTimeout(timeout) + state.channel.removeEventListener('message', onMessage) + resolve(event.data.clients.get.result.client) + } + } + + function onTimeout () { + state.channel.removeEventListener('message', onMessage) + resolve(null) + } + }) + + if (result) { + return new Client(result) + } + } + + async matchAll (options = null) { + state.channel.postMessage({ + clients: { + matchAll: options ?? {} + } + }) + + const results = await new Promise((resolve) => { + setTimeout(onTimeout, CLIENT_MATCH_ALL_TIMEOUT) + const clients = [] + + state.channel.addEventListener('message', onMessage) + + function onMessage (event) { + if (event.data?.clients?.matchAll?.result?.client) { + const { client } = event.data.client.matchAll.result + if (!options?.type || options.type === 'all') { + clients.push(client) + } else if (options.type === client.type) { + clients.push(client) + } + } + } + + function onTimeout () { + state.channel.removeEventListener('message', onMessage) + resolve(clients) + } + }) + + return results.map((result) => new Client(result)) + } + + async openWindow (url, options = null) { + const windows = await application.getWindows() + const indices = Object.keys(windows) + .map((key) => parseInt(key)) + .filter((index) => !Number.isNaN(index) && index < MAX_WINDOWS) + .sort() + + const index = indices.pop() + 1 + + if (index < MAX_WINDOWS) { + throw new DOMException('Max windows are opened', 'InvalidAccessError') + } + + const window = await application.createWindow({ ...options, index, path: url }) + + return new WindowClient({ + frameType: 'top-level', + type: 'window', + window, + url + }) + } + + async claim () { + state.channel.postMessage({ + clients: { + claim: { + scope: state.serviceWorker.scope, + scriptURL: state.serviceWorker.scriptURL + } + } + }) + } +} + +export default new Clients() diff --git a/api/service-worker/container.js b/api/service-worker/container.js new file mode 100644 index 0000000000..07cdf2fb84 --- /dev/null +++ b/api/service-worker/container.js @@ -0,0 +1,529 @@ +/* global EventTarget */ +import { ServiceWorkerRegistration } from './registration.js' +import { createServiceWorker, SHARED_WORKER_URL } from './instance.js' +import { SharedWorker } from '../shared-worker/index.js' +import { Deferred } from '../async.js' +import application from '../application.js' +import location from '../location.js' +import state from './state.js' +import ipc from '../ipc.js' + +const SERVICE_WORKER_WINDOW_PATH = `${location.origin}/socket/service-worker/index.html` + +class ServiceWorkerContainerInternalStateMap extends Map { + define (container, property, descriptor) { + Object.defineProperty(container, property, { + configurable: false, + enumerable: true, + ...descriptor + }) + } +} + +class ServiceWorkerContainerInternalState { + currentWindow = null + controller = null + sharedWorker = null + channel = new BroadcastChannel('socket.runtime.ServiceWorkerContainer') + ready = new Deferred() + init = new Deferred() + + isRegistering = false + isRegistered = false + + // level 1 events + oncontrollerchange = null + onmessageerror = null + onmessage = null + onerror = null +} + +class ServiceWorkerContainerRealm { + static instance = null + + static async init (container) { + const realm = new ServiceWorkerContainerRealm() + return await realm.init(container) + } + + frame = null + async init () { + if (ServiceWorkerContainerRealm.instance) { + return + } + + ServiceWorkerContainerRealm.instance = this + + if (!globalThis.top || !globalThis.top.document) { + return + } + + const frameId = '__service-worker-frame__' + const existingFrame = globalThis.top.document.querySelector( + `iframe[id="${frameId}"]` + ) + + if (existingFrame) { + this.frame = existingFrame + return + } + + const pending = [] + const target = ( + globalThis.top.document.head ?? + globalThis.top.document.body ?? + globalThis.top.document + ) + + pending.push(new Promise((resolve) => { + globalThis.top.addEventListener('message', function onMessage (event) { + if (event.data.__service_worker_frame_init === true) { + globalThis.top.removeEventListener('message', onMessage) + resolve() + } + }) + })) + + this.frame = globalThis.top.document.createElement('iframe') + this.frame.id = frameId + this.frame.src = SERVICE_WORKER_WINDOW_PATH + this.frame.setAttribute('loading', 'eager') + this.frame.setAttribute('sandbox', 'allow-same-origin allow-scripts') + + Object.assign(this.frame.style, { + display: 'none', + height: 0, + width: 0 + }) + + target.prepend(this.frame) + + await Promise.all(pending) + } +} + +const internal = new ServiceWorkerContainerInternalStateMap() + +async function preloadExistingRegistration (container) { + const registration = await container.getRegistration() + if (registration) { + if (registration.active) { + if ( + application.config.webview_service_worker_mode === 'hybrid' + ) { + if ( + !internal.get(container).isRegistered && + !internal.get(container).isRegistering + ) { + container.register(registration.active.scriptURL, { scope: registration.scope }) + } + } else { + queueMicrotask(() => { + container.dispatchEvent(new Event('controllerchange')) + }) + + queueMicrotask(() => { + internal.get(container).ready.resolve(registration) + }) + } + } else { + const serviceWorker = registration.installing || registration.waiting + serviceWorker.addEventListener('statechange', function onStateChange (event) { + if ( + serviceWorker.state === 'activating' || + serviceWorker.state === 'activated' + ) { + serviceWorker.removeEventListener('statechange', onStateChange) + + queueMicrotask(() => { + container.dispatchEvent(new Event('controllerchange')) + }) + + queueMicrotask(() => { + internal.get(container).ready.resolve(registration) + }) + } + }) + } + } +} + +async function activateRegistrationFromClaim (container, claim) { + await container.register(claim.scriptURL) + await preloadExistingRegistration(container) +} + +/** + * Predicate to determine if service workers are allowed + * @return {boolean} + */ +export function isServiceWorkerAllowed () { + const { config } = application + + if ( + globalThis.__RUNTIME_SERVICE_WORKER_CONTEXT__ || + globalThis.location.pathname === '/socket/service-worker/index.html' + ) { + return false + } + + return String(config.permissions_allow_service_worker) !== 'false' +} + +/** + * A `ServiceWorkerContainer` implementation that is attached to the global + * `globalThis.navigator.serviceWorker` object. + */ +export class ServiceWorkerContainer extends EventTarget { + get ready () { + return internal.get(this).ready.promise + } + + get controller () { + return internal.get(this).controller + } + + /** + * A special initialization function for augmenting the global + * `globalThis.navigator.serviceWorker` platform `ServiceWorkerContainer` + * instance. + * + * All functions MUST be sure to what a lexically bound `this` becomes as the + * target could change with respect to the `internal` `Map` instance which + * contains private implementation properties relevant to the runtime + * `ServiceWorkerContainer` internal state implementations. + * @ignore + */ + async init () { + if (internal.get(this)) { + return internal.get(this).init + } + + internal.set(this, new ServiceWorkerContainerInternalState()) + + this.register = this.register.bind(this) + this.getRegistration = this.getRegistration.bind(this) + this.getRegistrations = this.getRegistrations.bind(this) + + internal.define(this, 'controller', { + get () { + return internal.get(this).controller + } + }) + + internal.define(this, 'oncontrollerchange', { + get () { + return internal.get(this).oncontrollerchange + }, + + set (oncontrollerchange) { + if (internal.get(this).oncontrollerchange) { + this.removeEventListener('controllerchange', internal.get(this).oncontrollerchange) + } + + internal.get(this).oncontrollerchange = null + + if (typeof oncontrollerchange === 'function') { + this.addEventListener('controllerchange', oncontrollerchange) + internal.get(this).oncontrollerchange = oncontrollerchange + } + } + }) + + internal.define(this, 'onmessageerror', { + get () { + return internal.get(this).onmessageerror + }, + + set (onmessageerror) { + if (internal.get(this).onmessageerror) { + this.removeEventListener('messageerror', internal.get(this).onmessageerror) + } + + internal.get(this).onmessageerror = null + + if (typeof onmessageerror === 'function') { + this.addEventListener('messageerror', onmessageerror) + internal.get(this).onmessageerror = onmessageerror + } + } + }) + + internal.define(this, 'onmessage', { + get () { + return internal.get(this).onmessage + }, + + set (onmessage) { + if (internal.get(this).onmessage) { + this.removeEventListener('message', internal.get(this).onmessage) + } + + internal.get(this).onmessage = null + + if (typeof onmessage === 'function') { + this.addEventListener('message', onmessage) + internal.get(this).onmessage = onmessage + this.startMessages() + } + } + }) + + internal.define(this, 'onerror', { + get () { + return internal.get(this).onerror + }, + + set (onerror) { + if (internal.get(this).onerror) { + this.removeEventListener('error', internal.get(this).onerror) + } + + internal.get(this).onerror = null + + if (typeof onerror === 'function') { + this.addEventListener('error', onerror) + internal.get(this).onerror = onerror + } + } + }) + + internal.get(this).ready.then(async (registration) => { + if (registration) { + internal.get(this).controller = registration.active + internal.get(this).currentWindow = await application.getCurrentWindow() + } + }) + + if (!globalThis.isServiceWorkerScope && isServiceWorkerAllowed()) { + state.channel.addEventListener('message', (event) => { + if (event.data?.clients?.claim?.scope) { + const { scope } = event.data.clients.claim + if (globalThis.location.pathname.startsWith(scope)) { + activateRegistrationFromClaim(this, event.data.clients.claim) + } + } + }) + + if ( + String(application.config.webview_service_worker_frame) !== 'false' && + application.config.webview_service_worker_mode === 'hybrid' + ) { + await ServiceWorkerContainerRealm.init(this) + } + + await preloadExistingRegistration(this) + } + + setTimeout(() => internal.get(this).init.resolve(), 250) + } + + async getRegistration (clientURL) { + if (globalThis.top && globalThis.window && globalThis.top !== globalThis.window) { + return await globalThis.top.navigator.serviceWorker.getRegistration(clientURL) + } + + let scope = clientURL + let currentScope = null + + // @ts-ignore + if (scope && URL.canParse(scope, globalThis.location.href)) { + scope = new URL(scope, globalThis.location.href).pathname + } + + if (globalThis.isWorkerScope) { + if (globalThis.RUNTIME_WORKER_LOCATION.startsWith('blob:')) { + currentScope = new URL('.', new URL(globalThis.RUNTIME_WORKER_LOCATION).pathname).pathname + } else { + currentScope = new URL('.', globalThis.RUNTIME_WORKER_LOCATION).pathname + } + } else if (globalThis.location.protocol === 'blob:') { + currentScope = new URL('.', globalThis.location.pathname).pathname + } else { + currentScope = globalThis.location.pathname + } + + if (!scope) { + scope = currentScope + } + + const result = await ipc.request('serviceWorker.getRegistration', { scope }) + + if (result.err) { + throw result.err + } + + const info = result.data + + if (!info?.registration?.state || !info?.registration?.id) { + return + } + + if (scope === currentScope) { + state.serviceWorker.state = info.registration.state.replace('registered', 'installing') + state.serviceWorker.scope = scope + state.serviceWorker.scriptURL = info.registration.scriptURL + } + + const serviceWorker = createServiceWorker(state.serviceWorker.state, { + subscribe: clientURL || scope === currentScope, + scriptURL: info.registration.scriptURL, + id: info.registration.id + }) + + return new ServiceWorkerRegistration(info, serviceWorker) + } + + async getRegistrations () { + if (globalThis.top && globalThis.window && globalThis.top !== globalThis.window) { + return await globalThis.top.navigator.serviceWorker.getRegistrations() + } + + const result = await ipc.request('serviceWorker.getRegistrations') + + if (result.err) { + throw result.err + } + + const registrations = [] + + if (Array.isArray(result.data)) { + for (const registration of result.data) { + const info = { registration } + info.registration.state = info.registration.state.replace('registered', 'installing') + const serviceWorker = createServiceWorker(registration.state, { + scriptURL: info.registration.scriptURL, + id: info.registration.id + }) + registrations.push(new ServiceWorkerRegistration(info, serviceWorker)) + } + } + + return registrations + } + + async register (scriptURL, options = null) { + await internal.get(this).init + + if (globalThis.top && globalThis.window && globalThis.top !== globalThis.window) { + return await globalThis.top.navigator.serviceWorker.register(scriptURL, options) + } + + scriptURL = new URL(scriptURL, globalThis.location.href).toString() + + if (!options || typeof options !== 'object') { + options = {} + } + + if (!options.scope || typeof options.scope !== 'string') { + options.scope = new URL('./', scriptURL).pathname + } + + internal.get(this).isRegistered = false + internal.get(this).isRegistering = true + + const result = await ipc.request('serviceWorker.register', { + ...options, + scriptURL + }) + + internal.get(this).isRegistering = false + + if (result.err) { + throw result.err + } + + internal.get(this).isRegistered = true + + const info = result.data + + if (!info?.registration) { + return // registration likely never completed + } + + const url = 'blob:'.startsWith(globalThis.location.origin) + ? new URL(info.registration.scope, new URL(globalThis.location.origin).pathname) + : new URL(info.registration.scope, globalThis.location.origin) + + const container = this + + if (info?.registration && url.pathname.startsWith(options.scope)) { + state.serviceWorker.state = info.registration.state.replace('registered', 'installing') + state.serviceWorker.scriptURL = info.registration.scriptURL + + const serviceWorker = createServiceWorker(state.serviceWorker.state, { + scriptURL: info.registration.scriptURL, + id: info.registration.id + }) + + const registration = new ServiceWorkerRegistration(info, serviceWorker) + serviceWorker.addEventListener('statechange', function onStateChange (event) { + if ( + serviceWorker.state === 'activating' || + serviceWorker.state === 'activated' + ) { + serviceWorker.removeEventListener('statechange', onStateChange) + + queueMicrotask(() => { + container.dispatchEvent(new Event('controllerchange')) + }) + + queueMicrotask(() => { + internal.get(container).ready.resolve(registration) + }) + } + }) + + return registration + } + } + + startMessages () { + if (globalThis.top && globalThis.window && globalThis.top !== globalThis.window) { + return globalThis.top.navigator.serviceWorker.startMessages() + } + + if (!internal.get(this).sharedWorker && globalThis.RUNTIME_WORKER_LOCATION !== SHARED_WORKER_URL) { + internal.get(this).sharedWorker = new SharedWorker(SHARED_WORKER_URL) + internal.get(this).sharedWorker.port.start() + internal.get(this).sharedWorker.port.onmessage = async (event) => { + if ( + event.data?.from === 'realm' && + event.data?.registration?.id && + event.data?.client?.id === globalThis.__args.client.id && + event.data?.client?.type === globalThis.__args.client.type && + event.data?.client?.frameType === globalThis.__args.client.frameType + ) { + const registrations = await this.getRegistrations() + for (const registration of registrations) { + const info = registration[Symbol.for('socket.runtime.ServiceWorkerRegistration.info')] + if (info?.id === event.data.registration.id) { + const serviceWorker = createServiceWorker(state.serviceWorker.state, { + subscribe: false, + scriptURL: info.scriptURL, + id: info.id + }) + + const messageEvent = new MessageEvent('message', { + origin: new URL(info.scriptURL, location.origin).origin, + data: event.data.message + }) + + Object.defineProperty(messageEvent, 'source', { + configurable: false, + enumerable: false, + writable: false, + value: serviceWorker + }) + + this.dispatchEvent(messageEvent) + break + } + } + } + } + } + } +} + +export default ServiceWorkerContainer diff --git a/api/service-worker/context.js b/api/service-worker/context.js new file mode 100644 index 0000000000..88bf2694ff --- /dev/null +++ b/api/service-worker/context.js @@ -0,0 +1,91 @@ +import { Environment } from './env.js' +import clients from './clients.js' + +/** + * A context given to `ExtendableEvent` interfaces and provided to + * simplified service worker modules + */ +export class Context { + #event = null + + /** + * Context data. This may be a custom protocol handler scheme data + * by default, if available. + * @type {any?} + */ + data = null + + /** + * `Context` class constructor. + * @param {import('./events.js').ExtendableEvent} event + */ + constructor (event) { + this.#event = event + } + + /** + * The `ExtendableEvent` for this `Context` instance. + * @type {ExtendableEvent} + */ + get event () { + return this.#event + } + + /** + * An environment context object. + * @type {object?} + */ + get env () { + return Environment.instance?.context ?? null + } + + /** + * Resets the current environment context. + * @return {Promise<boolean>} + */ + async resetEnvironment () { + if (Environment.instance) { + return await Environment.instance.reset() + } + + return false + } + + /** + * Unused, but exists for cloudflare compat. + * @ignore + */ + passThroughOnException () {} + + /** + * Tells the event dispatcher that work is ongoing. + * It can also be used to detect whether that work was successful. + * @param {Promise} promise + */ + async waitUntil (promise) { + return await this.event.waitUntil(promise) + } + + /** + * TODO + */ + async handled () { + return await this.event.handled + } + + /** + * Gets the client for this event context. + * @return {Promise<import('./clients.js').Client>} + */ + async client () { + if (this.event.clientId) { + return await clients.get(this.event.clientId) + } + + return null + } +} + +export default { + Context +} diff --git a/api/service-worker/debug.js b/api/service-worker/debug.js new file mode 100644 index 0000000000..db7a862394 --- /dev/null +++ b/api/service-worker/debug.js @@ -0,0 +1,28 @@ +import globals from '../internal/globals.js' +import util from '../util.js' + +export function debug (...args) { + const state = globals.get('ServiceWorker.state') + + if (process.env.SOCKET_RUNTIME_SERVICE_WORKER_DEBUG) { + console.debug(...args) + } + + if (args[0] instanceof Error) { + globalThis.postMessage({ + __service_worker_debug: [ + `[${state.serviceWorker.scriptURL}]: ${util.format(...args)}` + ] + }) + + if (typeof state?.reportError === 'function') { + state.reportError(args[0]) + } else if (typeof globalThis.reportError === 'function') { + globalThis.reportError(args[0]) + } + } else { + globalThis.postMessage({ __service_worker_debug: [util.format(...args)] }) + } +} + +export default debug diff --git a/api/service-worker/env.js b/api/service-worker/env.js new file mode 100644 index 0000000000..929e09dd9c --- /dev/null +++ b/api/service-worker/env.js @@ -0,0 +1,240 @@ +/* global ErrorEvent, EventTarget */ +import database from '../internal/database.js' + +/** + * @typedef {{ + * scope: string + * }} EnvironmentOptions + */ + +/** + * An event dispatched when a environment value is updated (set, delete) + */ +export class EnvironmentEvent extends Event { + /** + * `EnvironmentEvent` class constructor. + * @param {'set'|'delete'} type + * @param {object=} [entry] + */ + constructor (type, entry = null) { + super(type) + this.entry = entry + } +} + +/** + * Awaits a promise forwarding errors to the `Environment` instance. + * @ignore + * @param {Environment} env + * @param {Promise} promise + */ +async function forward (env, promise) { + try { + return await promise + } catch (error) { + env.dispatchEvent(new ErrorEvent('error', { error })) + } +} + +/** + * An environment context object with persistence and durability + * for service worker environments. + */ +export class Environment extends EventTarget { + /** + * Maximum entries that will be restored from storage into the environment + * context object. + * @type {number} + */ + static MAX_CONTEXT_ENTRIES = 16 * 1024 + + /** + * Opens an environment for a particular scope. + * @param {EnvironmentOptions} options + * @return {Environment} + */ + static async open (options) { + const environment = new this(options) + await environment.open() + return environment + } + + /** + * The current `Environment` instance + * @type {Environment?} + */ + static instance = null + + #database = null + #context = {} + #proxy = null + #scope = '/' + #type = 'serviceWorker' + + /** + * `Environment` class constructor + * @ignore + * @param {EnvironmentOptions} options + */ + constructor (options) { + super() + + Environment.instance = this + + this.#type = options?.type ?? this.#type + this.#scope = options.scope ?? this.#scope + this.#proxy = new Proxy(this.#context, { + get: (target, property) => { + return target[property] ?? undefined + }, + + set: (target, property, value) => { + target[property] = value + if (this.database && this.database.opened) { + forward(this, this.database.put(property, value)) + } + + this.dispatchEvent(new EnvironmentEvent('set', { + key: property, + value + })) + return true + }, + + deleteProperty: (target, property) => { + if (this.database && this.database.opened) { + forward(this, this.database.delete(property)) + } + + this.dispatchEvent(new EnvironmentEvent('delete', { + key: property + })) + + return Reflect.deleteProperty(target, property) + }, + + has: (target, property) => { + return Reflect.has(target, property) + } + }) + } + + /** + * A reference to the currently opened environment database. + * @type {import('../internal/database.js').Database} + */ + get database () { + return this.#database + } + + /** + * A proxied object for reading and writing environment state. + * Values written to this object must be cloneable with respect to the + * structured clone algorithm. + * @see {https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm} + * @type {Proxy<object>} + */ + get context () { + return this.#proxy + } + + /** + * The environment type + * @type {string} + */ + get type () { + return this.#type + } + + /** + * The current environment name. This value is also used as the + * internal database name. + * @type {string} + */ + get name () { + return `socket.runtime.${this.#type}.env(${this.#scope})` + } + + /** + * Resets the current environment to an empty state. + */ + async reset () { + await this.close() + await database.drop(this.name) + await this.open() + } + + /** + * Opens the environment. + * @ignore + */ + async open () { + if (!this.#database) { + this.#database = await database.open(this.name) + const entries = await this.#database.get(undefined, { + count: Environment.MAX_CONTEXT_ENTRIES + }) + + for (const [key, value] of entries) { + this.#context[key] = value + } + } + } + + /** + * Closes the environment database, purging existing state. + * @ignore + */ + async close () { + await this.#database.close() + for (const key in this.#context) { + Reflect.deleteProperty(this.#context, key) + } + } +} + +/** + * Opens an environment for a particular scope. + * @param {EnvironmentOptions} options + * @return {Promise<Environment>} + */ +export async function open (options) { + return await Environment.open(options) +} + +/** + * Closes an active `Environment` instance, dropping the global + * instance reference. + * @return {Promise<boolean>} + */ +export async function close () { + if (Environment.instance) { + const instance = Environment.instance + Environment.instance = null + await instance.close() + return true + } + + return false +} + +/** + * Resets an active `Environment` instance + * @return {Promise<boolean>} + */ +export async function reset () { + if (Environment.instance) { + const instance = Environment.instance + await instance.reset() + return true + } + + return false +} + +export default { + Environment, + close, + reset, + open +} diff --git a/api/service-worker/events.js b/api/service-worker/events.js new file mode 100644 index 0000000000..b750e04e53 --- /dev/null +++ b/api/service-worker/events.js @@ -0,0 +1,436 @@ +/* global MessagePort */ +import { Deferred } from '../async.js' +import { Context } from './context.js' +import application from '../application.js' +import location from '../location.js' +import state from './state.js' +import ipc from '../ipc.js' + +export const textEncoder = new TextEncoderStream() + +export const FETCH_EVENT_TIMEOUT = ( + // TODO(@jwerle): document this + parseInt(application.config.webview_service_worker_fetch_event_timeout) || + 30000 +) + +export const FETCH_EVENT_MAX_RESPONSE_REDIRECTS = ( + // TODO(@jwerle): document this + parseInt(application.config.webview_service_worker_fetch_event_max_response_redirects) || + 16 // this aligns with WebKit +) + +/** + * The `ExtendableEvent` interface extends the lifetime of the "install" and + * "activate" events dispatched on the global scope as part of the service + * worker lifecycle. + */ +export class ExtendableEvent extends Event { + #promise = new Deferred() + #promises = [] + #pendingPromiseCount = 0 + #context = null + + /** + * `ExtendableEvent` class constructor. + * @ignore + */ + constructor (...args) { + super(...args) + this.#context = new Context(this) + } + + /** + * A context for this `ExtendableEvent` instance. + * @type {import('./context.js').Context} + */ + get context () { + return this.#context + } + + /** + * A promise that can be awaited which waits for this `ExtendableEvent` + * instance no longer has pending promises. + * @type {Promise} + */ + get awaiting () { + return this.waitsFor() + } + + /** + * The number of pending promises + * @type {number} + */ + get pendingPromises () { + return this.#pendingPromiseCount + } + + /** + * `true` if the `ExtendableEvent` instance is considered "active", + * otherwise `false`. + * @type {boolean} + */ + get isActive () { + return ( + this.#pendingPromiseCount > 0 || + this.eventPhase === Event.AT_TARGET + ) + } + + /** + * Tells the event dispatcher that work is ongoing. + * It can also be used to detect whether that work was successful. + * @param {Promise} promise + */ + waitUntil (promise) { + // we ignore the isTrusted check here and just verify the event phase + if (this.eventPhase !== Event.AT_TARGET) { + throw new DOMException('Event is not active', 'InvalidStateError') + } + + if (typeof promise?.then === 'function') { + this.#pendingPromiseCount++ + this.#promises.push(promise) + promise.then( + () => queueMicrotask(() => { + if (--this.#pendingPromiseCount === 0) { + this.#promise.resolve() + } + }), + () => queueMicrotask(() => { + if (--this.#pendingPromiseCount === 0) { + this.#promise.resolve() + } + }) + ) + + // handle 0 pending promises + } + } + + /** + * Returns a promise that this `ExtendableEvent` instance is waiting for. + * @return {Promise} + */ + async waitsFor () { + if (this.#pendingPromiseCount === 0) { + this.#promise.resolve() + } + + return await this.#promise + } +} + +/** + * This is the event type for "fetch" events dispatched on the service worker + * global scope. It contains information about the fetch, including the + * request and how the receiver will treat the response. + */ +export class FetchEvent extends ExtendableEvent { + static defaultHeaders = new Headers() + + #handled = new Deferred() + #request = null + #clientId = null + #isReload = false + #fetchId = null + #responded = false + #timeout = null + + /** + * `FetchEvent` class constructor. + * @ignore + * @param {string=} [type = 'fetch'] + * @param {object=} [options] + */ + constructor (type = 'fetch', options = null) { + super(type, options) + + this.#fetchId = options?.fetchId ?? null + this.#request = options?.request ?? null + this.#clientId = options?.clientId ?? '' + this.#isReload = options?.isReload === true + this.#timeout = setTimeout(() => { + this.respondWith(new Response('Request Timeout', { + status: 408, + statusText: 'Request Timeout' + })) + }, FETCH_EVENT_TIMEOUT) + } + + /** + * The handled property of the `FetchEvent` interface returns a promise + * indicating if the event has been handled by the fetch algorithm or not. + * This property allows executing code after the browser has consumed a + * response, and is usually used together with the `waitUntil()` method. + * @type {Promise} + */ + get handled () { + return this.#handled.then(Promise.resolve()) + } + + /** + * The request read-only property of the `FetchEvent` interface returns the + * `Request` that triggered the event handler. + * @type {Request} + */ + get request () { + return this.#request + } + + /** + * The `clientId` read-only property of the `FetchEvent` interface returns + * the id of the Client that the current service worker is controlling. + * @type {string} + */ + get clientId () { + return this.#clientId + } + + /** + * @ignore + * @type {string} + */ + get resultingClientId () { + return '' + } + + /** + * @ignore + * @type {string} + */ + get replacesClientId () { + return '' + } + + /** + * @ignore + * @type {boolean} + */ + get isReload () { + return this.#isReload + } + + /** + * @ignore + * @type {Promise} + */ + get preloadResponse () { + return Promise.resolve(null) + } + + /** + * The `respondWith()` method of `FetchEvent` prevents the webview's + * default fetch handling, and allows you to provide a promise for a + * `Response` yourself. + * @param {Response|Promise<Response>} response + */ + respondWith (response) { + if (this.#responded) { + return + } + + this.#responded = true + clearTimeout(this.#timeout) + + const clientId = this.#clientId + const handled = this.#handled + const id = this.#fetchId + + queueMicrotask(async () => { + try { + response = await response + + if (!response || !(response instanceof Response)) { + // TODO(@jwerle): handle this + return + } + + if (response.type === 'error') { + const statusCode = 0 + const headers = [] + const params = { + statusCode, + clientId, + headers, + id + } + + params['runtime-preload-injection'] = 'disabled' + + const result = await ipc.request('serviceWorker.fetch.response', params) + + if (result.err) { + state.reportError(result.err) + } + + handled.resolve() + return + } + + let arrayBuffer = null + let statusCode = response.status ?? 200 + + // just follow the redirect here now + if (statusCode >= 300 && statusCode < 400 && response.headers.has('location')) { + let previousResponse = response + let remainingRedirects = FETCH_EVENT_MAX_RESPONSE_REDIRECTS + + while (remainingRedirects-- > 0) { + const redirectLocation = previousResponse.headers.get('location') + + if (!redirectLocation) { + statusCode = 404 + break + } + + const url = new URL(redirectLocation, location.origin) + previousResponse = await fetch(url.href) + + if (previousResponse.status >= 200 && previousResponse.status < 300) { + arrayBuffer = await previousResponse.arrayBuffer() + break + } else if (previousResponse.status >= 300 && statusCode < 400) { + continue + } else { + statusCode = previousResponse.statusCode + arrayBuffer = await previousResponse.arrayBuffer() + break + } + } + } else { + arrayBuffer = await response.arrayBuffer() + } + + const headers = [] + .concat(Array.from(response.headers.entries())) + .concat(Array.from(FetchEvent.defaultHeaders.entries())) + .map((entry) => entry.join(':')) + .concat('Runtime-Response-Source:serviceworker') + .join('\n') + + const params = { + statusCode, + clientId, + headers, + id + } + + params['runtime-preload-injection'] = ( + response.headers.get('runtime-preload-injection') || + 'auto' + ) + + const result = await ipc.write( + 'serviceWorker.fetch.response', + params, + new Uint8Array(arrayBuffer) + ) + + if (result.err) { + state.reportError(result.err) + } + + handled.resolve() + } catch (err) { + state.reportError(err) + } finally { + handled.resolve() + } + }) + } +} + +export class ExtendableMessageEvent extends ExtendableEvent { + #data = null + #ports = [] + #origin = null + #source = null + #lastEventId = '' + + /** + * `ExtendableMessageEvent` class constructor. + * @param {string=} [type = 'message'] + * @param {object=} [options] + */ + constructor (type = 'message', options = null) { + super(type, options) + this.#data = options?.data ?? null + + if (Array.isArray(options?.ports)) { + for (const port of options.ports) { + if (port instanceof MessagePort) { + this.#ports.push(port) + } + } + } + + if (options?.source) { + this.#source = options.source + } + } + + /** + * @type {any} + */ + get data () { + return this.#data + } + + /** + * @type {MessagePort[]} + */ + get ports () { + return this.#ports + } + + /** + * @type {import('./clients.js').Client?} + */ + get source () { + return this.#source + } + + /** + * @type {string?} + */ + get origin () { + return this.#origin + } + + /** + * @type {string} + */ + get lastEventId () { + return this.#lastEventId + } +} + +export class NotificationEvent extends ExtendableEvent { + #action = '' + #notification = null + + constructor (type, options) { + super(type, options) + + if (typeof options?.action === 'string') { + this.#action = options.action + } + + this.#notification = options.notification + } + + get action () { + return this.#action + } + + get notification () { + return this.#notification + } +} + +export default { + ExtendableMessageEvent, + ExtendableEvent, + FetchEvent +} diff --git a/api/service-worker/global.js b/api/service-worker/global.js new file mode 100644 index 0000000000..6994ed127a --- /dev/null +++ b/api/service-worker/global.js @@ -0,0 +1,131 @@ +import { ExtendableEvent, FetchEvent } from './events.js' +import { ServiceWorkerRegistration } from './registration.js' +import { createServiceWorker } from './instance.js' +import clients from './clients.js' +import state from './state.js' +import ipc from '../ipc.js' + +// events +let onactivate = null +let onmessage = null +let oninstall = null +let onfetch = null + +// this is set one time +let registration = null +let serviceWorker = null + +export class ServiceWorkerGlobalScope { + get isServiceWorkerScope () { + return true + } + + get ExtendableEvent () { + return ExtendableEvent + } + + get FetchEvent () { + return FetchEvent + } + + get serviceWorker () { + return serviceWorker + } + + get registration () { + return registration + } + + set registration (value) { + if (!registration) { + const info = { registration: value } + + serviceWorker = createServiceWorker(state.serviceWorker.state, info.registration) + registration = new ServiceWorkerRegistration(info, serviceWorker) + } + } + + get clients () { + return clients + } + + get onactivate () { + return onactivate + } + + set onactivate (listener) { + if (onactivate) { + globalThis.removeEventListener('activate', onactivate) + } + + onactivate = null + + if (typeof listener === 'function') { + globalThis.addEventListener('activate', listener) + onactivate = listener + } + } + + get onmessage () { + return onmessage + } + + set onmessage (listener) { + if (onmessage) { + globalThis.removeEventListener('message', onmessage) + } + + onmessage = null + + if (typeof listener === 'function') { + globalThis.addEventListener('message', listener) + onmessage = listener + } + } + + get oninstall () { + return oninstall + } + + set oninstall (listener) { + if (oninstall) { + globalThis.removeEventListener('install', oninstall) + } + + oninstall = null + + if (typeof listener === 'function') { + globalThis.addEventListener('install', listener) + oninstall = listener + } + } + + get onfetch () { + return onfetch + } + + set onfetch (listener) { + if (fetch) { + globalThis.removeEventListener('fetch', fetch) + } + + onfetch = null + + if (typeof listener === 'function') { + globalThis.addEventListener('fetch', listener) + onfetch = listener + } + } + + async skipWaiting () { + const result = await ipc.request('serviceWorker.skipWaiting', { + id: state.id + }) + + if (result.err) { + throw result.err + } + } +} + +export default ServiceWorkerGlobalScope.prototype diff --git a/api/service-worker/index.html b/api/service-worker/index.html new file mode 100644 index 0000000000..828400c539 --- /dev/null +++ b/api/service-worker/index.html @@ -0,0 +1,145 @@ +<!doctype html> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content=" + connect-src socket: https: http: blob: ipc: wss: ws: ws://localhost:* {{protocol_handlers}}; + script-src socket: https: http: blob: http://localhost:* 'unsafe-eval' 'unsafe-inline' {{protocol_handlers}}; + worker-src socket: https: http: blob: 'unsafe-eval' 'unsafe-inline' {{protocol_handlers}}; + frame-src socket: https: http: blob: http://localhost:*; + img-src socket: https: http: blob: http://localhost:*; + child-src socket: https: http: blob:; + object-src 'none'; + " + > + <script charset="utf-8" type="text/javascript"> + Object.defineProperty(globalThis, '__RUNTIME_SERVICE_WORKER_CONTEXT__', { + configurable: false, + enumerable: false, + writable: false, + value: true + }) + </script> + <script type="module"> + import application from 'socket:application' + import process from 'socket:process' + + Object.assign(globalThis, { + async openExternalLink (href) { + const currentWindow = await application.getCurrentWindow() + await currentWindow.openExternal(href) + } + }) + + document.title = `Service Worker Debugger - v${process.version}` + </script> + <script type="module" src="./init.js"></script> + <style type="text/css" media="all"> + * { + box-sizing: border-box; + } + + body { + background: rgba(40, 40, 40, 1); + color: rgba(255, 255, 255, 1); + font-family: 'Inter-Light', sans-serif; + font-size: 14px; + margin: 0; + position: absolute; top: 0px; left: 0; right:0; bottom: 0px; + } + + a { + color: inherit; + text-decoration: none; + transition: all 0.2s ease; + + &.with-hover:hover { + border-bottom: 2px solid rgba(255, 255, 255, 1); + } + } + + p { + background: rgba(14, 85, 152, .25); + border-radius: 2px; + display: inline-block; + font: 12px/26px 'Inter-Light', sans-serif; + margin: 0; + text-align: center; + width: 100%; + + &.message { + display: block; + overflow: hidden; + padding: 0 8px; + position: relative; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } + } + + pre#log { + background: rgba(0, 0, 0, 1); + overflow-y: scroll; + padding: 16px; + margin: 0; + position: absolute; top: 0px; left: 0; right:0; bottom: 0px; + + & span { + opacity: 0.8; + transition: all 0.2s ease; + + &:hover { + opacity: 1; + transition: all 0.05s ease; + } + + & code { + color: rgb(215, 215, 215); + display: block; + font-size: 12px; + line-break: anywhere; + margin-bottom: 8px; + opacity: 0.8; + white-space: wrap; + transition: all 0.1s ease; + + &:hover { + color: rgb(225, 225, 225); + opacity: 1; + transition: all 0.025s ease; + } + + & span { + &.red { + color: red; + } + } + } + + & details { + &[open] span { + opacity: 1; + transition: all 0.05s ease; + & code { + color: rgb(225, 225, 225); + opacity: 1; + transition: all 0.025s ease; + } + } + + & > summary { + cursor: pointer; + list-style: none; + } + } + } + } + </style> + </head> + <body> + <pre id="log"></pre> + </body> +</html> diff --git a/api/service-worker/init.js b/api/service-worker/init.js new file mode 100644 index 0000000000..26287edcf4 --- /dev/null +++ b/api/service-worker/init.js @@ -0,0 +1,364 @@ +/* global Worker */ +import { SHARED_WORKER_URL } from './instance.js' +import { SharedWorker } from '../shared-worker/index.js' +import { Notification } from '../notification.js' +import { sleep } from '../timers.js' +import globals from '../internal/globals.js' +import crypto from '../crypto.js' +import hooks from '../hooks.js' +import ipc from '../ipc.js' + +export const workers = new Map() + +globals.register('ServiceWorkerContext.workers', workers) +globals.register('ServiceWorkerContext.info', new Map()) + +const sharedWorker = new SharedWorker(SHARED_WORKER_URL) +sharedWorker.port.start() +sharedWorker.port.onmessage = (event) => { + if (event.data?.from === 'instance' && event.data.registration?.id) { + for (const worker of workers.values()) { + if (worker.info.id === event.data.registration.id) { + worker.postMessage(event.data) + break + } + } + } else if (event.data?.showNotification && event.data.registration?.id) { + onNotificationShow(event, sharedWorker.port) + } else if (event.data?.getNotifications && event.data.registration?.id) { + onGetNotifications(event, sharedWorker.port) + } +} + +export class ServiceWorkerInstance extends Worker { + #info = null + #notifications = [] + + constructor (filename, options) { + super(filename, { + name: `ServiceWorker (${options?.info?.pathname ?? filename})`, + ...options, + [Symbol.for('socket.runtime.internal.worker.type')]: 'serviceWorker' + }) + + this.#info = options?.info ?? null + this.addEventListener('message', this.onMessage.bind(this)) + } + + get info () { + return this.#info + } + + get notifications () { + return this.#notifications + } + + async onMessage (event) { + const { info } = this + if (event.data.__service_worker_ready === true) { + this.postMessage({ register: info }) + } else if (event.data.__service_worker_registered?.id === info.id) { + this.postMessage({ install: info }) + } else if (event.data?.message && event?.data.client?.id) { + sharedWorker.port.postMessage({ + ...event.data, + from: 'realm' + }) + } else if (Array.isArray(event.data.__service_worker_debug)) { + const log = document.querySelector('#log') + if (log) { + for (const entry of event.data.__service_worker_debug) { + const lines = entry.split('\n') + const span = document.createElement('span') + let target = span + + for (const line of lines) { + if (!line) continue + const item = document.createElement('code') + item.innerHTML = line + .replace(/\s/g, ' ') + .replace(/\\s/g, ' ') + .replace(/<anonymous>/g, '<anonymous>') + .replace(/([a-z|A-Z|_|0-9]+(Error|Exception)):/g, '<span class="red"><b>$1</b>:</span>') + + if (target === span && lines.length > 1) { + target = document.createElement('details') + const summary = document.createElement('summary') + summary.appendChild(item) + target.appendChild(summary) + span.appendChild(target) + } else { + target.appendChild(item) + } + } + + log.appendChild(span) + } + + log.scrollTop = log.scrollHeight + } + } else if (event.data?.showNotification && event.data.registration?.id) { + onNotificationShow(event, this) + } else if (event.data?.getNotifications && event.data.registration?.id) { + onGetNotifications(event, this) + } else if (event.data?.notificationclose?.id) { + onNotificationClose(event, this) + } + } +} + +export class ServiceWorkerInfo { + id = null + url = null + hash = null + scope = null + scriptURL = null + + constructor (data) { + for (const key in data) { + const value = data[key] + if (key in this) { + this[key] = value + } + } + + const url = new URL(this.scriptURL) + this.url = url.toString() + this.hash = crypto.murmur3(url.pathname + (this.scope || '')) + } + + get pathname () { + return new URL(this.url).pathname + } +} + +export async function onRegister (event) { + const info = new ServiceWorkerInfo(event.detail) + + if (!info.id || workers.has(info.hash)) { + return + } + + const worker = new ServiceWorkerInstance('./worker.js', { + info + }) + + workers.set(info.hash, worker) + globals.get('ServiceWorkerContext.info').set(info.hash, info) +} + +export async function onUnregister (event) { + const info = new ServiceWorkerInfo(event.detail) + + if (!workers.has(info.hash)) { + return + } + + const worker = workers.get(info.hash) + workers.delete(info.hash) + + worker.postMessage({ activate: info }) +} + +export async function onSkipWaiting (event) { + onActivate(event) +} + +export async function onActivate (event) { + const info = new ServiceWorkerInfo(event.detail) + + if (!workers.has(info.hash)) { + return + } + + const worker = workers.get(info.hash) + + worker.postMessage({ activate: info }) +} + +export async function onFetch (event) { + const info = new ServiceWorkerInfo(event.detail) + const exists = workers.has(info.hash) + + // this may be an early fetch, just try waiting at most + // 32*16 milliseconds for the worker to be available or + // the 'activate' event before generating a 404 response + await Promise.race([ + (async function () { + let retries = 16 + while (!workers.has(info.hash) && --retries > 0) { + await sleep(32) + } + + if (!exists) { + await sleep(256) + } + })(), + new Promise((resolve) => { + globalThis.top.addEventListener( + 'serviceWorker.activate', + async function (event) { + // @ts-ignore + const { hash } = new ServiceWorkerInfo(event.detail) + if (hash === info.hash) { + await sleep(64) + resolve(null) + } + } + ) + }) + ]) + + if (!workers.has(info.hash)) { + const options = { + statusCode: 404, + clientId: event.detail.fetch.client.id, + headers: '', + id: event.detail.fetch.id + } + + return await ipc.write('serviceWorker.fetch.response', options) + } + + const client = event.detail.fetch.client ?? {} + const request = { + id: event.detail.fetch.id, + url: new URL( + ( + event.detail.fetch.pathname + + (event.detail.fetch.query.length ? '?' + event.detail.fetch.query : '') + ), + `${event.detail.fetch.scheme}://${event.detail.fetch.host}` + ).toString(), + + method: event.detail.fetch.method, + headers: event.detail.fetch.headers + } + + const worker = workers.get(info.hash) + + worker.postMessage({ fetch: { ...info, client, request } }) +} + +export function onNotificationShow (event, target) { + for (const worker of workers.values()) { + if (worker.info.id === event.data.registration.id) { + let notification = null + + try { + notification = new Notification( + event.data.showNotification.title, + event.data.showNotification + ) + } catch (error) { + return target.postMessage({ + nonce: event.data.nonce, + notification: { + error: { message: error.message } + } + }) + } + + worker.notifications.push(notification) + notification.onshow = () => { + notification.onshow = null + return target.postMessage({ + nonce: event.data.nonce, + notification: { + id: notification.id + } + }) + } + + notification.onclick = (event) => { + worker.postMessage({ + notificationclick: { + title: notification.title, + options: notification.options.toJSON(), + data: { + id: notification.id, + timestamp: notification.timestamp + } + } + }) + } + + notification.onclose = (event) => { + notification.onclose = null + notification.onclick = null + worker.postMessage({ + notificationclose: { + title: notification.title, + options: notification.options.toJSON(), + data: { + id: notification.id, + timestamp: notification.timestamp + } + } + }) + + const index = worker.notifications.indexOf(notification) + if (index >= 0) { + worker.notifications.splice(index, 1) + } + } + break + } + } +} + +export function onNotificationClose (event, target) { + for (const worker of workers.values()) { + for (const notification of worker.notifications) { + if (event.data.notificationclose.id === notification.id) { + notification.close() + return + } + } + } +} + +export function onGetNotifications (event, target) { + for (const worker of workers.values()) { + if (worker.info.id === event.data.registration.id) { + return target.postMessage({ + nonce: event.data.nonce, + notifications: worker.notifications + .filter((notification) => + !event.data.getNotifications?.tag || + event.data.getNotifications.tag === notification.tag + ) + .map((notification) => ({ + title: notification.title, + options: notification.options.toJSON(), + data: { + id: notification.id, + timestamp: notification.timestamp + } + })) + }) + } + } +} + +export default null + +globalThis.top.addEventListener('serviceWorker.register', onRegister) +globalThis.top.addEventListener('serviceWorker.unregister', onUnregister) +globalThis.top.addEventListener('serviceWorker.skipWaiting', onSkipWaiting) +globalThis.top.addEventListener('serviceWorker.activate', onActivate) +globalThis.top.addEventListener('serviceWorker.fetch', onFetch) + +hooks.onReady(async () => { + // notify top frame that the service worker init module is ready + globalThis.top.postMessage({ + __service_worker_frame_init: true + }) + + const result = await ipc.request('serviceWorker.getRegistrations') + if (Array.isArray(result.data)) { + for (const info of result.data) { + await navigator.serviceWorker.register(info.scriptURL, info) + } + } +}) diff --git a/api/service-worker/instance.js b/api/service-worker/instance.js new file mode 100644 index 0000000000..e39e4f100c --- /dev/null +++ b/api/service-worker/instance.js @@ -0,0 +1,175 @@ +import { SharedWorker } from '../shared-worker/index.js' +import location from '../location.js' +import state from './state.js' + +const serviceWorkers = new Map() +let sharedWorker = null + +export const SHARED_WORKER_URL = `${globalThis.origin}/socket/service-worker/shared-worker.js` + +export const ServiceWorker = globalThis.ServiceWorker ?? class ServiceWorker extends EventTarget { + get onmessage () { return null } + set onmessage (_) {} + get onerror () { return null } + set onerror (_) {} + get onstatechange () { return null } + set onstatechange (_) {} + get state () { return null } + get scriptURL () { return null } + postMessage () {} +} + +export function createServiceWorker ( + currentState = state.serviceWorker.state, + options = null +) { + const id = options?.id ?? state.id ?? null + + if (!globalThis.isServiceWorkerScope) { + if (id && serviceWorkers.has(id)) { + return serviceWorkers.get(id) + } + } + + const channel = new BroadcastChannel('socket.runtime.serviceWorker.state') + + // events + const eventTarget = new EventTarget() + let onstatechange = null + let onerror = null + + // state + let serviceWorker = null + let scriptURL = options?.scriptURL ?? null + + if ( + globalThis.RUNTIME_WORKER_LOCATION !== SHARED_WORKER_URL && + globalThis.location.pathname !== '/socket/service-worker/index.html' + ) { + sharedWorker = new SharedWorker(SHARED_WORKER_URL) + sharedWorker.port.start() + } + + serviceWorker = Object.create(ServiceWorker.prototype, { + postMessage: { + enumerable: false, + configurable: false, + value (message, ...args) { + if (sharedWorker && globalThis.__args?.client) { + sharedWorker.port.postMessage({ + message, + from: 'instance', + registration: { id }, + client: { + id: globalThis.__args.client.id, + url: location.pathname + location.search, + type: globalThis.__args.client.type, + index: globalThis.__args.index, + origin: location.origin, + frameType: globalThis.__args.client.frameType + } + }, ...args) + } + } + }, + + state: { + configurable: true, + enumerable: false, + get: () => currentState === null ? state.serviceWorker.state : currentState + }, + + scriptURL: { + configurable: true, + enumerable: false, + get: () => scriptURL + }, + + onerror: { + enumerable: false, + get: () => onerror, + set: (listener) => { + if (onerror) { + eventTarget.removeEventListener('error', onerror) + } + + onerror = null + + if (typeof listener === 'function') { + eventTarget.addEventListener('error', listener) + onerror = listener + } + } + }, + + onstatechange: { + enumerable: false, + get: () => onstatechange, + set: (listener) => { + if (onstatechange) { + eventTarget.removeEventListener('statechange', onstatechange) + } + + onstatechange = null + + if (typeof listener === 'function') { + eventTarget.addEventListener('statechange', listener) + onstatechange = listener + } + } + }, + + dispatchEvent: { + configurable: false, + enumerable: false, + value: eventTarget.dispatchEvent.bind(eventTarget) + }, + + addEventListener: { + configurable: false, + enumerable: false, + value: eventTarget.addEventListener.bind(eventTarget) + }, + + removeEventListener: { + configurable: false, + enumerable: false, + value: eventTarget.removeEventListener.bind(eventTarget) + } + }) + + if (options?.subscribe !== false) { + channel.addEventListener('message', (event) => { + const { data } = event + if (data?.serviceWorker?.id === id) { + if (data.serviceWorker.state && data.serviceWorker.state !== currentState) { + const scope = new URL(location.href).pathname + if (scope.startsWith(data.serviceWorker.scope)) { + if (data.serviceWorker.scriptURL) { + scriptURL = data.serviceWorker.scriptURL + } + + if (data.serviceWorker.state !== currentState) { + currentState = data.serviceWorker.state + const event = new Event('statechange') + + Object.defineProperties(event, { + target: { value: serviceWorker } + }) + + eventTarget.dispatchEvent(event) + } + } + } + } + }) + } + + if (!globalThis.isServiceWorkerScope && id) { + serviceWorkers.set(id, serviceWorker) + } + + return serviceWorker +} + +export default createServiceWorker diff --git a/api/service-worker/notification.js b/api/service-worker/notification.js new file mode 100644 index 0000000000..ae094bfdf2 --- /dev/null +++ b/api/service-worker/notification.js @@ -0,0 +1,129 @@ +import { Notification, NotificationOptions } from '../notification.js' +import { SHARED_WORKER_URL } from './instance.js' +import { NotAllowedError } from '../errors.js' +import { SharedWorker } from '../shared-worker/index.js' +import permissions from '../internal/permissions.js' + +let sharedWorker = null + +const observedNotifications = new Set() + +if (globalThis.isServiceWorkerScope) { + globalThis.addEventListener('notificationclose', (event) => { + for (const notification of observedNotifications) { + if (notification.id === event.notification.id) { + notification.dispatchEvent(new Event('close')) + observedNotifications.delete(notification) + } + } + }) +} + +function ensureSharedWorker () { + if (!globalThis.isServiceWorkerScope && !sharedWorker) { + sharedWorker = new SharedWorker(SHARED_WORKER_URL) + sharedWorker.port.start() + } +} + +export async function showNotification (registration, title, options) { + ensureSharedWorker() + + if (title && typeof title === 'object') { + options = title + title = options.title ?? '' + } + + const info = registration[Symbol.for('socket.runtime.ServiceWorkerRegistration.info')] + + if (!info) { + throw new TypeError('Invalid ServiceWorkerRegistration instance given') + } + + if (!registration.active) { + throw new TypeError('ServiceWorkerRegistration is not active') + } + + const query = await permissions.query({ name: 'notifications' }) + + if (query.state !== 'granted') { + throw new NotAllowedError('Operation not permitted') + } + + // will throw if invalid options are given + options = new NotificationOptions(options, /* allowServiceWorkerGlobalScope= */ true) + const nonce = Math.random().toString(16).slice(2) + const target = globalThis.isServiceWorkerScope ? globalThis : sharedWorker.port + const message = { + nonce, + registration: { id: info.id }, + showNotification: { title, ...options.toJSON() } + } + + target.postMessage(message) + + await new Promise((resolve, reject) => { + target.addEventListener('message', function onMessage (event) { + if (event.data?.nonce === nonce) { + target.removeEventListener('message', onMessage) + if (event.data.error) { + reject(new Error(event.data.error.message)) + } else { + resolve(event.data.notification) + } + } + }) + }) +} + +export async function getNotifications (registration, options = null) { + ensureSharedWorker() + + const info = registration[Symbol.for('socket.runtime.ServiceWorkerRegistration.info')] + + if (!info) { + throw new TypeError('Invalid ServiceWorkerRegistration instance given') + } + + if (!registration.active) { + throw new TypeError('ServiceWorkerRegistration is not active') + } + + const nonce = Math.random().toString(16).slice(2) + const target = globalThis.isServiceWorkerScope ? globalThis : sharedWorker.port + const message = { + nonce, + registration: { id: info.id }, + getNotifications: { tag: options?.tag ?? null } + } + + target.postMessage(message) + + const notifications = await new Promise((resolve, reject) => { + target.addEventListener('message', function onMessage (event) { + if (event.data?.nonce === nonce) { + target.removeEventListener('message', onMessage) + if (event.data.error) { + reject(new Error(event.data.message)) + } else { + resolve(event.data.notifications.map((notification) => new Notification( + notification.title, + notification.options, + notification.data + ))) + } + } + }) + }) + + for (const notification of notifications) { + observedNotifications.add(notification) + } + + return notifications +} + +export default { + showNotification, + getNotifications +} diff --git a/api/service-worker/registration.js b/api/service-worker/registration.js new file mode 100644 index 0000000000..4ec6eda4e1 --- /dev/null +++ b/api/service-worker/registration.js @@ -0,0 +1,120 @@ +import { showNotification, getNotifications } from './notification.js' +import ipc from '../ipc.js' + +export class ServiceWorkerRegistration { + #info = null + #active = null + #waiting = null + #installing = null + #onupdatefound = null + + constructor (info, serviceWorker) { + this.#info = info + + // many states here just end up being the 'installing' state to the front end + if (/installing|parsed|registered|registering/.test(serviceWorker.state)) { + this.#installing = serviceWorker + } else if (serviceWorker.state === 'installed') { + this.#waiting = serviceWorker + } else if (serviceWorker.state === 'activating' || serviceWorker.state === 'activated') { + this.#active = serviceWorker + } + + serviceWorker.addEventListener('statechange', (event) => { + const { state } = event.target + + if (state === 'installing') { + this.#active = null + this.#waiting = null + this.#installing = serviceWorker + } + + if (state === 'installed') { + this.#active = null + this.#waiting = serviceWorker + this.#installing = null + } + + if (state === 'activating' || state === 'activated') { + this.#active = serviceWorker + this.#waiting = null + this.#installing = null + } + }) + } + + get [Symbol.for('socket.runtime.ServiceWorkerRegistration.info')] () { + return this.#info?.registration ?? null + } + + get scope () { + return this.#info.registration.scope + } + + get updateViaCache () { + return 'none' + } + + get installing () { + return this.#installing + } + + get waiting () { + return this.#waiting + } + + get active () { + return this.#active + } + + get onupdatefound () { + return this.#onupdatefound + } + + set onupdatefound (onupdatefound) { + if (this.#onupdatefound) { + this.removeEventListener('updatefound', this.#onupdatefound) + } + + this.#onupdatefound = null + + if (typeof onupdatefound === 'function') { + this.addEventListener('updatefound', this.#onupdatefound) + this.#onupdatefound = onupdatefound + } + } + + get navigationPreload () { + return null + } + + async getNotifications () { + return await getNotifications(this) + } + + async showNotification (title, options) { + return await showNotification(this, title, options) + } + + async unregister () { + const result = await ipc.request('serviceWorker.unregister', { + scope: this.scope + }) + + if (result.err) { + throw result.err + } + } + + async update () { + } +} + +if (typeof globalThis.ServiceWorkerRegistration === 'function') { + Object.setPrototypeOf( + ServiceWorkerRegistration.prototype, + globalThis.ServiceWorkerRegistration.prototype + ) +} + +export default ServiceWorkerRegistration diff --git a/api/service-worker/shared-worker.js b/api/service-worker/shared-worker.js new file mode 100644 index 0000000000..9821a8351a --- /dev/null +++ b/api/service-worker/shared-worker.js @@ -0,0 +1,61 @@ +const Uint8ArrayPrototype = Uint8Array.prototype +const TypedArrayPrototype = Object.getPrototypeOf(Uint8ArrayPrototype) +const TypedArray = TypedArrayPrototype.constructor + +function isTypedArray (object) { + return object instanceof TypedArray +} + +function isArrayBuffer (object) { + return object instanceof ArrayBuffer +} + +function findMessageTransfers (transfers, object, options = null) { + if (isTypedArray(object) || ArrayBuffer.isView(object)) { + add(object.buffer) + } else if (isArrayBuffer(object)) { + add(object) + } else if (object instanceof MessagePort) { + add(object) + } else if (Array.isArray(object)) { + for (const value of object) { + findMessageTransfers(transfers, value, options) + } + } else if (object && typeof object === 'object') { + for (const key in object) { + if ( + key.startsWith('__vmScriptReferenceArgs_') && + options?.ignoreScriptReferenceArgs === true + ) { + continue + } + + findMessageTransfers(transfers, object[key], options) + } + } + + return transfers + + function add (value) { + if (!transfers.includes(value)) { + transfers.push(value) + } + } +} + +const ports = [] +globalThis.addEventListener('connect', (event) => { + for (const port of event.ports) { + port.start() + ports.push(port) + port.addEventListener('message', (event) => { + for (const p of ports) { + if (p !== port) { + const transfer = [] + findMessageTransfers(transfer, event.data) + p.postMessage(event.data, { transfer }) + } + } + }) + } +}) diff --git a/api/service-worker/state.js b/api/service-worker/state.js new file mode 100644 index 0000000000..d9f8692cc6 --- /dev/null +++ b/api/service-worker/state.js @@ -0,0 +1,212 @@ +import application from '../application.js' +import debug from './debug.js' +import ipc from '../ipc.js' + +export const channel = new BroadcastChannel('socket.runtime.serviceWorker.state') + +const descriptors = { + channel: { + configurable: false, + enumerable: false, + value: channel + }, + + clients: { + configurable: false, + enumerable: true, + value: Object.create(null) + }, + + notify: { + configurable: false, + enumerable: false, + writable: false, + async value (type) { + channel.postMessage({ [type]: this[type] }) + + if (this.id && type === 'serviceWorker') { + debug( + '[%s]: ServiceWorker (%s) updated state to "%s"', + new URL(this.serviceWorker.scriptURL).pathname.replace(/^\/socket\//, 'socket:'), + this.id, + this.serviceWorker.state + ) + + await ipc.request('serviceWorker.updateState', { + id: this.id, + scope: this.serviceWorker.scope, + state: this.serviceWorker.state, + scriptURL: this.serviceWorker.scriptURL, + workerURL: globalThis.location.href + }) + } + } + }, + + serviceWorker: { + configurable: false, + enumerable: true, + value: Object.create(null, { + scope: { + configurable: false, + enumerable: true, + writable: true, + value: globalThis.window + ? new URL('.', globalThis.location.href).pathname + : '/' + }, + + scriptURL: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + state: { + configurable: false, + enumerable: true, + writable: true, + value: 'parsed' + }, + + id: { + configurable: false, + enumerable: true, + writable: true, + value: null + } + }) + }, + + id: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + fetch: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + install: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + activate: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + reportError: { + configurable: false, + enumerable: false, + writable: true, + value: globalThis.reportError.bind(globalThis) + } +} + +export const state = Object.create(null, descriptors) + +channel.addEventListener('message', (event) => { + if (event.data?.serviceWorker) { + let href = globalThis.location.href + if (href.startsWith('blob:')) { + href = new URL(href).pathname + } + + const scope = new URL('.', href).pathname + if (scope.startsWith(event.data.serviceWorker.scope)) { + Object.assign(state.serviceWorker, event.data.serviceWorker) + } + } else if (event.data?.clients?.get?.id) { + if (event.data.clients.get.id === globalThis.__args.client.id) { + channel.postMessage({ + clients: { + get: { + result: { + client: { + id: globalThis.__args.client.id, + url: globalThis.location.pathname + globalThis.location.search, + type: globalThis.__args.client.type, + index: globalThis.__args.index, + frameType: globalThis.__args.client.frameType + } + } + } + } + }) + } + } else if (event.data?.clients?.matchAll) { + const type = event.data.clients.matchAll?.type ?? 'window' + if (type === 'all' || type === globalThis.__args.type) { + channel.postMessage({ + clients: { + matchAll: { + result: { + client: { + id: globalThis.__args.client.id, + url: globalThis.location.pathname + globalThis.location.search, + type: globalThis.__args.client.type, + index: globalThis.__args.index, + frameType: globalThis.__args.client.frameType + } + } + } + } + }) + } + } +}) + +if (globalThis.document) { + channel.addEventListener('message', async (event) => { + if (event.data?.client?.id === globalThis.__args.client.id) { + if (event.data.client.focus === true) { + const currentWindow = await application.getCurrentWindow() + try { + await currentWindow.restore() + } catch {} + globalThis.focus() + } + } + }) + + globalThis.document.addEventListener('visibilitychange', (event) => { + channel.postMessage({ + client: { + id: globalThis.__args.client.id, + visibilityState: globalThis.document.visibilityState + } + }) + }) + + globalThis.addEventListener('focus', (event) => { + channel.postMessage({ + client: { + id: globalThis.__args.client.id, + focused: true + } + }) + }) + + globalThis.addEventListener('blur', (event) => { + channel.postMessage({ + client: { + id: globalThis.__args.client.id, + focused: false + } + }) + }) +} + +export default state diff --git a/api/service-worker/storage.js b/api/service-worker/storage.js new file mode 100644 index 0000000000..1c1dcc374f --- /dev/null +++ b/api/service-worker/storage.js @@ -0,0 +1,935 @@ +/* global DOMException */ +import { IllegalConstructorError } from '../errors.js' +import { Environment } from './env.js' +import { Deferred } from '../async/deferred.js' +import state from './state.js' +import ipc from '../ipc.js' + +// used to ensure `Storage` is not illegally constructed +const createStorageSymbol = Symbol('Storage.create') + +/** + * @typedef {{ done: boolean, value: string | undefined }} IndexIteratorResult + */ + +/** + * An iterator interface for an `Index` instance. + */ +export class IndexIterator { + #index = null + #current = 0 + + /** + * `IndexIterator` class constructor. + * @ignore + * @param {Index} index + */ + constructor (index) { + this.#index = index + } + + /** + * `true` if the iterator is "done", otherwise `false`. + * @type {boolean} + */ + get done () { + return this.#current === -1 || this.#current >= this.#index.length + } + + /** + * Returns the next `IndexIteratorResult`. + * @return {IndexIteratorResult} + */ + next () { + if (this.done) { + return { done: true, value: undefined } + } + + const value = this.#index.entry(this.#current++) + return { done: false, value } + } + + /** + * Mark `IndexIterator` as "done" + * @return {IndexIteratorResult} + */ + return () { + this.#current = -1 + return { done: true, value: undefined } + } +} + +/** + * A container used by the `Provider` to index keys and values + */ +export class Index { + #keys = [] + #values = [] + + /** + * A reference to the keys in this index. + * @type {string[]} + */ + get keys () { + return this.#keys + } + + /** + * A reference to the values in this index. + * @type {string[]} + */ + get values () { + return this.#values + } + + /** + * The number of entries in this index. + * @type {number} + */ + get length () { + return this.#keys.length + } + + /** + * Returns the key at a given `index`, if it exists otherwise `null`. + * @param {number} index} + * @return {string?} + */ + key (index) { + return this.#keys[index] ?? null + } + + /** + * Returns the value at a given `index`, if it exists otherwise `null`. + * @param {number} index} + * @return {string?} + */ + value (index) { + return this.#values[index] ?? null + } + + /** + * Inserts a value in the index. + * @param {string} key + * @param {string} value + */ + insert (key, value) { + const index = this.#keys.indexOf(key) + if (index >= 0) { + this.#values[index] = value + } else { + this.#keys.push(key) + this.#values.push(value) + } + } + + /** + * Computes the index of a key in this index. + * @param {string} key + * @return {number} + */ + indexOf (key) { + return this.#keys.indexOf(key) + } + + /** + * Clears all keys and values in the index. + */ + clear () { + this.#keys.splice(0, this.#keys.length) + this.#values.splice(0, this.#values.length) + } + + /** + * Returns an entry at `index` if it exists, otherwise `null`. + * @param {number} index + * @return {string[]|null} + */ + entry (index) { + const key = this.key(index) + const value = this.value(index) + + if (key) { + return [key, value] + } + + return null + } + + /** + * Removes entries at a given `index`. + * @param {number} index + * @return {boolean} + */ + remove (index) { + if (index >= 0 && index < this.#keys.length) { + this.#keys.splice(index, 1) + this.#values.splice(index, 1) + return true + } + + return false + } + + /** + * Returns an array of computed entries in this index. + * @return {IndexIterator} + */ + entries () { + return this[Symbol.iterator]() + } + + /** + * @ignore + * @return {IndexIterator} + */ + [Symbol.iterator] () { + return new IndexIterator(this) + } +} + +/** + * A base class for a storage provider. + */ +export class Provider { + #id = state.id + #index = new Index() + #ready = new Deferred() + + /** + * @type {{ error: Error | null }} + */ + #state = { error: null } + + /** + * `Provider` class constructor. + */ + constructor () { + try { + const request = this.load() + + if (typeof request.then === 'function') { + request.then(() => this.#ready.resolve()) + } else { + queueMicrotask(() => this.#ready.resolve()) + } + + if (typeof request?.catch === 'function') { + request.catch((err) => { + this.#state.error = err + state.reportError(err) + }) + } + } catch (err) { + this.#state.error = err + state.reportError(err) + } + } + + /** + * An error currently associated with the provider, likely from an + * async operation. + * @type {Error?} + */ + get error () { + return this.#state.error + } + + /** + * A promise that resolves when the provider is ready. + * @type {Promise} + */ + get ready () { + return this.#ready.promise + } + + /** + * A reference the service worker storage ID, which is the service worker + * registration ID. + * @type {string} + * @throws DOMException + */ + get id () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + return this.#id + } + + /** + * A reference to the provider `Index` + * @type {Index} + * @throws DOMException + */ + get index () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + return this.#index + } + + /** + * The number of entries in the provider. + * @type {number} + * @throws DOMException + */ + get length () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + return this.#index.length + } + + /** + * Returns `true` if the provider has a value for a given `key`. + * @param {string} key} + * @return {boolean} + * @throws DOMException + */ + has (key) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + return this.#index.indexOf(key) >= 0 + } + + /** + * Get a value by `key`. + * @param {string} key + * @return {string?} + * @throws DOMException + */ + get (key) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + const index = this.#index.indexOf(key) + return this.#index.value(index) + } + + /** + * Sets a `value` by `key` + * @param {string} key + * @param {string} value + * @throws DOMException + */ + set (key, value) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + this.#index.insert(key, value) + } + + /** + * Removes a value by `key`. + * @param {string} key + * @return {boolean} + * @throws DOMException + */ + remove (key) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + const index = this.#index.indexOf(key) + return this.#index.remove(index) + } + + /** + * Clear all keys and values. + * @throws DOMException + */ + clear () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + this.#index.clear() + } + + /** + * The keys in the provider index. + * @return {string[]} + * @throws DOMException + */ + keys () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + return this.#index.keys + } + + /** + * The values in the provider index. + * @return {string[]} + * @throws DOMException + */ + values () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + return this.#index.values + } + + /** + * Returns the key at a given `index` + * @param {number} index + * @return {string|null} + * @throws DOMException + */ + key (index) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + return this.#index.keys[index] ?? null + } + + /** + * Loads the internal index with keys and values. + * @return {Promise} + */ + async load () { + // no-op + } +} + +/** + * An in-memory storage provider. It just used the built-in provider `Index` + * for storing key-value entries. + */ +export class MemoryStorageProvider extends Provider {} + +/** + * A session storage provider that persists for the runtime of the + * application and through service worker restarts. + */ +export class SessionStorageProvider extends Provider { + /** + * Get a value by `key`. + * @param {string} key + * @return {string?} + * @throws DOMException + * @throws NotFoundError + */ + get (key) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + if (super.has(key)) { + return super.get(key) + } + + const { id } = this + const result = ipc.sendSync('serviceWorker.storage.get', { id, key }) + + // @ts-ignore + if (result.err && result.err.code !== 'NOT_FOUND_ERR') { + throw result.err + } + + const value = result.data?.value ?? null + + if (value !== null) { + super.set(key, value) + } + + return value + } + + /** + * Sets a `value` by `key` + * @param {string} key + * @param {string} value + * @throws DOMException + * @throws NotFoundError + */ + set (key, value) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + value = String(value) + + const { id } = this + // send async + ipc.send('serviceWorker.storage.set', { id, key, value }) + return super.set(key, value) + } + + /** + * Remove a value by `key`. + * @param {string} key + * @return {string?} + * @throws DOMException + * @throws NotFoundError + */ + remove (key) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + const { id } = this + const result = ipc.sendSync('serviceWorker.storage.remove', { id, key }) + + if (result.err) { + throw result.err + } + + return super.remove(key) + } + + /** + * Clear all keys and values. + * @throws DOMException + * @throws NotFoundError + */ + clear () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + const { id } = this + const result = ipc.sendSync('serviceWorker.storage.clear', { id }) + + if (result.err) { + throw result.err + } + + return super.clear() + } + + /** + * Loads the internal index with keys and values. + * @return {Promise} + * @throws NotFoundError + */ + async load () { + const { id } = this + const result = await ipc.request('serviceWorker.storage', { id }) + + if (result.err) { + throw result.err + } + + if (result.data && typeof result.data === 'object') { + for (const key in result.data) { + const value = result.data[key] + this.index.insert(key, value) + } + } + + return await super.load() + } +} + +/** + * A local storage provider that persists until the data is cleared. + */ +export class LocalStorageProvider extends Provider { + /** + * Get a value by `key`. + * @param {string} key + * @return {string?} + * @throws DOMException + */ + get (key) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + // @ts-ignore + const { localStorage } = Environment.instance.context + + if (localStorage && typeof localStorage === 'object') { + if (key in localStorage) { + return localStorage[key] + } + } + + const value = super.get(key) + + if (value !== null) { + super.set(key, value) + } + + return value + } + + /** + * Sets a `value` by `key` + * @param {string} key + * @param {string} value + * @throws DOMException + */ + set (key, value) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + // @ts-ignore + const { localStorage } = Environment.instance.context + + if (localStorage) { + // @ts-ignore + Environment.instance.context.localStorage = { + ...localStorage, + [key]: String(value) + } + } + + return super.set(key, value) + } + + /** + * Remove a value by `key`. + * @param {string} key + * @return {boolean} + * @throws DOMException + */ + remove (key) { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + // @ts-ignore + const { localStorage } = Environment.instance.context + + if (localStorage && typeof localStorage === 'object') { + if (key in localStorage) { + delete localStorage[key] + // @ts-ignore + Environment.instance.context.localStorage = localStorage + } + } + + return super.remove(key) + } + + /** + * Clear all keys and values. + * @throws DOMException + */ + clear () { + if (this.error) { + throw Object.assign(new DOMException( + 'Storage provider in error state', 'InvalidStateError' + ), { cause: this.error }) + } + + // @ts-ignore + Environment.instance.context.localStorage = {} + return super.clear() + } + + /** + * Loads the internal index with keys and values. + * @return {Promise} + * @throws DOMException + */ + async load () { + if (!Environment.instance) { + throw Object.assign(new DOMException( + 'Storage provider is missing Environment', 'InvalidStateError' + ), { cause: this.error }) + } + + // ensure `Environment` is opened + await Environment.instance.open() + + // @ts-ignore + const localStorage = Environment.instance.context.localStorage || {} + + for (const key in localStorage) { + const value = localStorage[key] + this.index.insert(key, value) + } + } +} + +/** + * A generic interface for storage implementations + */ +export class Storage { + /** + * A factory for creating a `Storage` instance that is backed + * by a storage provider. Extending classes should define a `Provider` + * class that is statically available on the extended `Storage` class. + * @param {symbol} token + * @return {Promise<Proxy<Storage>>} + */ + static async create (token) { + // @ts-ignore + const { Provider } = this + + if (typeof Provider !== 'function') { + throw new TypeError('Storage implementation is missing Provider implementation') + } + + const provider = new Provider() + const instance = new this(token, provider) + + await provider.ready + + return new Proxy(instance, { + get (_, property) { + if (instance.hasItem(property)) { + return instance.getItem(property) + } + + if (property in instance) { + const value = instance[property] + if (typeof value === 'function') { + return value.bind(instance) + } + + return value + } + + return undefined + }, + + set (_, property, value) { + if (property in Storage.prototype) { + return false + } + + instance.setItem(property, value) + return true + }, + + has (_, property) { + return instance.hasItem(property) + }, + + deleteProperty (_, property) { + if (instance.hasItem(property)) { + instance.removeItem(property) + return true + } + + return false + }, + + getOwnPropertyDescriptor (_, property) { + if (instance.hasItem(property)) { + return { + configurable: true, + enumerable: true, + writable: true, + value: instance.getItem(property) + } + } + }, + + ownKeys (_) { + return Array.from(instance.provider.keys()) + } + }) + } + + #provider = null + + /** + * `Storage` class constructor. + * @ignore + * @param {symbol} token + * @param {Provider} provider + */ + constructor (token, provider) { + if (token !== createStorageSymbol) { + throw new IllegalConstructorError('Illegal constructor') + } + + this.#provider = provider + } + + /** + * A readonly reference to the storage provider. + * @type {Provider} + */ + get provider () { + return this.#provider + } + + /** + * The number of entries in the storage. + * @type {number} + */ + get length () { + return this.provider.length + } + + /** + * Returns `true` if the storage has a value for a given `key`. + * @param {string} key + * @return {boolean} + * @throws TypeError + */ + hasItem (key) { + if (arguments.length === 0) { + throw new TypeError('Not enough arguments') + } + + return this.provider.has(key) + } + + /** + * Clears the storage of all entries + */ + clear () { + this.provider.clear() + } + + /** + * Returns the key at a given `index` + * @param {number} index + * @return {string|null} + */ + key (index) { + if (arguments.length === 0) { + throw new TypeError('Not enough arguments') + } + + return this.provider.key(index) + } + + /** + * Get a storage value item for a given `key`. + * @param {string} key + * @return {string|null} + */ + getItem (key) { + if (arguments.length === 0) { + throw new TypeError('Not enough arguments') + } + + return this.provider.get(key) + } + + /** + * Removes a storage value entry for a given `key`. + * @param {string} + * @return {boolean} + */ + removeItem (key) { + if (arguments.length === 0) { + throw new TypeError('Not enough arguments') + } + + this.provider.remove(key) + } + + /** + * Sets a storage item `value` for a given `key`. + * @param {string} key + * @param {string} value + */ + setItem (key, value) { + if (arguments.length === 0) { + throw new TypeError('Not enough arguments') + } + + this.provider.set(key, value) + } + + /** + * @ignore + */ + get [Symbol.toStringTag] () { + return 'Storage' + } +} + +/** + * An in-memory `Storage` interface. + */ +export class MemoryStorage extends Storage { + static Provider = MemoryStorageProvider +} + +/** + * A locally persisted `Storage` interface. + */ +export class LocalStorage extends Storage { + static Provider = LocalStorageProvider +} + +/** + * A session `Storage` interface. + */ +export class SessionStorage extends Storage { + static Provider = SessionStorageProvider +} + +/** + * A factory for creating storage interfaces. + * @param {'memoryStorage'|'localStorage'|'sessionStorage'} type + * @return {Promise<Storage>} + */ +export async function createStorageInterface (type) { + if (type === 'memoryStorage') { + return await MemoryStorage.create(createStorageSymbol) + } else if (type === 'localStorage') { + return await LocalStorage.create(createStorageSymbol) + } else if (type === 'sessionStorage') { + return await SessionStorage.create(createStorageSymbol) + } + + throw new TypeError( + `Invalid 'Storage' interface type given: Received: ${type}` + ) +} + +export default { + Storage, + LocalStorage, + MemoryStorage, + SessionStorage, + createStorageInterface +} diff --git a/api/service-worker/worker.js b/api/service-worker/worker.js new file mode 100644 index 0000000000..69be8cfc94 --- /dev/null +++ b/api/service-worker/worker.js @@ -0,0 +1,497 @@ +/* eslint-disable import/first */ +globalThis.isServiceWorkerScope = true + +import { ServiceWorkerGlobalScope } from './global.js' +import { createStorageInterface } from './storage.js' +import { Module, createRequire } from '../module.js' +import { createCallSites } from '../internal/callsite.js' +import { STATUS_CODES } from '../http.js' +import { Notification } from '../notification.js' +import { Environment } from './env.js' +import { Deferred } from '../async.js' +import { Buffer } from '../buffer.js' +import { Cache } from '../commonjs/cache.js' +import globals from '../internal/globals.js' +import process from '../process.js' +import clients from './clients.js' +import debug from './debug.js' +import hooks from '../hooks.js' +import state from './state.js' +import path from '../path.js' +import util from '../util.js' +import ipc from '../ipc.js' + +import { + ExtendableMessageEvent, + NotificationEvent, + ExtendableEvent, + FetchEvent +} from './events.js' + +import '../console.js' + +Object.defineProperties( + globalThis, + Object.getOwnPropertyDescriptors(ServiceWorkerGlobalScope.prototype) +) + +export default null + +export const SERVICE_WORKER_READY_TOKEN = { __service_worker_ready: true } + +export const module = { exports: {} } +export const events = new Set() +export const stages = { register: null, install: null, activate: null } +// service worker life cycle stages +stages.register = new Deferred() +stages.install = new Deferred(stages.register) +stages.activate = new Deferred(stages.install) + +// event listeners +hooks.onReady(onReady) +globalThis.addEventListener('message', onMessage) + +// service worker globals +globals.register('ServiceWorker.state', state) +globals.register('ServiceWorker.stages', stages) +globals.register('ServiceWorker.events', events) +globals.register('ServiceWorker.module', module) + +let protocolData = null + +export function onReady () { + globalThis.postMessage(SERVICE_WORKER_READY_TOKEN) +} + +export async function onMessage (event) { + if (event instanceof ExtendableMessageEvent) { + return + } + + const { data } = event + + if (data?.register) { + event.stopImmediatePropagation() + + const { id, scope, scriptURL } = data.register + const url = new URL(scriptURL) + + if (!url.pathname.startsWith('/socket/')) { + // preload commonjs cache for user space server workers + Cache.restore(['loader.status', 'loader.response']) + } + + state.id = id + state.serviceWorker.id = id + state.serviceWorker.scope = scope + state.serviceWorker.scriptURL = scriptURL + + Module.main.addEventListener('error', (event) => { + if (event.error) { + debug(event.error) + } + }) + + Object.defineProperties(globalThis, { + require: { + configurable: false, + enumerable: false, + writable: false, + value: createRequire(scriptURL) + }, + + origin: { + configurable: false, + enumerable: true, + writable: false, + value: url.origin + }, + + __dirname: { + configurable: false, + enumerable: false, + writable: false, + value: path.dirname(url.pathname) + }, + + __filename: { + configurable: false, + enumerable: false, + writable: false, + value: url.pathname + }, + + module: { + configurable: false, + enumerable: false, + writable: false, + value: module + }, + + exports: { + configurable: false, + enumerable: false, + get: () => module.exports + }, + + process: { + configurable: false, + enumerable: false, + get: () => process + }, + + Buffer: { + configurable: false, + enumerable: false, + get: () => Buffer + }, + + global: { + configurable: false, + enumerable: false, + get: () => globalThis + } + }) + + // create global registration from construct + globalThis.registration = data.register + + try { + // define the actual location of the worker. not `blob:...` + globalThis.RUNTIME_WORKER_LOCATION = scriptURL + + // update and notify initial state change + state.serviceWorker.state = 'registering' + await state.notify('serviceWorker') + + // open envirnoment + await Environment.open({ id, scope }) + + // install storage interfaces + Object.defineProperties(globalThis, { + localStorage: { + configurable: false, + enumerable: false, + writable: false, + value: await createStorageInterface('localStorage') + }, + + sessionStorage: { + configurable: false, + enumerable: false, + writable: false, + value: await createStorageInterface('sessionStorage') + }, + + memoryStorage: { + configurable: false, + enumerable: false, + writable: false, + value: await createStorageInterface('memoryStorage') + } + }) + + // import module, which could be ESM, CommonJS, + // or a simple ServiceWorker + const result = await import(scriptURL) + + if (typeof module.exports === 'function') { + module.exports = { + default: module.exports + } + } else { + Object.assign(module.exports, result) + } + + state.serviceWorker.state = 'registered' + await state.notify('serviceWorker') + } catch (err) { + debug(err) + state.serviceWorker.state = 'error' + await state.notify('serviceWorker') + return + } + + if (module.exports.default && typeof module.exports.default === 'object') { + if (typeof module.exports.default.fetch === 'function') { + state.fetch = module.exports.default.fetch.bind(module.exports.default) + } + } else if (typeof module.exports.default === 'function') { + state.fetch = module.exports.default + } else if (typeof module.exports.fetch === 'function') { + state.fetch = module.exports.fetch.bind(module.exports) + } + + if (module.exports.default && typeof module.exports.default === 'object') { + if (typeof module.exports.default.install === 'function') { + state.install = module.exports.default.install.bind(module.exports.default) + } + } else if (typeof module.exports.install === 'function') { + state.install = module.exports.install.bind(module.exports) + } + + if (module.exports.default && typeof module.exports.default === 'object') { + if (typeof module.exports.default.activate === 'function') { + state.activate = module.exports.default.activate.bind(module.exports.default) + } + } else if (typeof module.exports.activate === 'function') { + state.activate = module.exports.activate.bind(module.exports) + } + + if (module.exports.default && typeof module.exports.default === 'object') { + if (typeof module.exports.default.reportError === 'function') { + state.reportError = module.exports.default.reportError.bind(module.exports.default) + } + } else if (typeof module.exports.reportError === 'function') { + state.reportError = module.reportError.bind(module.exports) + } + + if (typeof state.activate === 'function') { + globalThis.addEventListener('activate', async (event) => { + try { + const promise = state.activate(event.context.env, event.ontext) + event.waitUntil(promise) + await promise + } catch (err) { + debug(err) + } + }) + } + + if (typeof state.install === 'function') { + globalThis.addEventListener('install', async (event) => { + try { + const promise = state.install(event.context.env, event.context) + event.waitUntil(promise) + await promise + } catch (err) { + debug(err) + } + }) + } + + if (typeof state.fetch === 'function') { + if (!state.install) { + globalThis.addEventListener('install', () => { + globalThis.skipWaiting() + }) + } + + if (!state.activate) { + globalThis.addEventListener('activate', () => { + clients.claim() + }) + } + + globalThis.addEventListener('fetch', async (event) => { + const deferred = new Deferred() + let response = null + + event.respondWith(deferred.promise) + + try { + const promise = state.fetch( + event.request, + event.context.env, + event.context + ) + event.waitUntil(promise) + response = await promise + } catch (err) { + debug(err) + if (event.request.headers.get('accept') === 'application/json') { + const stack = createCallSites(err, err.stack) + response = Response.json({ name: err.name, message: err.message, stack }, { + statusText: err.message || STATUS_CODES[500], + status: 500, + headers: { + 'Runtime-Preload-Injection': 'disabled' + } + }) + } else { + response = new Response(util.inspect(err), { + statusText: err.message || STATUS_CODES[500], + status: 500, + headers: { + 'Runtime-Preload-Injection': 'disabled' + } + }) + } + } + + if (response) { + if (!response.statusText) { + response.statusText = STATUS_CODES[response.status] + } + + deferred.resolve(response) + } else { + deferred.resolve(new Response('Not Found', { + statusText: STATUS_CODES[404], + status: 404, + headers: { + 'Runtime-Preload-Injection': 'disabled' + } + })) + } + }) + } + + globalThis.postMessage({ __service_worker_registered: { id } }) + return stages.register.resolve() + } + + if (data?.unregister) { + event.stopImmediatePropagation() + state.serviceWorker.state = 'none' + await state.notify('serviceWorker') + globalThis.close() + return + } + + if (data?.install?.id === state.id) { + event.stopImmediatePropagation() + await stages.register + + const installEvent = new ExtendableEvent('install') + + events.add(installEvent) + state.serviceWorker.state = 'installing' + await state.notify('serviceWorker') + + globalThis.dispatchEvent(installEvent) + await installEvent.waitsFor() + + state.serviceWorker.state = 'installed' + await state.notify('serviceWorker') + events.delete(installEvent) + + return stages.install.resolve() + } + + if (data?.activate?.id === state.id) { + event.stopImmediatePropagation() + await stages.install + + const activateEvent = new ExtendableEvent('activate') + + events.add(activateEvent) + state.serviceWorker.state = 'activating' + await state.notify('serviceWorker') + + globalThis.dispatchEvent(activateEvent) + await activateEvent.waitsFor() + + state.serviceWorker.state = 'activated' + await state.notify('serviceWorker') + events.delete(activateEvent) + + return stages.activate.resolve() + } + + if (data?.fetch?.request) { + event.stopImmediatePropagation() + await stages.activate + + if (/post|put|patch|query/i.test(data.fetch.request.method)) { + const result = await ipc.request('serviceWorker.fetch.request.body', { + id: data.fetch.request.id + }, { responseType: 'arraybuffer' }) + + if (result.data) { + if (result.data instanceof ArrayBuffer) { + data.fetch.request.body = result.data + } else if (result.data instanceof Buffer) { + data.fetch.request.body = result.data.buffer + } else if (result.data.buffer) { + data.fetch.request.body = result.data.buffer + } else if (typeof result.data === 'object') { + data.fetch.request.body = JSON.stringify(result.data) + } else { + data.fetch.request.body = result.data + } + } + } + + if (data.fetch.request.body) { + data.fetch.request.body = new Uint8Array(data.fetch.request.body) + } + + const url = new URL(data.fetch.request.url) + const fetchEvent = new FetchEvent('fetch', { + clientId: data.fetch.client.id, + fetchId: data.fetch.request.id, + request: new Request(data.fetch.request.url, { + headers: new Headers(data.fetch.request.headers), + method: (data.fetch.request.method ?? 'GET').toUpperCase(), + body: data.fetch.request.body + }) + }) + + events.add(fetchEvent) + if (protocolData) { + fetchEvent.context.data = protocolData + } else if (url.protocol !== 'socket:' && url.protocol !== 'npm') { + const result = await ipc.request('protocol.getData', { + scheme: url.protocol.replace(':', '') + }) + + if (result.data !== null && result.data !== undefined) { + try { + fetchEvent.context.data = JSON.parse(result.data) + } catch { + fetchEvent.context.data = result.data + } + + protocolData = fetchEvent.context.data + } + } + + globalThis.dispatchEvent(fetchEvent) + await fetchEvent.waitsFor() + events.delete(fetchEvent) + return + } + + if (event.data?.notificationclick) { + event.stopImmediatePropagation() + globalThis.dispatchEvent(new NotificationEvent('notificationclick', { + action: event.data.notificationclick.action, + notification: new Notification( + event.data.notificationclick.title, + event.data.notificationclick.options, + event.data.notificationclick.data + ) + })) + return + } + + if (event.data?.notificationclose) { + event.stopImmediatePropagation() + globalThis.dispatchEvent(new NotificationEvent('notificationclose', { + action: event.data.notificationclose.action, + notification: new Notification( + event.data.notificationclose.title, + event.data.notificationclose.options, + event.data.notificationclose.data + ) + })) + return + } + + if ( + typeof event.data?.from === 'string' && + event.data.message && + event.data.client + ) { + event.stopImmediatePropagation() + globalThis.dispatchEvent(new ExtendableMessageEvent('message', { + source: await clients.get(event.data.client.id), + origin: event.data.client.origin, + ports: event.ports, + data: event.data.message + })) + // eslint-disable-next-line + return + } +} diff --git a/api/shared-worker.js b/api/shared-worker.js new file mode 100644 index 0000000000..d89b46d1ee --- /dev/null +++ b/api/shared-worker.js @@ -0,0 +1,16 @@ +import { SharedWorker } from './shared-worker/index.js' +import { Environment } from './shared-worker/env.js' + +/** + * A reference to the opened environment. This value is an instance of an + * `Environment` if the scope is a ServiceWorker scope. + * @type {Environment|null} + */ +export const env = Environment.instance + +export { + Environment, + SharedWorker +} + +export default SharedWorker diff --git a/api/shared-worker/debug.js b/api/shared-worker/debug.js new file mode 100644 index 0000000000..5acf272776 --- /dev/null +++ b/api/shared-worker/debug.js @@ -0,0 +1,28 @@ +import globals from '../internal/globals.js' +import util from '../util.js' + +export function debug (...args) { + const state = globals.get('SharedWorker.state') + + if (process.env.SOCKET_RUNTIME_SHARED_WORKER_DEBUG) { + console.debug(...args) + } + + if (args[0] instanceof Error) { + globalThis.postMessage({ + __shared_worker_debug: [ + `[${state.sharedWorker.scriptURL}]: ${util.format(...args)}` + ] + }) + + if (typeof state?.reportError === 'function') { + state.reportError(args[0]) + } else if (typeof globalThis.reportError === 'function') { + globalThis.reportError(args[0]) + } + } else { + globalThis.postMessage({ __shared_worker_debug: [util.format(...args)] }) + } +} + +export default debug diff --git a/api/shared-worker/global.js b/api/shared-worker/global.js new file mode 100644 index 0000000000..14f6f53f38 --- /dev/null +++ b/api/shared-worker/global.js @@ -0,0 +1,27 @@ +// events +let onconnect = null + +export class SharedWorkerGlobalScope { + get isSharedWorkerScope () { + return true + } + + get onconnect () { + return onconnect + } + + set onconnect (listener) { + if (onconnect) { + globalThis.removeEventListener('connect', onconnect) + } + + onconnect = null + + if (typeof listener === 'function') { + globalThis.addEventListener('connect', listener) + onconnect = listener + } + } +} + +export default SharedWorkerGlobalScope.prototype diff --git a/api/shared-worker/index.html b/api/shared-worker/index.html new file mode 100644 index 0000000000..dba4d4fc0f --- /dev/null +++ b/api/shared-worker/index.html @@ -0,0 +1,145 @@ +<!doctype html> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content=" + connect-src socket: https: http: blob: ipc: wss: ws: ws://localhost:* {{protocol_handlers}}; + script-src socket: https: http: blob: http://localhost:* 'unsafe-eval' 'unsafe-inline' {{protocol_handlers}}; + worker-src socket: https: http: blob: 'unsafe-eval' 'unsafe-inline' {{protocol_handlers}}; + frame-src socket: https: http: blob: http://localhost:*; + img-src socket: https: http: blob: http://localhost:*; + child-src socket: https: http: blob:; + object-src 'none'; + " + > + <script charset="utf-8" type="text/javascript"> + Object.defineProperty(globalThis, '__RUNTIME_SHARED_WORKER_CONTEXT__', { + configurable: false, + enumerable: false, + writable: false, + value: true + }) + </script> + <script type="module"> + import application from 'socket:application' + import process from 'socket:process' + + Object.assign(globalThis, { + async openExternalLink (href) { + const currentWindow = await application.getCurrentWindow() + await currentWindow.openExternal(href) + } + }) + + document.title = `Shared Worker Debugger - v${process.version}` + </script> + <script type="module" src="./init.js"></script> + <style type="text/css" media="all"> + * { + box-sizing: border-box; + } + + body { + background: rgba(40, 40, 40, 1); + color: rgba(255, 255, 255, 1); + font-family: 'Inter-Light', sans-serif; + font-size: 14px; + margin: 0; + position: absolute; top: 0px; left: 0; right:0; bottom: 0px; + } + + a { + color: inherit; + text-decoration: none; + transition: all 0.2s ease; + + &.with-hover:hover { + border-bottom: 2px solid rgba(255, 255, 255, 1); + } + } + + p { + background: rgba(14, 85, 152, .25); + border-radius: 2px; + display: inline-block; + font: 12px/26px 'Inter-Light', sans-serif; + margin: 0; + text-align: center; + width: 100%; + + &.message { + display: block; + overflow: hidden; + padding: 0 8px; + position: relative; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } + } + + pre#log { + background: rgba(0, 0, 0, 1); + overflow-y: scroll; + padding: 16px; + margin: 0; + position: absolute; top: 0px; left: 0; right:0; bottom: 0px; + + & span { + opacity: 0.8; + transition: all 0.2s ease; + + &:hover { + opacity: 1; + transition: all 0.05s ease; + } + + & code { + color: rgb(215, 215, 215); + display: block; + font-size: 12px; + line-break: anywhere; + margin-bottom: 8px; + opacity: 0.8; + white-space: wrap; + transition: all 0.1s ease; + + &:hover { + color: rgb(225, 225, 225); + opacity: 1; + transition: all 0.025s ease; + } + + & span { + &.red { + color: red; + } + } + } + + & details { + &[open] span { + opacity: 1; + transition: all 0.05s ease; + & code { + color: rgb(225, 225, 225); + opacity: 1; + transition: all 0.025s ease; + } + } + + & > summary { + cursor: pointer; + list-style: none; + } + } + } + } + </style> + </head> + <body> + <pre id="log"></pre> + </body> +</html> diff --git a/api/shared-worker/index.js b/api/shared-worker/index.js new file mode 100644 index 0000000000..5683cf5d8e --- /dev/null +++ b/api/shared-worker/index.js @@ -0,0 +1,191 @@ +/* global XMLHttpRequest, ErrorEvent */ +import application from '../application.js' +import location from '../location.js' +import crypto from '../crypto.js' +import client from '../application/client.js' +import ipc from '../ipc.js' + +let contextWindow = null + +export const SHARED_WORKER_WINDOW_INDEX = 46 +export const SHARED_WORKER_WINDOW_TITLE = 'socket:shared-worker' +export const SHARED_WORKER_WINDOW_PATH = '/socket/shared-worker/index.html' + +export const channel = new BroadcastChannel('socket.runtime.sharedWorker') +export const workers = new Map() + +channel.addEventListener('message', (event) => { + if (event.data?.error?.id) { + const ref = workers.get(event.data.error.id) + if (ref) { + const worker = ref.deref() + if (!worker) { + workers.delete(event.data.error.id) + } else { + worker.dispatchEvent(new ErrorEvent('error', { + error: new Error(event.data.error.message) || '' + })) + } + } + } +}) + +export async function init (sharedWorker, options) { + await getContextWindow() + channel.postMessage({ + connect: { + scriptURL: options.scriptURL, + client: client.toJSON(), + name: options.name, + port: sharedWorker.port, + id: sharedWorker.id + } + }) + + workers.set(sharedWorker.id, new WeakRef(sharedWorker)) +} + +export class SharedWorkerMessagePort extends ipc.IPCMessagePort { + [Symbol.for('socket.runtime.serialize')] () { + return { + ...(super[Symbol.for('socket.runtime.serialize')]()), + __type__: 'SharedWorkerMessagePort' + } + } +} + +export class SharedWorker extends EventTarget { + #id = null + #port = null + #ready = null + #onerror = null + + /** + * `SharedWorker` class constructor. + * @param {string|URL|Blob} aURL + * @param {string|object=} [nameOrOptions] + */ + constructor (aURL, nameOrOptions = null) { + if (typeof aURL === 'string' && !URL.canParse(aURL, location.href)) { + const blob = new Blob([aURL], { type: 'text/javascript' }) + aURL = URL.createObjectURL(blob).toString() + } else if (String(aURL).startsWith('blob:')) { + const request = new XMLHttpRequest() + request.open('GET', String(aURL), false) + request.send() + const blob = new Blob([request.responseText || request.response], { type: 'application/javascript' }) + aURL = URL.createObjectURL(blob) + } + + const url = new URL(aURL, location.origin) + const id = crypto.murmur3(url.origin + url.pathname) + + super(url.toString(), nameOrOptions) + + this.#id = id + this.#port = SharedWorkerMessagePort.create({ id }) + this.#ready = init(this, { + scriptURL: url.toString(), + name: typeof nameOrOptions === 'string' + ? nameOrOptions + : typeof nameOrOptions?.name === 'string' + ? nameOrOptions.name + : null + }) + } + + get onerror () { + return this.#onerror + } + + set onerror (onerror) { + if (typeof this.#onerror === 'function') { + this.removeEventListener('error', this.#onerror) + } + + this.#onerror = null + + if (typeof onerror === 'function') { + this.#onerror = onerror + this.addEventListener('error', onerror) + } + } + + get ready () { + return this.#ready + } + + get port () { + return this.#port + } + + get id () { + return this.#id + } +} + +/** + * Gets the SharedWorker context window. + * This function will create it if it does not already exist. + * @return {Promise<import('./window.js').ApplicationWindow} + */ +export async function getContextWindow () { + if (contextWindow) { + await contextWindow.ready + return contextWindow + } + + const existingContextWindow = await application.getWindow( + SHARED_WORKER_WINDOW_INDEX, + { max: false } + ) + + const pendingContextWindow = ( + existingContextWindow ?? + application.createWindow({ + canExit: false, + headless: !process.env.SOCKET_RUNTIME_SHARED_WORKER_DEBUG, + // @ts-ignore + debug: Boolean(process.env.SOCKET_RUNTIME_SHARED_WORKER_DEBUG), + index: SHARED_WORKER_WINDOW_INDEX, + title: SHARED_WORKER_WINDOW_TITLE, + path: SHARED_WORKER_WINDOW_PATH, + config: { + webview_watch_reload: false + } + }).catch(() => application.getWindow(SHARED_WORKER_WINDOW_INDEX, { + max: false + })) + ) + + const promises = [ + Promise.resolve(pendingContextWindow) + ] + + if (!existingContextWindow) { + promises.push(new Promise((resolve) => { + const timeout = setTimeout(resolve, 500) + channel.addEventListener('message', function onMessage (event) { + if (event.data?.ready === SHARED_WORKER_WINDOW_INDEX) { + clearTimeout(timeout) + resolve(null) + channel.removeEventListener('message', onMessage) + } + }) + })) + } + + const ready = Promise.all(promises) + contextWindow = pendingContextWindow + contextWindow.ready = ready + + await ready + contextWindow = await pendingContextWindow + contextWindow.ready = ready + + await contextWindow.hide() + + return contextWindow +} + +export default SharedWorker diff --git a/api/shared-worker/init.js b/api/shared-worker/init.js new file mode 100644 index 0000000000..847b27cd03 --- /dev/null +++ b/api/shared-worker/init.js @@ -0,0 +1,163 @@ +/* global Worker */ +import { channel } from './index.js' +import globals from '../internal/globals.js' +import crypto from '../crypto.js' + +export const workers = new Map() +export { channel } + +globals.register('SharedWorkerContext.workers', workers) +globals.register('SharedWorkerContext.info', new Map()) + +channel.addEventListener('message', (event) => { + if (event.data?.connect) { + onConnect(event) + } +}) + +export class SharedWorkerInstance extends Worker { + #info = null + + constructor (filename, options) { + super(filename, { + name: `SharedWorker (${options?.info?.pathname ?? filename})`, + ...options, + [Symbol.for('socket.runtime.internal.worker.type')]: 'sharedWorker' + }) + + this.#info = options?.info ?? null + this.addEventListener('message', this.onMessage.bind(this)) + } + + get info () { + return this.#info + } + + async onMessage (event) { + if (Array.isArray(event.data.__shared_worker_debug)) { + const log = document.querySelector('#log') + if (log) { + for (const entry of event.data.__shared_worker_debug) { + const lines = entry.split('\n') + const span = document.createElement('span') + let target = span + + for (const line of lines) { + if (!line) continue + const item = document.createElement('code') + item.innerHTML = line + .replace(/\s/g, ' ') + .replace(/\\s/g, ' ') + .replace(/<anonymous>/g, '<anonymous>') + .replace(/([a-z|A-Z|_|0-9]+(Error|Exception)):/g, '<span class="red"><b>$1</b>:</span>') + + if (target === span && lines.length > 1) { + target = document.createElement('details') + const summary = document.createElement('summary') + summary.appendChild(item) + target.appendChild(summary) + span.appendChild(target) + } else { + target.appendChild(item) + } + } + + log.appendChild(span) + } + + log.scrollTop = log.scrollHeight + } + } + } +} + +export class SharedWorkerInfo { + id = null + port = null + client = null + scriptURL = null + + url = null + hash = null + + constructor (data) { + for (const key in data) { + const value = data[key] + if (key in this) { + this[key] = value + } + } + + const url = new URL(this.scriptURL) + this.url = url.toString() + this.hash = crypto.murmur3(url.toString()) + } + + get pathname () { + return new URL(this.url).pathname + } +} + +export async function onInstall (event) { + const info = new SharedWorkerInfo(event.data.install) + + if (!info.id || workers.has(info.hash)) { + return + } + + const worker = new SharedWorkerInstance('./worker.js', { + info + }) + + workers.set(info.hash, worker) + globals.get('SharedWorkerContext.info').set(info.hash, info) + worker.postMessage({ install: info }) +} + +export async function onUninstall (event) { + const info = new SharedWorkerInfo(event.data.uninstall) + + if (!workers.has(info.hash)) { + return + } + + const worker = workers.get(info.hash) + workers.delete(info.hash) + + worker.postMessage({ uninstall: info }) +} + +export async function onConnect (event) { + const info = new SharedWorkerInfo(event.data.connect) + + if (!info.id) { + return + } + + if (!workers.has(info.hash)) { + onInstall(new MessageEvent('message', { + data: { + install: event.data.connect + } + })) + + try { + await new Promise((resolve, reject) => { + channel.addEventListener('message', (event) => { + if (event.data?.error?.id === info.id) { + reject(new Error(event.data.error.message)) + } else if (event.data?.installed?.id === info.id) { + resolve() + } + }) + }) + } catch (err) { + console.error(err) + } + } + + const worker = workers.get(info.hash) + worker.postMessage({ connect: info }) +} + +export default null diff --git a/api/shared-worker/state.js b/api/shared-worker/state.js new file mode 100644 index 0000000000..9228c74396 --- /dev/null +++ b/api/shared-worker/state.js @@ -0,0 +1,66 @@ +const descriptors = { + channel: { + configurable: false, + enumerable: false, + value: null + }, + + sharedWorker: { + configurable: false, + enumerable: true, + value: Object.create(null, { + scriptURL: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + state: { + configurable: false, + enumerable: true, + writable: true, + value: 'parsed' + }, + + id: { + configurable: false, + enumerable: true, + writable: true, + value: null + } + }) + }, + + id: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + connect: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + env: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + + reportError: { + configurable: false, + enumerable: false, + writable: true, + value: globalThis.reportError.bind(globalThis) + } +} + +export const state = Object.create(null, descriptors) + +export default state diff --git a/api/shared-worker/worker.js b/api/shared-worker/worker.js new file mode 100644 index 0000000000..ffd4f78d1d --- /dev/null +++ b/api/shared-worker/worker.js @@ -0,0 +1,236 @@ +/* eslint-disable import/first */ +globalThis.isSharedWorkerScope = true + +import { SharedWorkerMessagePort, channel } from './index.js' +import { SharedWorkerGlobalScope } from './global.js' +import { Module, createRequire } from '../module.js' +import { Environment } from '../service-worker/env.js' +import { Buffer } from '../buffer.js' +import { Cache } from '../commonjs/cache.js' +import globals from '../internal/globals.js' +import process from '../process.js' +import debug from './debug.js' +import hooks from '../hooks.js' +import state from './state.js' +import path from '../path.js' +import ipc from '../ipc.js' + +import '../console.js' + +export default null + +Object.defineProperties( + globalThis, + Object.getOwnPropertyDescriptors(SharedWorkerGlobalScope.prototype) +) + +export const SHARED_WORKER_READY_TOKEN = { __shared_worker_ready: true } + +// state +export const module = { exports: {} } +export const connections = new Set() + +// event listeners +hooks.onReady(onReady) +globalThis.addEventListener('message', onMessage) + +// shared worker globals +globals.register('SharedWorker.state', state) +globals.register('SharedWorker.module', module) + +export function onReady () { + globalThis.postMessage(SHARED_WORKER_READY_TOKEN) +} + +export async function onMessage (event) { + const { data } = event + + if (data?.install) { + event.stopImmediatePropagation() + + const { id, scriptURL } = data.install + const url = new URL(scriptURL) + + if (!url.pathname.startsWith('/socket/')) { + // preload commonjs cache for user space server workers + Cache.restore(['loader.status', 'loader.response']) + } + + state.id = id + state.sharedWorker.id = id + state.sharedWorker.scriptURL = scriptURL + + Module.main.addEventListener('error', (event) => { + if (event.error) { + debug(event.error) + } + }) + + Object.defineProperties(globalThis, { + require: { + configurable: false, + enumerable: false, + writable: false, + value: createRequire(scriptURL) + }, + + origin: { + configurable: false, + enumerable: true, + writable: false, + value: url.origin + }, + + __dirname: { + configurable: false, + enumerable: false, + writable: false, + value: path.dirname(url.pathname) + }, + + __filename: { + configurable: false, + enumerable: false, + writable: false, + value: url.pathname + }, + + module: { + configurable: false, + enumerable: false, + writable: false, + value: module + }, + + exports: { + configurable: false, + enumerable: false, + get: () => module.exports + }, + + process: { + configurable: false, + enumerable: false, + get: () => process + }, + + Buffer: { + configurable: false, + enumerable: false, + get: () => Buffer + }, + + global: { + configurable: false, + enumerable: false, + get: () => globalThis + } + }) + + try { + // define the actual location of the worker. not `blob:...` + globalThis.RUNTIME_WORKER_LOCATION = scriptURL + // update state + state.sharedWorker.state = 'installing' + // open envirnoment + state.env = await Environment.open({ type: 'sharedWorker', id }) + // import module, which could be ESM, CommonJS, + // or a simple SharedWorker + const result = await import(scriptURL) + + if (typeof module.exports === 'function') { + module.exports = { + default: module.exports + } + } else { + Object.assign(module.exports, result) + } + // update state + state.sharedWorker.state = 'installed' + } catch (err) { + debug(err) + state.sharedWorker.state = 'error' + channel.postMessage({ + error: { id: state.id, message: err.message } + }) + return + } + + if (module.exports.default && typeof module.exports.default === 'object') { + if (typeof module.exports.default.connect === 'function') { + state.connect = module.exports.default.connect.bind(module.exports.default) + } + } else if (typeof module.exports.connect === 'function') { + state.connect = module.exports.connect.bind(module.exports) + } + + if (module.exports.default && typeof module.exports.default === 'object') { + if (typeof module.exports.default.reportError === 'function') { + state.reportError = module.exports.default.reportError.bind(module.exports.default) + } + } else if (typeof module.exports.reportError === 'function') { + state.reportError = module.reportError.bind(module.exports) + } + + if (typeof state.connect === 'function') { + globalThis.addEventListener('connect', async (event) => { + try { + const promise = state.connect(state.env, event.data, event.ports[0]) + await promise + } catch (err) { + debug(err) + channel.postMessage({ + error: { id: state.id, message: err.message } + }) + } + }) + } + + channel.postMessage({ installed: { id: state.id } }) + debug( + '[%s]: SharedWorker (%s) installed', + new URL(scriptURL).pathname.replace(/^\/socket\//, 'socket:'), + state.id + ) + return + } + + if (data?.connect) { + event.stopImmediatePropagation() + const connection = ipc.inflateIPCMessageTransfers(event.data.connect, new Map(Object.entries({ + SharedWorkerMessagePort + }))) + + for (const entry of connections) { + if (entry.id === connection.port.id) { + entry.close(false) + connections.delete(entry) + break + } + } + + connections.add(connection.port) + const connectEvent = new MessageEvent('connect') + Object.defineProperty(connectEvent, 'ports', { + configurable: false, + writable: false, + value: Object.seal(Object.freeze([connection.port])) + }) + + globalThis.dispatchEvent(connectEvent) + + debug( + '[%s]: SharedWorker (%s) connection from client (%s/%s) at %s', + new URL(state.sharedWorker.scriptURL).pathname.replace(/^\/socket\//, 'socket:'), + state.id, + connection.client.id, + [ + connection.client.frameType.replace('none', ''), + connection.client.type + ].filter(Boolean).join('-'), + connection.client.location + ) + // eslint-disable-next-line + return + } +} diff --git a/api/signal.js b/api/signal.js new file mode 100644 index 0000000000..b9861c719b --- /dev/null +++ b/api/signal.js @@ -0,0 +1,15 @@ +/** + * @module signal + * @deprecated Use `socket:process/signal` instead. + */ + +import signal from './process/signal.js' +export * from './process/signal.js' +export default signal + +// XXX(@jwerle): we should probably use a standard `deprecated()` function +// like nodejs' `util.deprecate()` instead of `console.warn()` +console.warn( + 'The module "socket:signal" is deprecated. ' + + 'Please use "socket:process/signal" instead"' +) diff --git a/api/stream-relay.js b/api/stream-relay.js deleted file mode 100644 index 0599d2fd31..0000000000 --- a/api/stream-relay.js +++ /dev/null @@ -1,3 +0,0 @@ -import def from './stream-relay/index.js' -export * from './stream-relay/index.js' -export default def diff --git a/api/stream-relay/index.js b/api/stream-relay/index.js deleted file mode 100644 index df8ffdb286..0000000000 --- a/api/stream-relay/index.js +++ /dev/null @@ -1,2086 +0,0 @@ -/** - * @module stream-relay - * @status Experimental - * - * This module provides primitives for constructing a distributed network - * for relaying messages between peers. Peers can be addressed by their unique - * peer ID and can also be directly connected to by their IP address and port. - * - * Note: The code in the module may change a lot in the next few weeks but the - * API will continue to be backward compatible thoughout all future releases. - */ - -import { isBufferLike } from '../util.js' -import { Buffer } from '../buffer.js' -import { sodium, randomBytes } from '../crypto.js' - -import { Encryption } from './encryption.js' -import { Cache } from './cache.js' -import * as NAT from './nat.js' - -import { - Packet, - PacketPing, - PacketPong, - PacketIntro, - PacketPublish, - PacketStream, - PacketSync, - PacketJoin, - PacketQuery, - sha256, - VERSION -} from './packets.js' - -let logcount = 0 -const process = globalThis.process || window.__args -const COLOR_GRAY = '\x1b[90m' -const COLOR_WHITE = '\x1b[37m' -const COLOR_RESET = '\x1b[0m' - -export const debug = (pid, ...args) => { - if (!process.env.DEBUG) return - - const output = COLOR_GRAY + - String(logcount++).padStart(6) + ' │ ' + COLOR_WHITE + - pid.slice(0, 4) + ' ' + args.join(' ') + COLOR_RESET - - if (new RegExp(process.env.DEBUG).test(output)) console.log(output) -} - -export { Packet, sha256, Cache, Encryption, NAT } - -/** - * Retry delay in milliseconds for ping. - * @type {number} - */ -export const PING_RETRY = 500 - -/** - * Probe wait timeout in milliseconds. - * @type {number} - */ -export const PROBE_WAIT = 512 - -/** - * Default keep alive timeout. - * @type {number} - */ -export const DEFAULT_KEEP_ALIVE = 30_000 - -/** - * Default rate limit threshold in milliseconds. - * @type {number} - */ -export const DEFAULT_RATE_LIMIT_THRESHOLD = 8000 - -const PRIV_PORTS = 1024 -const MAX_PORTS = 65535 - PRIV_PORTS -const MAX_BANDWIDTH = 1024 * 32 - -const PEERID_REGEX = /^([A-Fa-f0-9]{2}){32}$/ - -/** - * Port generator factory function. - * @param {object} ports - the cache to use (a set) - * @param {number?} p - initial port - * @return {number} - */ -export const getRandomPort = (ports = new Set(), p) => { - do { - p = Math.max(1024, Math.ceil(Math.random() * 0xffff)) - } while (ports.has(p) && ports.size < MAX_PORTS) - - ports.add(p) - return p -} - -const isReplicatable = type => ( - type === PacketPublish.type || - type === PacketJoin.type -) - -/** - * Computes rate limit predicate value for a port and address pair for a given - * threshold updating an input rates map. This method is accessed concurrently, - * the rates object makes operations atomic to avoid race conditions. - * - * @param {Map} rates - * @param {number} type - * @param {number} port - * @param {string} address - * @return {boolean} - */ -export function rateLimit (rates, type, port, address, subclusterIdQuota) { - const R = isReplicatable(type) - const key = (R ? 'R' : 'C') + ':' + address + ':' + port - const quota = subclusterIdQuota || (R ? 512 : 4096) - const time = Math.floor(Date.now() / 60000) - const rate = rates.get(key) || { time, quota, used: 0 } - - rate.mtime = Date.now() // checked by mainLoop for garabge collection - - if (time !== rate.time) { - rate.time = time - if (rate.used > rate.quota) rate.quota -= 1 - else if (rate.used < quota) rate.quota += 1 - rate.used = 0 - } - - rate.used += 1 - - rates.set(key, rate) - if (rate.used >= rate.quota) return true -} - -/** - * A `RemotePeer` represents an initial, discovered, or connected remote peer. - * Typically, you will not need to create instances of this class directly. - */ -export class RemotePeer { - peerId = null - address = null - port = 0 - natType = null - clusters = {} - pingId = null - distance = 0 - connected = false - opening = 0 - probed = 0 - proxy = null - clock = 0 - uptime = 0 - lastUpdate = 0 - lastRequest = 0 - localPeer = null - - /** - * `RemotePeer` class constructor. - * @param {{ - * peerId?: string, - * address?: string, - * port?: number, - * natType?: number, - * clusters: object, - * reflectionId?: string, - * distance?: number, - * publicKey?: string, - * privateKey?: string, - * clock?: number, - * lastUpdate?: number, - * lastRequest?: number - * }} o - */ - constructor (o, peer) { - this.localPeer = peer - - if (!o.peerId) throw new Error('expected .peerId') - if (o.indexed) o.natType = NAT.UNRESTRICTED - if (o.natType && !NAT.isValid(o.natType)) throw new Error('invalid .natType') - - const cid = o.clusterId?.toString('base64') - const scid = o.subclusterId?.toString('base64') - - if (cid && scid) { - this.clusters[cid] = { [scid]: { rateLimit: MAX_BANDWIDTH } } - } - - Object.assign(this, o) - } - - async write (sharedKey, args) { - let rinfo = this - if (this.proxy) rinfo = this.proxy - - const keys = await Encryption.createKeyPair(sharedKey) - - args.subclusterId = Buffer.from(keys.publicKey) - args.clusterId = Buffer.from(this.localPeer.clusterId, 'base64') - args.usr3 = Buffer.from(this.peerId, 'hex') - args.usr4 = Buffer.from(this.localPeer.peerId, 'hex') - args.message = this.localPeer.encryption.seal(args.message, keys) - - const packets = await this.localPeer._message2packets(PacketStream, args.message, args) - - if (this.proxy) { - debug(this.localPeer.peerId, `>> WRITE STREAM HAS PROXY ${this.proxy.address}:${this.proxy.port}`) - } - - for (const packet of packets) { - const from = this.localPeer.peerId.slice(0, 6) - const to = this.peerId.slice(0, 6) - debug(this.localPeer.peerId, `>> WRITE STREAM (from=${from}, to=${to}, via=${rinfo.address}:${rinfo.port})`) - - this.localPeer.gate.set(Buffer.from(packet.packetId).toString('hex'), 1) - await this.localPeer.send(await Packet.encode(packet), rinfo.port, rinfo.address, this.socket) - } - - return packets - } -} - -/** - * `Peer` class factory. - * @param {{ createSocket: function('udp4', null, object?): object }} options - */ -export const wrap = dgram => { - class Peer { - port = null - address = null - natType = NAT.UNKNOWN - nextNatType = NAT.UNKNOWN - clusters = {} - reflectionId = null - reflectionTimeout = null - reflectionStage = 0 - reflectionRetry = 1 - reflectionFirstResponder = null - peerId = '' - isListening = false - ctime = Date.now() - lastUpdate = 0 - lastSync = 0 - closing = false - clock = 0 - unpublished = {} - cache = null - uptime = 0 - maxHops = 16 - bdpCache = /** @type {number[]} */ ([]) - - onListening = null - onDelete = null - - sendQueue = [] - firewall = null - rates = new Map() - streamBuffer = new Map() - gate = new Map() - returnRoutes = new Map() - - metrics = { - i: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, REJECTED: 0 }, - o: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0 } - } - - peers = JSON.parse(/* snapshot_start=1691579150299, filter=easy,static */` - [{"address":"44.213.42.133","port":10885,"peerId":"4825fe0475c44bc0222e76c5fa7cf4759cd5ef8c66258c039653f06d329a9af5","natType":31,"indexed":true},{"address":"107.20.123.15","port":31503,"peerId":"2de8ac51f820a5b9dc8a3d2c0f27ccc6e12a418c9674272a10daaa609eab0b41","natType":31,"indexed":true},{"address":"54.227.171.107","port":43883,"peerId":"7aa3d21ceb527533489af3888ea6d73d26771f30419578e85fba197b15b3d18d","natType":31,"indexed":true},{"address":"54.157.134.116","port":34420,"peerId":"1d2315f6f16e5f560b75fbfaf274cad28c12eb54bb921f32cf93087d926f05a9","natType":31,"indexed":true},{"address":"184.169.205.9","port":52489,"peerId":"db00d46e23d99befe42beb32da65ac3343a1579da32c3f6f89f707d5f71bb052","natType":31,"indexed":true},{"address":"35.158.123.13","port":31501,"peerId":"4ba1d23266a2d2833a3275c1d6e6f7ce4b8657e2f1b8be11f6caf53d0955db88","natType":31,"indexed":true},{"address":"3.68.89.3","port":22787,"peerId":"448b083bd8a495ce684d5837359ce69d0ff8a5a844efe18583ab000c99d3a0ff","natType":31,"indexed":true},{"address":"3.76.100.161","port":25761,"peerId":"07bffa90d89bf74e06ff7f83938b90acb1a1c5ce718d1f07854c48c6c12cee49","natType":31,"indexed":true},{"address":"3.70.241.230","port":61926,"peerId":"1d7ee8d965794ee286ac425d060bab27698a1de92986dc6f4028300895c6aa5c","natType":31,"indexed":true},{"address":"3.70.160.181","port":41141,"peerId":"707c07171ac9371b2f1de23e78dad15d29b56d47abed5e5a187944ed55fc8483","natType":31,"indexed":true},{"address":"3.122.250.236","port":64236,"peerId":"a830615090d5cdc3698559764e853965a0d27baad0e3757568e6c7362bc6a12a","natType":31,"indexed":true},{"address":"18.130.98.23","port":25111,"peerId":"ba483c1477ab7a99de2d9b60358d9641ff6a6dc6ef4e3d3e1fc069b19ac89da4","natType":31,"indexed":true},{"address":"13.42.10.247","port":2807,"peerId":"032b79de5b4581ee39c6d15b12908171229a8eb1017cf68fd356af6bbbc21892","natType":31,"indexed":true},{"address":"18.229.140.216","port":36056,"peerId":"73d726c04c05fb3a8a5382e7a4d7af41b4e1661aadf9020545f23781fefe3527","natType":31,"indexed":true}] - `/* snapshot_end=1691579150299 */).map((/** @type {object} */ o) => new RemotePeer({ ...o, indexed: true }, this)) - - /** - * `Peer` class constructor. Avoid calling this directly (use the create method). - * @private - * @param {object?} [persistedState] - */ - constructor (persistedState = {}) { - const config = persistedState?.config ?? persistedState ?? {} - - this.encryption = new Encryption() - - if (!config.peerId) throw new Error('constructor expected .peerId') - if (typeof config.peerId !== 'string' || !PEERID_REGEX.test(config.peerId)) throw new Error('invalid .peerId') - - this.config = { - keepalive: DEFAULT_KEEP_ALIVE, - ...config - } - - let cacheData - - if (persistedState?.data?.length > 0) { - cacheData = new Map(persistedState.data) - } - - this.cache = new Cache(cacheData, config.siblingResolver) - this.cache.onEjected = p => this.mcast(p) - - this.unpublished = persistedState?.unpublished || {} - this._onError = err => this.onError && this.onError(err) - - Object.assign(this, config) - - if (!this.indexed && !this.clusterId) throw new Error('constructor expected .clusterId') - if (typeof this.peerId !== 'string') throw new Error('peerId should be of type string') - - this.port = config.port || null - this.natType = config.natType || null - this.address = config.address || null - - this.socket = dgram.createSocket('udp4', null, this) - this.probeSocket = dgram.createSocket('udp4', null, this).unref() - - const isRecoverable = err => - err.code === 'ECONNRESET' || - err.code === 'ECONNREFUSED' || - err.code === 'EADDRINUSE' || - err.code === 'ETIMEDOUT' - - this.socket.on('error', err => isRecoverable(err) && this._listen()) - this.probeSocket.on('error', err => isRecoverable(err) && this._listen()) - } - - /** - * An implementation for clearning an interval that can be overridden by the test suite - * @param Number the number that identifies the timer - * @return {undefined} - * @ignore - */ - _clearInterval (tid) { - clearInterval(tid) - } - - /** - * An implementation for clearning a timeout that can be overridden by the test suite - * @param Number the number that identifies the timer - * @return {undefined} - * @ignore - */ - _clearTimeout (tid) { - clearTimeout(tid) - } - - /** - * An implementation of an internal timer that can be overridden by the test suite - * @return {Number} - * @ignore - */ - _setInterval (fn, t) { - return setInterval(fn, t) - } - - /** - * An implementation of an timeout timer that can be overridden by the test suite - * @return {Number} - * @ignore - */ - _setTimeout (fn, t) { - return setTimeout(fn, t) - } - - /** - * A method that encapsulates the listing procedure - * @return {undefined} - * @ignore - */ - async _listen () { - await sodium.ready - - this.socket.removeAllListeners() - this.probeSocket.removeAllListeners() - - this.socket.on('message', (...args) => this._onMessage(...args)) - this.socket.on('error', (...args) => this._onError(...args)) - this.probeSocket.on('message', (...args) => this._onProbeMessage(...args)) - this.probeSocket.on('error', (...args) => this._onError(...args)) - - this.socket.setMaxListeners(2048) - this.probeSocket.setMaxListeners(2048) - - const listening = Promise.all([ - new Promise(resolve => this.socket.on('listening', resolve)), - new Promise(resolve => this.probeSocket.on('listening', resolve)) - ]) - - this.socket.bind(this.config.port || 0) - this.probeSocket.bind(this.config.probeInternalPort || 0) - - await listening - - this.config.port = this.socket.address().port - this.config.probeInternalPort = this.probeSocket.address().port - - if (this.onListening) this.onListening() - this.isListening = true - - debug(this.peerId, `++ INIT (config.internalPort=${this.config.port}, config.probeInternalPort=${this.config.probeInternalPort})`) - } - - /* - * This method will bind the sockets, begin pinging known peers, and start - * the main program loop. - * @return {Any} - */ - async init (cb) { - if (!this.isListening) await this._listen() - if (cb) this.onReady = cb - - // tell all well-known peers that we would like to hear from them, if - // we hear from any we can ask for the reflection information we need. - for (const peer of this.peers.filter(p => p.indexed)) { - await this.ping(peer, false, { message: { requesterPeerId: this.peerId } }) - } - - this._mainLoop(Date.now()) - this.mainLoopTimer = this._setInterval(ts => this._mainLoop(ts), this.config.keepalive) - - if (this.indexed && this.onReady) return this.onReady() - } - - /** - * Continuously evaluate the state of the peer and its network - * @return {undefined} - * @ignore - */ - async _mainLoop (ts) { - if (this.closing) return this._clearInterval(this.mainLoopTimer) - - const offline = globalThis.navigator && !globalThis.navigator.onLine - if (offline) { - if (this.onConnecting) this.onConnecting({ code: -2, status: 'Offline' }) - return true - } - - if (!this.reflectionId) this.requestReflection() - if (this.onInterval) this.onInterval() - - this.uptime += this.config.keepalive - - // wait for nat type to be discovered - if (!NAT.isValid(this.natType)) return true - - for (const [k, packet] of [...this.cache.data]) { - const p = Packet.from(packet) - if (!p) continue - if (!p.timestamp) p.timestamp = ts - const clusterId = p.clusterId.toString('base64') - - const mult = this.clusters[clusterId] ? 2 : 1 - const ttl = (p.ttl < Packet.ttl) ? p.ttl : Packet.ttl * mult - const deadline = p.timestamp + ttl - - if (deadline <= ts) { - if (p.hops < this.maxHops) this.mcast(p) - this.cache.delete(k) - debug(this.peerId, '-- DELETE', k, this.cache.size) - if (this.onDelete) this.onDelete(p) - } - } - - for (let [k, v] of this.gate.entries()) { - v -= 1 - if (!v) this.gate.delete(k) - else this.gate.set(k, v) - } - - for (let [k, v] of this.returnRoutes.entries()) { - v -= 1 - if (!v) this.returnRoutes.delete(k) - else this.returnRoutes.set(k, v) - } - - // prune peer list - for (const [i, peer] of Object.entries(this.peers)) { - if (peer.indexed) continue - const expired = (peer.lastUpdate + this.config.keepalive) < Date.now() - if (expired) { // || !NAT.isValid(peer.natType)) { - const p = this.peers.splice(i, 1) - if (this.onDisconnect) this.onDisconnect(p) - continue - } - } - - // heartbeat - const { hash } = await this.cache.summarize('', this.cachePredicate) - for (const [, peer] of Object.entries(this.peers)) { - this.ping(peer, false, { - message: { - requesterPeerId: this.peerId, - natType: this.natType, - cacheSummaryHash: hash || null, - cacheSize: this.cache.size - } - }) - } - - // if this peer has previously tried to join any clusters, multicast a - // join messages for each into the network so we are always searching. - for (const cluster of Object.values(this.clusters)) { - for (const subcluster of Object.values(cluster)) { - this.join(subcluster.sharedKey, subcluster) - } - } - return true - } - - /** - * Enqueue packets to be sent to the network - * @param {Buffer} data - An encoded packet - * @param {number} port - The desination port of the remote host - * @param {string} address - The destination address of the remote host - * @param {Socket=this.socket} socket - The socket to send on - * @return {undefined} - * @ignore - */ - send (data, port, address, socket = this.socket) { - this.sendQueue.push({ data, port, address, socket }) - this._scheduleSend() - } - - /** - * @private - */ - _scheduleSend () { - if (this.sendTimeout) this._clearTimeout(this.sendTimeout) - this.sendTimeout = this._setTimeout(() => { this._dequeue() }) - } - - /** - * @private - */ - _dequeue () { - if (!this.sendQueue.length) return - const { data, port, address, socket } = this.sendQueue.shift() - - socket.send(data, port, address, err => { - if (this.sendQueue.length) this._scheduleSend() - if (err) return this._onError(err) - - const packet = Packet.decode(data) - if (!packet) return - - this.metrics.o[packet.type]++ - delete this.unpublished[packet.packetId.toString('hex')] - if (this.onSend && packet.type) this.onSend(packet, port, address) - debug(this.peerId, `>> SEND (from=${this.address}:${this.port}, to=${address}:${port}, type=${packet.type})`) - }) - } - - /** - * Send any unpublished packets - * @return {undefined} - * @ignore - */ - async sendUnpublished () { - for (const [packetId] of Object.entries(this.unpublished)) { - const packet = this.cache.get(packetId) - - if (!packet) { // it may have been purged already - delete this.unpublished[packetId] - continue - } - - await this.mcast(packet) - debug(this.peerId, `-> RESEND (packetId=${packetId})`) - if (this.onState) await this.onState(this.getState()) - } - } - - /** - * Get the serializable state of the peer (can be passed to the constructor or create method) - * @return {undefined} - */ - getState () { - this.config.clock = this.clock // save off the clock - - return { - config: this.config, - data: [...this.cache.data.entries()], - unpublished: this.unpublished - } - } - - /** - * Get a selection of known peers - * @return {Array<RemotePeer>} - * @ignore - */ - getPeers (packet, peers, ignorelist, filter = o => o) { - const rand = () => Math.random() - 0.5 - - const base = p => { - if (ignorelist.findIndex(ilp => (ilp.port === p.port) && (ilp.address === p.address)) > -1) return false - if (p.lastUpdate === 0) return false - if (p.lastUpdate < Date.now() - (this.config.keepalive * 4)) return false - if (this.peerId === p.peerId) return false // same as me - if (packet.message.requesterPeerId === p.peerId) return false // same as requester - @todo: is this true in all cases? - if (!p.port || !NAT.isValid(p.natType)) return false - return true - } - - const candidates = peers - .filter(filter) - .filter(base) - .sort(rand) - - const list = candidates.slice(0, 3) - - if (!list.some(p => p.indexed)) { - const indexed = candidates.filter(p => p.indexed && !list.includes(p)) - if (indexed.length) list.push(indexed[0]) - } - - const clusterId = packet.clusterId.toString('base64') - const friends = candidates.filter(p => p.clusters && p.clusters[clusterId] && !list.includes(p)) - if (friends.length) { - list.unshift(friends[0]) - list.unshift(...candidates.filter(c => c.address === friends[0].address && c.peerId === friends[0].peerId)) - } - - return list - } - - /** - * Send an eventually consistent packet to a selection of peers (fanout) - * @return {undefined} - * @ignore - */ - async mcast (packet, ignorelist = []) { - const peers = this.getPeers(packet, this.peers, ignorelist) - const pid = packet.packetId.toString('hex') - - packet.hops += 1 - - for (const peer of peers) { - this.send(await Packet.encode(packet), peer.port, peer.address) - } - - if (this.onMulticast) this.onMulticast(packet) - if (this.gate.has(pid)) return - this.gate.set(pid, 1) - } - - /** - * The process of determining this peer's NAT behavior (firewall and dependentness) - * @return {undefined} - * @ignore - */ - async requestReflection () { - if (this.closing || this.indexed || this.reflectionId) { - debug(this.peerId, '<> REFLECT ABORTED', this.reflectionId) - return - } - - if (this.natType && (this.lastUpdate > 0 && (Date.now() - this.config.keepalive) < this.lastUpdate)) { - debug(this.peerId, `<> REFLECT NOT NEEDED (last-recv=${Date.now() - this.lastUpdate}ms)`) - return - } - - debug(this.peerId, '-> REQ REFLECT', this.reflectionId, this.reflectionStage) - if (this.onConnecting) this.onConnecting({ code: -1, status: `Entering reflection (lastUpdate ${Date.now() - this.lastUpdate}ms)` }) - - const peers = [...this.peers] - .filter(p => p.lastUpdate !== 0) - .filter(p => p.natType === NAT.UNRESTRICTED || p.natType === NAT.ADDR_RESTRICTED || p.indexed) - - if (peers.length < 2) { - if (this.onConnecting) this.onConnecting({ code: -1, status: 'Not enough pingable peers' }) - debug(this.peerId, 'XX REFLECT NOT ENOUGH PINGABLE PEERS - RETRYING') - - if (++this.reflectionRetry > 16) this.reflectionRetry = 1 - return this._setTimeout(() => this.requestReflection(), this.reflectionRetry * 256) - } - - this.reflectionRetry = 1 - - const requesterPeerId = this.peerId - const opts = { requesterPeerId, isReflection: true } - - this.reflectionId = opts.reflectionId = randomBytes(6).toString('hex').padStart(12, '0') - - if (this.onConnecting) { - this.onConnecting({ code: 0.5, status: `Found ${peers.length} elegible peers for reflection` }) - } - // - // # STEP 1 - // The purpose of this step is strictily to discover the external port of - // the probe socket. - // - if (this.reflectionStage === 0) { - if (this.onConnecting) this.onConnecting({ code: 1, status: 'Discover External Port' }) - // start refelection with an zeroed NAT type - if (this.reflectionTimeout) this._clearTimeout(this.reflectionTimeout) - this.reflectionStage = 1 - - debug(this.peerId, '-> NAT REFLECT - STAGE1: A', this.reflectionId) - const list = peers.filter(p => p.probed).sort(() => Math.random() - 0.5) - const peer = list.length ? list[0] : peers[0] - peer.probed = Date.now() // mark this peer as being used to provide port info - this.ping(peer, false, { message: { ...opts, isProbe: true } }, this.probeSocket) - - // we expect onMessageProbe to fire and clear this timer or it will timeout - this.probeReflectionTimeout = this._setTimeout(() => { - this.probeReflectionTimeout = null - if (this.reflectionStage !== 1) return - debug(this.peerId, 'XX NAT REFLECT - STAGE1: C - TIMEOUT', this.reflectionId) - if (this.onConnecting) this.onConnecting({ code: 1, status: 'Timeout' }) - - this.reflectionStage = 1 - this.reflectionId = null - this.requestReflection() - }, 1024) - - debug(this.peerId, '-> NAT REFLECT - STAGE1: B', this.reflectionId) - return - } - - // - // # STEP 2 - // - // The purpose of step 2 is twofold: - // - // 1) ask two different peers for the external port and address for our primary socket. - // If they are different, we can determine that our NAT is a `ENDPOINT_DEPENDENT`. - // - // 2) ask the peers to also reply to our probe socket from their probe socket. - // These packets will both be dropped for `FIREWALL_ALLOW_KNOWN_IP_AND_PORT` and will both - // arrive for `FIREWALL_ALLOW_ANY`. If one packet arrives (which will always be from the peer - // which was previously probed), this indicates `FIREWALL_ALLOW_KNOWN_IP`. - // - if (this.reflectionStage === 1) { - this.reflectionStage = 2 - const { probeExternalPort } = this.config - if (this.onConnecting) this.onConnecting({ code: 1.5, status: 'Discover NAT' }) - - // peer1 is the most recently probed (likely the same peer used in step1) - // using the most recent guarantees that the the NAT mapping is still open - const peer1 = peers.filter(p => p.probed).sort((a, b) => b.probed - a.probed)[0] - - // peer has NEVER previously been probed - const peer2 = peers.filter(p => !p.probed).sort(() => Math.random() - 0.5)[0] - - if (!peer1 || !peer2) { - debug(this.peerId, 'XX NAT REFLECT - STAGE2: INSUFFICENT PEERS - RETRYING') - if (this.onConnecting) this.onConnecting({ code: 1.5, status: 'Insufficent Peers' }) - return this._setTimeout(() => this.requestReflection(), 256) - } - - debug(this.peerId, '-> NAT REFLECT - STAGE2: START', this.reflectionId) - - // reset reflection variables to defaults - this.nextNatType = NAT.UNKNOWN - this.reflectionFirstResponder = null - - this.ping(peer1, false, { message: { ...opts, probeExternalPort } }) - this.ping(peer2, false, { message: { ...opts, probeExternalPort } }) - - if (this.onConnecting) { - this.onConnecting({ code: 2, status: `Requesting reflection from ${peer1.address}` }) - this.onConnecting({ code: 2, status: `Requesting reflection from ${peer2.address}` }) - } - - if (this.reflectionTimeout) { - this._clearTimeout(this.reflectionTimeout) - this.reflectionTimeout = null - } - - this.reflectionTimeout = this._setTimeout(ts => { - this.reflectionTimeout = null - if (this.reflectionStage !== 2) return - if (this.onConnecting) this.onConnecting({ code: 2, status: 'Timeout' }) - this.reflectionStage = 1 - this.reflectionId = null - debug(this.peerId, 'XX NAT REFLECT - STAGE2: TIMEOUT', this.reflectionId) - return this.requestReflection() - }, 2048) - } - } - - /** - * Ping another peer - * @return {PacketPing} - * @ignore - */ - async ping (peer, withRetry, props, socket) { - if (!peer) { - return - } - - props.message.requesterPeerId = this.peerId - props.message.uptime = this.uptime - props.message.timestamp = Date.now() - - const packet = new PacketPing(props) - const data = await Packet.encode(packet) - - const send = async () => { - if (this.closing) return false - - const p = this.peers.find(p => p.peerId === peer.peerId) - // if (p?.reflectionId && p.reflectionId === packet.message.reflectionId) { - // return false - // } - - this.send(data, peer.port, peer.address, socket) - if (p) p.lastRequest = Date.now() - } - - send() - - if (withRetry) { - this._setTimeout(send, PING_RETRY) - this._setTimeout(send, PING_RETRY * 4) - } - - return packet - } - - getPeer (id) { - return this.peers.find(p => p.peerId === id) - } - - /** - * This should be called at least once when an app starts to multicast - * this peer, and starts querying the network to discover peers. - * @param {object} keys - Created by `Encryption.createKeyPair()`. - * @param {object=} args - Options - * @param {number=MAX_BANDWIDTH} args.rateLimit - How many requests per second to allow for this subclusterId. - * @return {RemotePeer} - */ - async join (sharedKey, args = { rateLimit: MAX_BANDWIDTH }) { - const keys = await Encryption.createKeyPair(sharedKey) - this.encryption.add(keys.publicKey, keys.privateKey) - - if (!this.port || !this.natType) return - - args.sharedKey = sharedKey - - const clusterId = args.clusterId || this.config.clusterId - const subclusterId = Buffer.from(keys.publicKey) - - const cid = clusterId?.toString('base64') - const scid = subclusterId?.toString('base64') - - this.clusters[cid] ??= {} - this.clusters[cid][scid] = args - - this.clock += 1 - - const packet = new PacketJoin({ - clock: this.clock, - clusterId, - subclusterId, - message: { - requesterPeerId: this.peerId, - natType: this.natType, - address: this.address, - port: this.port - } - }) - - debug(this.peerId, `-> JOIN (clusterId=${cid}, subclusterId=${scid}, clock=${packet.clock}/${this.clock})`) - if (this.onState) await this.onState(this.getState()) - this.mcast(packet) - this.gate.set(packet.packetId.toString('hex'), 1) - } - - /** - * @param {Packet} T - The constructor to be used to create packets. - * @param {Any} message - The message to be split and packaged. - * @return {Array<Packet<T>>} - * @ignore - */ - async _message2packets (T, message, args) { - const { clusterId, subclusterId, packet, nextId, meta = {}, usr1, usr2, sig } = args - - let messages = [message] - const len = message?.byteLength ?? message?.length ?? 0 - let clock = packet?.clock || 0 - - const siblings = [...this.cache.data.values()] - .filter(Boolean) - .filter(p => p?.previousId?.toString('hex') === packet?.packetId?.toString('hex')) - - if (siblings.length) { - // if there are siblings of the previous packet - // pick the highest clock value, the parent packet or the sibling - const sort = (a, b) => a.clock - b.clock - const sib = siblings.sort(sort).reverse()[0] - clock = Math.max(clock, sib.clock) + 1 - } - - clock += 1 - - if (len > 1024) { // Split packets that have messages bigger than Packet.maxLength - messages = [{ - meta, - ts: Date.now(), - size: message.length, - indexes: Math.ceil(message.length / 1024) - }] - let pos = 0 - while (pos < message.length) messages.push(message.slice(pos, pos += 1024)) - } - - // turn each message into an actual packet - const packets = messages.map(message => new T({ - ...args, - clusterId, - subclusterId, - clock, - message, - usr1, - usr2, - usr3: args.usr3, - usr4: args.usr4, - sig - })) - - if (packet) packets[0].previousId = packet.packetId - if (nextId) packets[packets.length - 1].nextId = nextId - - // set the .packetId (any maybe the .previousId and .nextId) - for (let i = 0; i < packets.length; i++) { - if (packets.length > 1) packets[i].index = i - - if (i === 0) { - packets[0].packetId = await sha256(packets[0].message, { bytes: true }) - } else { - // all fragments will have the same previous packetId - // the index is used to stitch them back together in order. - packets[i].previousId = packets[0].packetId - } - - if (packets[i + 1]) { - packets[i + 1].packetId = await sha256( - Buffer.concat([ - await sha256(packets[i].packetId, { bytes: true }), - await sha256(packets[i + 1].message, { bytes: true }) - ]), - { bytes: true } - ) - packets[i].nextId = packets[i + 1].packetId - } - } - - return packets - } - - /** - * Sends a packet into the network that will be replicated and buffered. - * Each peer that receives it will buffer it until TTL and then replicate - * it provided it has has not exceeded their maximum number of allowed hops. - * - * @param {object} keys - the public and private key pair created by `Encryption.createKeyPair()`. - * @param {object} args - The arguments to be applied. - * @param {Buffer} args.message - The message to be encrypted by keys and sent. - * @param {Packet<T>=} args.packet - The previous packet in the packet chain. - * @param {Buffer} args.usr1 - 32 bytes of arbitrary clusterId in the protocol framing. - * @param {Buffer} args.usr2 - 32 bytes of arbitrary clusterId in the protocol framing. - * @return {Array<PacketPublish>} - */ - async publish (sharedKey, args) { // wtf to do here, we need subclusterId and the actual user keys - if (!sharedKey) throw new Error('.publish() expected "sharedKey" argument in first position') - if (!isBufferLike(args.message)) throw new Error('.publish() will only accept a message of type buffer') - - const keys = await Encryption.createKeyPair(sharedKey) - - args.subclusterId = Buffer.from(keys.publicKey) - args.clusterId = args.clusterId || this.config.clusterId - - const message = this.encryption.seal(args.message, keys) - const packets = await this._message2packets(PacketPublish, message, args) - - for (const packet of packets) { - const p = Packet.from(packet) - this.cache.insert(packet.packetId.toString('hex'), p) - - if (this.onPacket) this.onPacket(p, this.port, this.address, true) - - this.unpublished[packet.packetId.toString('hex')] = Date.now() - if (globalThis.navigator && !globalThis.navigator.onLine) continue - - this.mcast(packet) - } - - return packets - } - - /** - * @return {undefined} - */ - async sync (peer) { - const rinfo = peer?.proxy || peer - - this.lastSync = Date.now() - const summary = await this.cache.summarize('', this.cachePredicate) - - debug(this.peerId, `-> SYNC START (dest=${peer.peerId.slice(0, 8)}, to=${rinfo.address}:${rinfo.port})`) - if (this.onSyncStart) this.onSyncStart(peer, rinfo.port, rinfo.address) - - // if we are out of sync send our cache summary - const data = await Packet.encode(new PacketSync({ - message: Cache.encodeSummary(summary) - })) - - this.send(data, rinfo.port, rinfo.address, peer.socket) - } - - close () { - this._clearInterval(this.mainLoopTimer) - - if (this.closing) return - - this.closing = true - this.socket.close() - - if (this.onClose) this.onClose() - } - - /** - * Deploy a query into the network - * @return {undefined} - * - */ - async query (query) { - const packet = new PacketQuery({ - message: query, - usr1: Buffer.from(String(Date.now())), - usr3: Buffer.from(randomBytes(32)), - usr4: Buffer.from(String(1)) - }) - const data = await Packet.encode(packet) - - const p = Packet.decode(data) // finalize a packet - const pid = p.packetId.toString('hex') - - if (this.gate.has(pid)) return - this.returnRoutes.set(p.usr3.toString('hex'), {}) - this.gate.set(pid, 1) // don't accidentally spam - - debug(this.peerId, `-> QUERY (type=question, query=${query}, packet=${pid.slice(0, 8)})`) - - await this.mcast(p) - } - - /** - * - * This is a default implementation for deciding what to summarize - * from the cache when receiving a request to sync. that can be overridden - * - */ - cachePredicate (packet) { - return packet.version === VERSION && packet.timestamp > Date.now() - Packet.ttl - } - - /** - * A connection was made, add the peer to the local list of known - * peers and call the onConnection if it is defined by the user. - * - * @return {undefined} - * @ignore - */ - async _onConnection (packet, peerId, port, address, proxy, socket) { - if (this.closing) return - - const natType = packet.message.natType - - const { clusterId, subclusterId } = packet - - let peer = this.getPeer(peerId) - - if (!peer) { - peer = new RemotePeer({ peerId }) - - if (this.peers.length >= 256) { - // TODO evicting an older peer definitely needs some more thought. - const oldPeerIndex = this.peers.findIndex(p => !p.lastUpdate && !p.indexed) - if (oldPeerIndex > -1) this.peers.splice(oldPeerIndex, 1) - } - - this.peers.push(peer) - } - - peer.connected = true - peer.lastUpdate = Date.now() - peer.port = port - peer.natType = natType - peer.address = address - if (proxy) peer.proxy = proxy - if (socket) peer.socket = socket - - const cid = clusterId.toString('base64') - const scid = subclusterId.toString('base64') - - if (cid) peer.clusters[cid] ??= {} - - if (cid && scid) { - const cluster = peer.clusters[cid] - cluster[scid] = { rateLimit: MAX_BANDWIDTH } - } - - if (!peer.localPeer) peer.localPeer = this - if (!this.connections) this.connections = new Map() - - debug(this.peerId, '<- CONNECTION ( ' + - `peerId=${peer.peerId.slice(0, 6)}, ` + - `address=${address}:${port}, ` + - `type=${packet.type}, ` + - `cluster=${cid.slice(0, 8)}, ` + - `sub-cluster=${scid.slice(0, 8)})` - ) - - if (this.onJoin && this.clusters[cid]) { - this.onJoin(packet, peer, port, address) - } - - if (!this.connections.has(peer)) { - this.onConnection && this.onConnection(packet, peer, port, address) - this.connections.set(peer, packet.message.cacheSummaryHash) - } - } - - /** - * Received a Sync Packet - * @return {undefined} - * @ignore - */ - async _onSync (packet, port, address) { - this.metrics.i[packet.type]++ - - this.lastSync = Date.now() - const pid = packet.packetId?.toString('hex') - - if (!isBufferLike(packet.message)) return - if (this.gate.has(pid)) return - - this.gate.set(pid, 1) - - const remote = Cache.decodeSummary(packet.message) - const local = await this.cache.summarize(remote.prefix, this.cachePredicate) - - if (!remote || !remote.hash || !local || !local.hash || local.hash === remote.hash) { - if (this.onSyncFinished) this.onSyncFinished(packet, port, address) - return - } - - if (this.onSync) this.onSync(packet, port, address, { remote, local }) - - const remoteBuckets = remote.buckets.filter(Boolean).length - debug(this.peerId, `<- ON SYNC (from=${address}:${port}, local=${local.hash.slice(0, 8)}, remote=${remote.hash.slice(0, 8)} remote-buckets=${remoteBuckets})`) - - for (let i = 0; i < local.buckets.length; i++) { - // - // nothing to send/sync, expect peer to send everything they have - // - if (!local.buckets[i] && !remote.buckets[i]) continue - - // - // you dont have any of these, im going to send them to you - // - if (!remote.buckets[i]) { - for (const [key, p] of this.cache.data.entries()) { - if (!key.startsWith(local.prefix + i.toString(16))) continue - - const packet = Packet.from(p) - if (!this.cachePredicate(packet)) continue - - const pid = packet.packetId.toString('hex') - debug(this.peerId, `-> SYNC SEND PACKET (type=data, packetId=${pid.slice(0, 8)}, to=${address}:${port})`) - - this.send(await Packet.encode(packet), port, address) - } - } else { - // - // need more details about what exactly isn't synce'd - // - const nextLevel = await this.cache.summarize(local.prefix + i.toString(16), this.cachePredicate) - const data = await Packet.encode(new PacketSync({ - message: Cache.encodeSummary(nextLevel) - })) - this.send(data, port, address) - } - } - } - - /** - * Received a Query Packet - * - * a -> b -> c -> (d) -> c -> b -> a - * - * @return {undefined} - * @example - * - * ```js - * peer.onQuery = (packet) => { - * // - * // read a database or something - * // - * return { - * message: Buffer.from('hello'), - * publicKey: '', - * privateKey: '' - * } - * } - * ``` - */ - async _onQuery (packet, port, address) { - this.metrics.i[packet.type]++ - - const pid = packet.packetId.toString('hex') - if (this.gate.has(pid)) return - this.gate.set(pid, 1) - - const queryTimestamp = parseInt(Buffer.from(packet.usr1).toString(), 10) - const queryId = Buffer.from(packet.usr3).toString('hex') - const queryType = parseInt(Buffer.from(packet.usr4).toString(), 10) - - // if the timestamp in usr1 is older than now - 2s, bail - if (queryTimestamp < (Date.now() - 2048)) return - - const type = queryType === 1 ? 'question' : 'answer' - debug(this.peerId, `<- QUERY (type=${type}, from=${address}:${port}, packet=${pid.slice(0, 8)})`) - - let rinfo = { port, address } - - // - // receiving an answer - // - if (this.returnRoutes.has(queryId)) { - rinfo = this.returnRoutes.get(queryId) - - let p = packet.copy() - if (p.index > -1) p = await this.cache.compose(p) - - if (p?.index === -1) { - this.returnRoutes.delete(p.previousId.toString('hex')) - p.type = PacketPublish.type - delete p.usr3 - delete p.usr4 - if (this.onAnswer) return this.onAnswer(p.message, p, port, address) - } - - if (!rinfo.address) return - } else { - // - // receiving a query - // - this.returnRoutes.set(queryId, { address, port }) - - const query = packet.message - const packets = [] - - // - // The requestor is looking for an exact packetId. In this case, - // the peer has a packet with a previousId or nextId that points - // to a packetId they don't have. There is no need to specify the - // index in the query, split packets will have a nextId. - // - // if cache packet = { nextId: 'deadbeef...' } - // then query = { packetId: packet.nextId } - // or query = { packetId: packet.previousId } - // - if (query.packetId && this.cache.has(query.packetId)) { - const p = this.cache.get(query.packetId) - if (p) packets.push(p) - } else if (this.onQuery) { - const q = await this.onQuery(query) - if (q) packets.push(...await this._message2packets(PacketQuery, q.message, q)) - } - - if (packets.length) { - for (const p of packets) { - p.type = PacketQuery.type // convert the type during transport - p.usr3 = packet.usr3 // ensure the packet has the queryId - p.usr4 = Buffer.from(String(2)) // mark it as an answer packet - this.send(await Packet.encode(p), rinfo.port, rinfo.address) - } - return - } - } - - if (packet.hops >= this.maxHops) return - debug(this.peerId, '>> QUERY RELAY', port, address) - return await this.mcast(packet) - } - - /** - * Received a Ping Packet - * @return {undefined} - * @ignore - */ - async _onPing (packet, port, address) { - this.metrics.i[packet.type]++ - - this.lastUpdate = Date.now() - const { reflectionId, isReflection, isConnection, isHeartbeat } = packet.message - - if (packet.message.requesterPeerId === this.peerId) return - - const { probeExternalPort, isProbe, pingId } = packet.message - - if (isHeartbeat) { - // const peer = this.getPeer(packet.message.requesterPeerId) - // if (peer && natType) peer.natType = natType - return - } - - // if (peer && reflectionId) peer.reflectionId = reflectionId - if (!port) port = packet.message.port - if (!address) address = packet.message.address - - const message = { - cacheSize: this.cache.size, - uptime: this.uptime, - responderPeerId: this.peerId, - requesterPeerId: packet.message.requesterPeerId, - port, - isProbe, - address - } - - if (reflectionId) message.reflectionId = reflectionId - if (isHeartbeat) message.isHeartbeat = Date.now() - if (pingId) message.pingId = pingId - - if (isReflection) { - message.isReflection = true - message.port = port - message.address = address - } else { - message.natType = this.natType - } - - if (isConnection) { - const peerId = packet.message.requesterPeerId - this._onConnection(packet, peerId, port, address) - - message.isConnection = true - delete message.address - delete message.port - delete message.isProbe - } - - const { hash } = await this.cache.summarize('', this.cachePredicate) - message.cacheSummaryHash = hash - - const packetPong = new PacketPong({ message }) - const buf = await Packet.encode(packetPong) - - this.send(buf, port, address) - - if (probeExternalPort) { - message.port = probeExternalPort - const packetPong = new PacketPong({ message }) - const buf = await Packet.encode(packetPong) - this.send(buf, probeExternalPort, address, this.probeSocket) - } - } - - /** - * Received a Pong Packet - * @return {undefined} - * @ignore - */ - async _onPong (packet, port, address) { - this.metrics.i[packet.type]++ - - this.lastUpdate = Date.now() - - const { reflectionId, pingId, isReflection, responderPeerId } = packet.message - - debug(this.peerId, `<- PONG (from=${address}:${port}, hash=${packet.message.cacheSummaryHash}, isConnection=${!!packet.message.isConnection})`) - const peer = this.getPeer(packet.message.responderPeerId) - - if (packet.message.isConnection) { - if (pingId) peer.pingId = pingId - this._onConnection(packet, packet.message.responderPeerId, port, address) - return - } - - if (!peer) return - - if (isReflection && !this.indexed) { - if (reflectionId !== this.reflectionId) return - - this._clearTimeout(this.reflectionTimeout) - - if (!this.reflectionFirstResponder) { - this.reflectionFirstResponder = { port, address, responderPeerId, reflectionId, packet } - if (this.onConnecting) this.onConnecting({ code: 2.5, status: `Received reflection from ${address}:${port}` }) - debug(this.peerId, '<- NAT REFLECT - STAGE2: FIRST RESPONSE', port, address, this.reflectionId) - } else { - if (this.onConnecting) this.onConnecting({ code: 2.5, status: `Received reflection from ${address}:${port}` }) - debug(this.peerId, '<- NAT REFLECT - STAGE2: SECOND RESPONSE', port, address, this.reflectionId) - if (packet.message.address !== this.address) return - - this.nextNatType |= ( - packet.message.port === this.reflectionFirstResponder.packet.message.port - ) - ? NAT.MAPPING_ENDPOINT_INDEPENDENT - : NAT.MAPPING_ENDPOINT_DEPENDENT - - debug( - this.peerId, - `++ NAT REFLECT - STATE UPDATE (natType=${this.natType}, nextType=${this.nextNatType})`, - packet.message.port, - this.reflectionFirstResponder.packet.message.port - ) - - // wait PROBE_WAIT milliseconds for zero or more probe responses to arrive. - this._setTimeout(async () => { - // build the NAT type by combining information about the firewall with - // information about the endpoint independence - let natType = this.nextNatType - - // in the case where we recieved zero probe responses, we assume the firewall - // is of the hardest type 'FIREWALL_ALLOW_KNOWN_IP_AND_PORT'. - if (!NAT.isFirewallDefined(natType)) natType |= NAT.FIREWALL_ALLOW_KNOWN_IP_AND_PORT - - // if ((natType & NAT.MAPPING_ENDPOINT_DEPENDENT) === 1) natType = NAT.ENDPOINT_RESTRICTED - - if (NAT.isValid(natType)) { - // const oldType = this.natType - this.natType = natType - this.reflectionId = null - this.reflectionStage = 0 - - // if (natType !== oldType) { - // alert all connected peers of our new NAT type - for (const peer of this.peers) { - peer.lastRequest = Date.now() - - debug(this.peerId, `-> PING (to=${peer.address}:${peer.port}, peer-id=${peer.peerId.slice(0, 8)}, is-connection=true)`) - - await this.ping(peer, false, { - message: { - requesterPeerId: this.peerId, - natType: this.natType, - cacheSize: this.cache.size, - isConnection: true - } - }) - } - - if (this.onNat) this.onNat(this.natType) - - debug(this.peerId, `++ NAT (type=${NAT.toString(this.natType)})`) - this.sendUnpublished() - // } - - if (this.onConnecting) this.onConnecting({ code: 3, status: `Discovered! (nat=${NAT.toString(this.natType)})` }) - if (this.onReady) this.onReady() - } - - this.reflectionId = null - this.reflectionFirstResponder = null - }, PROBE_WAIT) - } - - this.address = packet.message.address - this.port = packet.message.port - debug(this.peerId, `++ NAT UPDATE STATE (address=${this.address}, port=${this.port})`) - } - } - - /** - * Received an Intro Packet - * @return {undefined} - * @ignore - */ - async _onIntro (packet, port, address, _, opts = { attempts: 0 }) { - this.metrics.i[packet.type]++ - if (this.closing) return - - const pid = packet.packetId.toString('hex') - // the packet needs to be gated, but should allow for attempt - // recursion so that the fallback can still be selected. - if (this.gate.has(pid) && opts.attempts === 0) return - this.gate.set(pid, 1) - - const ts = packet.usr1.length && Number(packet.usr1.toString()) - - if (packet.hops >= this.maxHops) return - if (!isNaN(ts) && ((ts + this.config.keepalive) < Date.now())) return - if (packet.message.requesterPeerId === this.peerId) return // intro to myself? - if (packet.message.responderPeerId === this.peerId) return // intro from myself? - - // this is the peer that is being introduced to the new peers - const peerId = packet.message.requesterPeerId - const peerPort = packet.message.port - const peerAddress = packet.message.address - const natType = packet.message.natType - const { clusterId, subclusterId, clock } = packet - - // already introduced in the laste minute, just drop the packet - if (opts.attempts === 0 && this.gate.has(peerId + peerAddress + peerPort)) return - this.gate.set(peerId + peerAddress + peerPort, 2) - - // we already know this peer, and we're even connected to them! - let peer = this.getPeer(peerId) - if (!peer) peer = new RemotePeer({ peerId, natType, port: peerPort, address: peerAddress, clock, clusterId, subclusterId }) - if (peer.connected) return // already connected - if (clock > 0 && clock < peer.clock) return - peer.clock = clock - - // a mutex per inbound peer to ensure that it's not connecting concurrently, - // the check of the attempts ensures its allowed to recurse before failing so - // it can still fall back - if (this.gate.has('CONN' + peer.peerId) && opts.attempts === 0) return - this.gate.set('CONN' + peer.peerId, 1) - - const cid = clusterId.toString('base64') - const scid = subclusterId.toString('base64') - - debug(this.peerId, '<- INTRO (' + - `isRendezvous=${packet.message.isRendezvous}, ` + - `from=${address}:${port}, ` + - `to=${packet.message.address}:${packet.message.port}, ` + - `clustering=${cid.slice(0, 4)}/${scid.slice(0, 4)}` + - ')') - - if (this.onIntro) this.onIntro(packet, peer, peerPort, peerAddress) - - const pingId = Math.random().toString(16).slice(2) - const { hash } = await this.cache.summarize('', this.cachePredicate) - - const props = { - clusterId, - subclusterId, - message: { - natType: this.natType, - isConnection: true, - cacheSummaryHash: hash || null, - pingId: packet.message.pingId, - requesterPeerId: this.peerId - } - } - - const strategy = NAT.connectionStrategy(this.natType, packet.message.natType) - const proxyCandidate = this.peers.find(p => p.peerId === packet.message.responderPeerId) - - if (opts.attempts >= 2) { - this._onConnection(packet, peer.peerId, peerPort, peerAddress, proxyCandidate) - return false - } - - this._setTimeout(() => { - if (this.getPeer(peer.peerId)) return - opts.attempts = 2 - this._onIntro(packet, port, address, _, opts) - }, 1024 * 2) - - if (packet.message.isRendezvous) { - debug(this.peerId, `<- INTRO FROM RENDEZVOUS (to=${packet.message.address}:${packet.message.port}, dest=${packet.message.requesterPeerId.slice(0, 6)}, via=${address}:${port}, strategy=${NAT.toStringStrategy(strategy)})`) - } - - debug(this.peerId, `++ NAT INTRO (strategy=${NAT.toStringStrategy(strategy)}, from=${this.address}:${this.port} [${NAT.toString(this.natType)}], to=${packet.message.address}:${packet.message.port} [${NAT.toString(packet.message.natType)}])`) - - if (strategy === NAT.STRATEGY_TRAVERSAL_CONNECT) { - debug(this.peerId, `## NAT CONNECT (from=${this.address}:${this.port}, to=${peerAddress}:${peerPort}, pingId=${pingId})`) - - let i = 0 - if (!this.socketPool) { - this.socketPool = Array.from({ length: 256 }, (_, index) => { - return dgram.createSocket('udp4', null, this, index).unref() - }) - } - - // A probes 1 target port on B from 1024 source ports - // (this is 1.59% of the search clusterId) - // B probes 256 target ports on A from 1 source port - // (this is 0.40% of the search clusterId) - // - // Probability of successful traversal: 98.35% - // - const interval = this._setInterval(async () => { - // send messages until we receive a message from them. giveup after sending ±1024 - // packets and fall back to using the peer that sent this as the initial proxy. - if (i++ >= 1024) { - this._clearInterval(interval) - - opts.attempts++ - this._onIntro(packet, port, address, _, opts) - return false - } - - const p = { - clusterId, - subclusterId, - message: { - requesterPeerId: this.peerId, - cacheSummaryHash: hash || null, - natType: this.natType, - uptime: this.uptime, - isConnection: true, - timestamp: Date.now(), - pingId - } - } - - const data = await Packet.encode(new PacketPing(p)) - - const rand = () => Math.random() - 0.5 - const pooledSocket = this.socketPool.sort(rand).find(s => !s.active) - if (!pooledSocket) return // TODO recover from exausted socket pool - - // mark socket as active & deactivate it after timeout - pooledSocket.active = true - pooledSocket.reclaim = this._setTimeout(() => { - pooledSocket.active = false - pooledSocket.removeAllListeners() - }, 1024) - - pooledSocket.on('message', async (msg, rinfo) => { - // if (rinfo.port !== peerPort || rinfo.address !== peerAddress) return - - // cancel scheduled events - this._clearInterval(interval) - this._clearTimeout(pooledSocket.reclaim) - - // remove any events currently bound on the socket - pooledSocket.removeAllListeners() - pooledSocket.on('message', (msg, rinfo) => { - this._onMessage(msg, rinfo) - }) - - this._onConnection(packet, peer.peerId, rinfo.port, rinfo.address, undefined, pooledSocket) - - const p = { - clusterId, - subclusterId, - clock: this.clock, - message: { - requesterPeerId: this.peerId, - natType: this.natType, - isConnection: true - } - } - - const data = await Packet.encode(new PacketPing(p)) - - pooledSocket.send(data, rinfo.port, rinfo.address) - - // create a new socket to replace it in the pool - const oldIndex = this.socketPool.findIndex(s => s === pooledSocket) - this.socketPool[oldIndex] = dgram.createSocket('udp4', null, this).unref() - - this._onMessage(msg, rinfo) - }) - - try { - pooledSocket.send(data, peerPort, peerAddress) - } catch (err) { - console.error('STRATEGY_TRAVERSAL_CONNECT error', err) - } - }, 10) - - return - } - - if (strategy === NAT.STRATEGY_PROXY && !peer.proxy) { - // TODO could allow multiple proxies - this._onConnection(packet, peer.peerId, peerPort, peerAddress, proxyCandidate) - debug(this.peerId, '++ INTRO CHOSE PROXY STRATEGY') - } - - if (strategy === NAT.STRATEGY_TRAVERSAL_OPEN) { - peer.opening = Date.now() - - const portsCache = new Set() - - if (!this.bdpCache.length) { - globalThis.bdpCache = this.bdpCache = Array.from({ length: 1024 }, () => getRandomPort(portsCache)) - } - - for (const port of this.bdpCache) { - this.send(Buffer.from([0x1]), port, packet.message.address) - } - - return - } - - if (strategy === NAT.STRATEGY_DIRECT_CONNECT) { - debug(this.peerId, '++ NAT STRATEGY_DIRECT_CONNECT') - } - - if (strategy === NAT.STRATEGY_DEFER) { - debug(this.peerId, '++ NAT STRATEGY_DEFER') - } - - this.ping(peer, true, props) - } - - /** - * Received an Join Packet - * @return {undefined} - * @ignore - */ - async _onJoin (packet, port, address, data) { - this.metrics.i[packet.type]++ - - const pid = packet.packetId.toString('hex') - if (packet.message.requesterPeerId === this.peerId) return - if (this.gate.has(pid)) return - if (!packet.clusterId) return - - this.lastUpdate = Date.now() - - const peerId = packet.message.requesterPeerId - const rendezvousDeadline = packet.message.rendezvousDeadline - const clusterId = packet.clusterId - const subclusterId = packet.subclusterId - const peerAddress = packet.message.address - const peerPort = packet.message.port - - // prevents premature pruning; a peer is not directly connecting - const peer = this.peers.find(p => p.peerId === peerId) - if (peer) peer.lastUpdate = Date.now() - - // a rendezvous isn't relevant if it's too old, just drop the packet - if (rendezvousDeadline && rendezvousDeadline < Date.now()) return - - const cid = clusterId.toString('base64') - const scid = subclusterId.toString('base64') - - debug(this.peerId, '<- JOIN (' + - `peerId=${peerId.slice(0, 6)}, ` + - `clock=${packet.clock}, ` + - `hops=${packet.hops}, ` + - `clusterId=${cid}, ` + - `subclusterId=${scid}, ` + - `address=${address}:${port})` - ) - - // - // This packet represents a peer who wants to join the network and is a - // member of our cluster. The packet was replicated though the network - // and contains the details about where the peer can be reached, in this - // case we want to ping that peer so we can be introduced to them. - // - if (rendezvousDeadline && !this.indexed && this.clusters[cid]) { - if (!packet.message.rendezvousRequesterPeerId) { - const pid = packet.packetId.toString('hex') - this.gate.set(pid, 2) - - // TODO it would tighten up the transition time between dropped peers - // if we check strategy from (packet.message.natType, this.natType) and - // make introductions that create more mutually known peers. - debug(this.peerId, `<- JOIN RENDEZVOUS START (to=${peerAddress}:${peerPort}, via=${packet.message.rendezvousAddress}:${packet.message.rendezvousPort})`) - - const data = await Packet.encode(new PacketJoin({ - clock: packet.clock, - subclusterId: packet.subclusterId, - clusterId: packet.clusterId, - message: { - requesterPeerId: this.peerId, - natType: this.natType, - address: this.address, - port: this.port, - rendezvousType: packet.message.natType, - rendezvousRequesterPeerId: packet.message.requesterPeerId - } - })) - - this.send( - data, - packet.message.rendezvousPort, - packet.message.rendezvousAddress - ) - } - } - - const filter = p => ( - p.connected && // you can't intro peers who aren't connected - p.peerId !== packet.message.requesterPeerId && - p.peerId !== packet.message.rendezvousRequesterPeerId && - !p.indexed - ) - - let peers = this.getPeers(packet, this.peers, [{ port, address }], filter) - - // - // A peer who belongs to the same cluster as the peer who's replicated - // join was discovered, sent us a join that has a specification for who - // they want to be introduced to. - // - if (packet.message.rendezvousRequesterPeerId && this.peerId === packet.message.rendezvousPeerId) { - const peer = this.peers.find(p => p.peerId === packet.message.rendezvousRequesterPeerId) - - if (!peer) { - debug(this.peerId, '<- INTRO FROM RENDEZVOUS FAILED', packet) - return - } - - // peer.natType = packet.message.rendezvousType - peers = [peer] - - debug(this.peerId, `<- JOIN EXECUTING RENDEZVOUS (from=${packet.message.address}:${packet.message.port}, to=${peer.address}:${peer.port})`) - } - - for (const peer of peers) { - const message1 = { - requesterPeerId: peer.peerId, - responderPeerId: this.peerId, - isRendezvous: !!packet.message.rendezvousPeerId, - natType: peer.natType, - address: peer.address, - port: peer.port - } - - const message2 = { - requesterPeerId: packet.message.requesterPeerId, - responderPeerId: this.peerId, - isRendezvous: !!packet.message.rendezvousPeerId, - natType: packet.message.natType, - address: packet.message.address, - port: packet.message.port - } - - const opts = { - hops: packet.hops + 1, - clusterId, - subclusterId, - usr1: String(Date.now()) - } - - const intro1 = await Packet.encode(new PacketIntro({ ...opts, message: message1 })) - const intro2 = await Packet.encode(new PacketIntro({ ...opts, message: message2 })) - - // - // Send intro1 to the peer described in the message - // Send intro2 to the peer in this loop - // - debug(this.peerId, `>> INTRO SEND (from=${peer.address}:${peer.port}, to=${packet.message.address}:${packet.message.port})`) - debug(this.peerId, `>> INTRO SEND (from=${packet.message.address}:${packet.message.port}, to=${peer.address}:${peer.port})`) - - peer.lastRequest = Date.now() - - this.send(intro2, peer.port, peer.address) - this.send(intro1, packet.message.port, packet.message.address) - - this.gate.set(Packet.decode(intro1).packetId.toString('hex'), 2) - this.gate.set(Packet.decode(intro2).packetId.toString('hex'), 2) - } - - this.gate.set(packet.packetId.toString('hex'), 2) - - if (packet.hops >= this.maxHops) return - if (this.indexed && !packet.clusterId) return - - if (packet.hops === 1 && this.natType === NAT.UNRESTRICTED && !packet.message.rendezvousDeadline) { - packet.message.rendezvousAddress = this.address - packet.message.rendezvousPort = this.port - packet.message.rendezvousType = this.natType - packet.message.rendezvousPeerId = this.peerId - packet.message.rendezvousDeadline = Date.now() + this.config.keepalive - } - - debug(this.peerId, `-> JOIN RELAY (peerId=${peerId.slice(0, 6)}, from=${peerAddress}:${peerPort})`) - this.mcast(packet, [{ port, address }, { port: peerPort, address: peerAddress }]) - - if (packet.hops <= 1) { - this._onConnection(packet, packet.message.requesterPeerId, port, address) - } - } - - /** - * Received an Publish Packet - * @return {undefined} - * @ignore - */ - async _onPublish (packet, port, address, data) { - this.metrics.i[packet.type]++ - - // only cache if this packet if i am part of this subclusterId - // const cluster = this.clusters[packet.clusterId] - // if (cluster && cluster[packet.subclusterId]) { - - const pid = packet.packetId.toString('hex') - if (this.cache.has(pid)) { - debug(this.peerId, `<- PUBLISH DUPE (packetId=${pid.slice(0, 8)}, from=${address}:${port})`) - return - } - - debug(this.peerId, `<- PUBLISH (packetId=${pid.slice(0, 8)}, from=${address}:${port}, is-sync=${packet.usr4.toString() === 'SYNC'})`) - this.cache.insert(pid, packet) - - const ignorelist = [{ address, port }] - const scid = packet.subclusterId.toString('base64') - - if (!this.indexed && this.encryption.has(scid)) { - let p = packet.copy() - if (p.index > -1) p = await this.cache.compose(p) - if (p?.index === -1 && this.onPacket) this.onPacket(p, port, address) - } - - if (packet.hops >= this.maxHops) return - this.mcast(packet, ignorelist) - - // } - } - - /** - * Received an Stream Packet - * @return {undefined} - * @ignore - */ - async _onStream (packet, port, address, data) { - this.metrics.i[packet.type]++ - - const pid = packet.packetId.toString('hex') - if (this.gate.has(pid)) return - this.gate.set(pid, 1) - - const streamTo = packet.usr3.toString('hex') - const streamFrom = packet.usr4.toString('hex') - - // only help packets with a higher hop count if they are in our cluster - // if (packet.hops > 2 && !this.clusters[packet.cluster]) return - - const peerFrom = this.peers.find(p => p.peerId.toString('hex') === streamFrom.toString('hex')) - if (!peerFrom) return - - // stream message is for this peer - if (streamTo.toString('hex') === this.peerId.toString('hex')) { - const scid = packet.subclusterId.toString('base64') - - if (this.encryption.has(scid)) { - let p = packet.copy() // clone the packet so it's not modified - - if (packet.index > -1) { // if it needs to be composed... - p.timestamp = Date.now() - this.streamBuffer.set(p.packetId.toString('hex'), p) // cache the partial - - p = await this.cache.compose(p, this.streamBuffer) // try to compose - if (!p) return // could not compose - - if (p) { // if successful, delete the artifacts - const previousId = p.index === 0 ? p.packetId : p.previousId - const pid = previousId.toString('hex') - - this.streamBuffer.forEach((v, k) => { - if (k === pid) this.streamBuffer.delete(k) - if (v.previousId.toString('hex') === pid) this.streamBuffer.delete(k) - }) - } - } - - if (this.onStream) this.onStream(p, peerFrom, port, address) - } - - return - } - - // stream message is for another peer - const peerTo = this.peers.find(p => p.peerId === streamTo) - if (!peerTo) { - debug(this.peerId, `XX STREAM RELAY FORWARD DESTINATION NOT REACHABLE (to=${streamTo})`) - return - } - - if (packet.hops >= this.maxHops) { - debug(this.peerId, `XX STREAM RELAY MAX HOPS EXCEEDED (to=${streamTo})`) - return - } - - debug(this.peerId, `>> STREAM RELAY (to=${peerTo.address}:${peerTo.port}, id=${peerTo.peerId.slice(0, 6)})`) - this.send(await Packet.encode(packet), peerTo.port, peerTo.address) - if (packet.hops <= 2 && this.natType === NAT.UNRESTRICTED) this.mcast(packet) - } - - /** - * Received any packet on the probe port to determine the firewall: - * are you port restricted, host restricted, or unrestricted. - * @return {undefined} - * @ignore - */ - _onProbeMessage (data, { port, address }) { - this._clearTimeout(this.probeReflectionTimeout) - - const packet = Packet.decode(data) - if (!packet || packet.version !== VERSION) return - if (packet?.type !== 2) return - - const pid = packet.packetId.toString('hex') - if (this.gate.has(pid)) return - this.gate.set(pid, 1) - - const { reflectionId } = packet.message - debug(this.peerId, `<- NAT PROBE (from=${address}:${port}, stage=${this.reflectionStage}, id=${reflectionId})`) - - if (this.onProbe) this.onProbe(data, port, address) - if (this.reflectionId !== reflectionId || !this.reflectionId) return - - // reflection stage is encoded in the last hex char of the reflectionId, or 0 if not available. - // const reflectionStage = reflectionId ? parseInt(reflectionId.slice(-1), 16) : 0 - - if (this.reflectionStage === 1) { - debug(this.peerId, '<- NAT REFLECT - STAGE1: probe received', reflectionId) - if (!packet.message?.port) return // message must include a port number - - // successfully discovered the probe socket external port - this.config.probeExternalPort = packet.message.port - - // move to next reflection stage - this.reflectionStage = 1 - this.reflectionId = null - this.requestReflection() - return - } - - if (this.reflectionStage === 2) { - debug(this.peerId, '<- NAT REFLECT - STAGE2: probe received', reflectionId) - - // if we have previously sent an outbount message to this peer on the probe port - // then our NAT will have a mapping for their IP, but not their IP+Port. - if (!NAT.isFirewallDefined(this.nextNatType)) { - this.nextNatType |= NAT.FIREWALL_ALLOW_KNOWN_IP - debug(this.peerId, `<> PROBE STATUS: NAT.FIREWALL_ALLOW_KNOWN_IP (${packet.message.port} -> ${this.nextNatType})`) - } else { - this.nextNatType |= NAT.FIREWALL_ALLOW_ANY - debug(this.peerId, `<> PROBE STATUS: NAT.FIREWALL_ALLOW_ANY (${packet.message.port} -> ${this.nextNatType})`) - } - - // wait for all messages to arrive - } - } - - /** - * When a packet is received it is decoded, the packet contains the type - * of the message. Based on the message type it is routed to a function. - * like WebSockets, don't answer queries unless we know its another SRP peer. - * - * @param {Buffer|Uint8Array} data - * @param {{ port: number, address: string }} info - */ - async _onMessage (data, { port, address }) { - const packet = Packet.decode(data) - if (!packet || packet.version !== VERSION) return - - const peer = this.peers.find(p => p.address === address && p.port === port) - if (peer) peer.lastUpdate = Date.now() - - const cid = packet.clusterId.toString('base64') - const scid = packet.subclusterId.toString('base64') - - // debug('<- PACKET', packet.type, port, address) - const clusters = this.clusters[cid] - const subcluster = clusters && clusters[scid] - - if (!this.config.limitExempt) { - if (rateLimit(this.rates, packet.type, port, address, subcluster)) { - debug(this.peerId, `XX RATE LIMIT HIT (from=${address}, type=${packet.type})`) - this.metrics.i.REJECTED++ - return - } - if (this.onLimit && !this.onLimit(packet, port, address)) return - } - - const args = [packet, port, address, data] - - if (this.firewall) if (!this.firewall(...args)) return - if (this.onData) this.onData(...args) - - switch (packet.type) { - case PacketPing.type: return this._onPing(...args) - case PacketPong.type: return this._onPong(...args) - } - - if (!this.natType && !this.indexed) return - - switch (packet.type) { - case PacketIntro.type: return this._onIntro(...args) - case PacketJoin.type: return this._onJoin(...args) - case PacketPublish.type: return this._onPublish(...args) - case PacketStream.type: return this._onStream(...args) - case PacketSync.type: return this._onSync(...args) - case PacketQuery.type: return this._onQuery(...args) - } - } - } - - return Peer -} - -export default wrap diff --git a/api/stream-relay/sugar.js b/api/stream-relay/sugar.js deleted file mode 100644 index 0fef434235..0000000000 --- a/api/stream-relay/sugar.js +++ /dev/null @@ -1,373 +0,0 @@ -import { wrap, Encryption, sha256, NAT, RemotePeer } from './index.js' -import { sodium } from '../crypto.js' -import { Buffer } from '../buffer.js' -import { isBufferLike } from '../util.js' -import { Packet, CACHE_TTL } from './packets.js' - -/** - * Creates and manages a network bus for communication. - * - * @module Network - * @param {object} dgram - The dgram module for network communication. - * @param {object} events - The events module for event handling. - * @returns {Promise<events.EventEmitter>} - A promise that resolves to the network bus. - */ -export default (dgram, events) => { - let _peer = null - let bus = null - - /** - * Initializes and returns the network bus. - * - * @async - * @function - * @param {object} options - Configuration options for the network bus. - * @returns {Promise<events.EventEmitter>} - A promise that resolves to the initialized network bus. - */ - return async (options = {}) => { - if (bus) return bus - - await sodium.ready - bus = new events.EventEmitter() - bus._on = bus.on - bus._once = bus.once - bus._emit = bus.emit - - if (!options.indexed) { - if (!options.clusterId && !options.config?.clusterId) { - throw new Error('expected options.clusterId') - } - - if (typeof options.signingKeys !== 'object') throw new Error('expected options.signingKeys to be of type Object') - if (options.signingKeys.publicKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.publicKey to be of type Uint8Array') - if (options.signingKeys.privateKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.privateKey to be of type Uint8Array') - } - - let clusterId = bus.clusterId = options.clusterId || options.config?.clusterId - - if (clusterId) clusterId = Buffer.from(clusterId) // some peers don't have clusters - - _peer = new (wrap(dgram))(options) // only one peer per process makes sense - - _peer.onConnection = (packet, ...args) => { - } - - _peer.onJoin = (packet, ...args) => { - if (!packet.clusterId.equals(clusterId)) return - bus._emit('#join', packet, ...args) - } - - _peer.onPacket = (packet, ...args) => { - if (!packet.clusterId.equals(clusterId)) return - bus._emit('#packet', packet, ...args) - } - - _peer.onStream = (packet, ...args) => { - if (!packet.clusterId.equals(clusterId)) return - bus._emit('#stream', packet, ...args) - } - - _peer.onData = (...args) => bus._emit('#data', ...args) - _peer.onSend = (...args) => bus._emit('#send', ...args) - _peer.onFirewall = (...args) => bus._emit('#firewall', ...args) - _peer.onMulticast = (...args) => bus._emit('#multicast', ...args) - _peer.onJoin = (...args) => bus._emit('#join', ...args) - _peer.onSync = (...args) => bus._emit('#sync', ...args) - _peer.onSyncStart = (...args) => bus._emit('#sync-start', ...args) - _peer.onSyncEnd = (...args) => bus._emit('#sync-end', ...args) - _peer.onConnection = (...args) => bus._emit('#connection', ...args) - _peer.onDisconnection = (...args) => bus._emit('#disconnection', ...args) - _peer.onQuery = (...args) => bus._emit('#query', ...args) - _peer.onNat = (...args) => bus._emit('#network-change', ...args) - _peer.onWarn = (...args) => bus._emit('#warning', ...args) - _peer.onState = (...args) => bus._emit('#state', ...args) - _peer.onConnecting = (...args) => bus._emit('#connecting', ...args) - - // TODO check if its not a network error - _peer.onError = (...args) => bus._emit('#error', ...args) - - _peer.onReady = () => { - _peer.isReady = true - bus._emit('#ready', bus.address()) - } - - bus.peer = _peer - bus.peerId = _peer.peerId - - bus.subclusters = new Map() - - /** - * Gets the address information of the network peer. - * - * @function - * @returns {object} - The address information. - */ - bus.address = () => ({ - address: _peer.address, - port: _peer.port, - natType: NAT.toString(_peer.natType) - }) - - /** - * Indexes a new peer in the network. - * - * @function - * @param {object} params - Peer information. - * @param {string} params.peerId - The peer ID. - * @param {string} params.address - The peer address. - * @param {number} params.port - The peer port. - * @throws {Error} - Throws an error if required parameters are missing. - */ - bus.indexPeer = ({ peerId, address, port }) => { - if (!peerId) throw new Error('options.peerId required') - if (!address) throw new Error('options.address required') - if (!port) throw new Error('options.port required') - - _peer.peers.push(new RemotePeer({ peerId, address, port, indexed: true })) - } - - bus.reconnect = () => { - _peer.lastUpdate = 0 - _peer.requestReflection() - } - - bus.disconnect = () => { - _peer.natType = null - _peer.reflectionStage = 0 - _peer.reflectionId = null - _peer.reflectionTimeout = null - _peer.probeReflectionTimeout = null - } - - bus.sealMessage = (m, v = options.signingKeys) => _peer.encryption.sealMessage(m, v) - bus.openMessage = (m, v = options.signingKeys) => _peer.encryption.openMessage(m, v) - - bus.seal = (m, v = options.signingKeys) => _peer.encryption.seal(m, v) - bus.open = (m, v = options.signingKeys) => _peer.encryption.open(m, v) - - bus.query = (...args) => _peer.query(...args) - bus.state = () => _peer.getState() - bus.cacheSize = () => _peer.cache.size - bus.cacheBytes = () => _peer.cache.bytes - - const pack = async (eventName, value, opts = {}) => { - if (typeof eventName !== 'string') throw new Error('event name must be a string') - if (eventName.length === 0) throw new Error('event name too short') - - if (opts.ttl) opts.ttl = Math.min(opts.ttl, CACHE_TTL) - - const args = { - clusterId, - ...opts, - usr1: await sha256(eventName, { bytes: true }) - } - - if (!isBufferLike(value) && typeof value === 'object') { - try { - args.message = Buffer.from(JSON.stringify(value)) - } catch (err) { - return bus._emit('error', err) - } - } else { - args.message = Buffer.from(value) - } - - args.usr2 = Buffer.from(options.signingKeys.publicKey) - args.sig = Encryption.sign(args.message, options.signingKeys.privateKey) - - return args - } - - const unpack = async packet => { - let opened - let verified - const sub = bus.subclusters.get(packet.subclusterId.toString('base64')) - if (!sub) return {} - - try { - opened = _peer.encryption.open(packet.message, packet.subclusterId.toString('base64')) - } catch (err) { - sub._emit('warning', err) - return {} - } - - if (packet.sig) { - try { - if (Encryption.verify(opened, packet.sig, packet.usr2)) { - verified = true - } - } catch (err) { - sub._emit('warning', err) - return {} - } - } - - return { opened, verified } - } - - /** - * Publishes an event to the network bus. - * - * @async - * @function - * @param {string} eventName - The name of the event. - * @param {any} value - The value associated with the event. - * @param {object} opts - Additional options for publishing. - * @returns {Promise<any>} - A promise that resolves to the published event details. - */ - bus.emit = async (eventName, value, opts = {}) => { - return await _peer.publish(options.sharedKey, await pack(eventName, value, opts)) - } - - bus.on = async (eventName, cb) => { - if (eventName[0] !== '#') eventName = await sha256(eventName) - bus._on(eventName, cb) - } - - bus.subcluster = async (options = {}) => { - if (!options.sharedKey?.constructor.name) { - throw new Error('expected options.sharedKey to be of type Uint8Array') - } - - const derivedKeys = await Encryption.createKeyPair(options.sharedKey) - const subclusterId = Buffer.from(derivedKeys.publicKey) - const scid = subclusterId.toString('base64') - - if (bus.subclusters.has(scid)) return bus.subclusters.get(scid) - - const sub = new events.EventEmitter() - sub._emit = sub.emit - sub._on = sub.on - sub.peers = new Map() - - bus.subclusters.set(scid, sub) - - sub.peerId = _peer.peerId - sub.subclusterId = subclusterId - sub.sharedKey = options.sharedKey - sub.derivedKeys = derivedKeys - - sub.emit = async (eventName, value, opts = {}) => { - opts.clusterId = opts.clusterId || clusterId - opts.subclusterId = opts.subclusterId || sub.subclusterId - - const args = await pack(eventName, value, opts) - - if (sub.peers.values().length) { - let packets = [] - - for (const p of sub.peers.values()) { - const r = await p._peer.write(sub.sharedKey, args) - if (packets.length === 0) packets = r - } - - for (const packet of packets) { - const p = Packet.from(packet) - _peer.cache.insert(packet.packetId.toString('hex'), p) - - _peer.unpublished[packet.packetId.toString('hex')] = Date.now() - if (globalThis.navigator && !globalThis.navigator.onLine) continue - - _peer.mcast(packet) - } - return packets - } else { - const packets = await _peer.publish(sub.sharedKey, args) - return packets - } - } - - sub.on = async (eventName, cb) => { - if (eventName[0] !== '#') eventName = await sha256(eventName) - sub._on(eventName, cb) - } - - sub.off = async (eventName, fn) => { - if (eventName[0] !== '#') eventName = await sha256(eventName) - sub.removeListener(eventName, fn) - } - - sub.join = () => _peer.join(sub.sharedKey, options) - - bus._on('#ready', () => { - const subcluster = bus.subclusters.get(sub.subclusterId.toString('base64')) - if (subcluster) _peer.join(subcluster.sharedKey, options) - }) - - _peer.join(sub.sharedKey, options) - return sub - } - - bus._on('#join', async (packet, peer) => { - const sub = bus.subclusters.get(packet.subclusterId.toString('base64')) - if (!sub) return - - let ee = sub.peers.get(peer.peerId) - - if (!ee) { - ee = new events.EventEmitter() - - ee._on = ee.on - ee._emit = ee.emit - - ee.peerId = peer.peerId - ee.address = peer.address - ee.port = peer.port - - ee.emit = async (eventName, value, opts = {}) => { - if (!ee._peer.write) return - - opts.clusterId = opts.clusterId || clusterId - opts.subclusterId = opts.subclusterId || sub.subclusterId - - const args = await pack(eventName, value, opts) - return peer.write(sub.sharedKey, args) - } - - ee.on = async (eventName, cb) => { - if (eventName[0] !== '#') eventName = await sha256(eventName) - ee._on(eventName, cb) - } - } - - const oldPeer = sub.peers.has(peer.peerId) - const portChange = oldPeer.port !== peer.port - const addressChange = oldPeer.address !== peer.address - const natChange = oldPeer.natType !== peer.natType - const change = portChange || addressChange || natChange - - ee._peer = peer - - sub.peers.set(peer.peerId, ee) - if (!oldPeer || change) sub._emit('#join', ee, packet) - }) - - const handlePacket = async (packet, peer, port, address) => { - const scid = packet.subclusterId.toString('base64') - const sub = bus.subclusters.get(scid) - if (!sub) return - - const eventName = packet.usr1.toString('hex') - const { verified, opened } = await unpack(packet) - if (verified) packet.verified = true - - sub._emit(eventName, opened, packet) - - const ee = sub.peers.get(packet.streamFrom || peer?.peerId) - if (ee) ee._emit(eventName, opened, packet) - } - - bus._on('#stream', handlePacket) - bus._on('#packet', handlePacket) - - bus._on('#disconnection', peer => { - for (const sub of bus.subclusters) { - sub._emit('#leave', peer) - sub.peers.delete(peer.peerId) - } - }) - - await _peer.init() - return bus - } -} diff --git a/api/stream.js b/api/stream.js index afae1294cd..f98b7499bf 100644 --- a/api/stream.js +++ b/api/stream.js @@ -23,16 +23,18 @@ **/ /** - * @module Stream + * @module stream */ import { EventEmitter } from './events.js' -import * as exports from './stream.js' +import web from './stream/web.js' -export default exports +export { web } const STREAM_DESTROYED = new Error('Stream was destroyed') const PREMATURE_CLOSE = new Error('Premature close') +const queueTick = queueMicrotask + export class FixedFIFO { constructor (hwm) { if (!(hwm > 0) || ((hwm - 1) & hwm) !== 0) throw new Error('Max size for a FixedFIFO should be a power of two') @@ -43,6 +45,12 @@ export class FixedFIFO { this.next = null } + clear () { + this.top = this.btm = 0 + this.next = null + this.buffer.fill(undefined) + } + push (data) { if (this.buffer[this.top] !== undefined) return false this.buffer[this.top] = data @@ -58,19 +66,31 @@ export class FixedFIFO { return last } + peek () { + return this.buffer[this.btm] + } + isEmpty () { return this.buffer[this.btm] === undefined } } -export class FastFIFO { +export class FIFO { constructor (hwm) { this.hwm = hwm || 16 this.head = new FixedFIFO(this.hwm) this.tail = this.head + this.length = 0 + } + + clear () { + this.head = this.tail + this.head.clear() + this.length = 0 } push (val) { + this.length++ if (!this.head.push(val)) { const prev = this.head this.head = prev.next = new FixedFIFO(2 * this.head.buffer.length) @@ -79,6 +99,7 @@ export class FastFIFO { } shift () { + if (this.length !== 0) this.length-- const val = this.tail.shift() if (val === undefined && this.tail.next) { const next = this.tail.next @@ -86,95 +107,111 @@ export class FastFIFO { this.tail = next return this.tail.shift() } + + return val + } + + peek () { + const val = this.tail.peek() + if (val === undefined && this.tail.next) return this.tail.next.peek() return val } isEmpty () { - return this.head.isEmpty() + return this.length === 0 } } -const FIFO = FastFIFO /* eslint-disable no-multi-spaces */ -const MAX = ((1 << 25) - 1) +// 28 bits used total (4 from shared, 14 from read, and 10 from write) +const MAX = ((1 << 28) - 1) // Shared state -const OPENING = 0b001 -const DESTROYING = 0b010 -const DESTROYED = 0b100 +const OPENING = 0b0001 +const PREDESTROYING = 0b0010 +const DESTROYING = 0b0100 +const DESTROYED = 0b1000 const NOT_OPENING = MAX ^ OPENING +const NOT_PREDESTROYING = MAX ^ PREDESTROYING + +// Read state (4 bit offset from shared state) +const READ_ACTIVE = 0b00000000000001 << 4 +const READ_UPDATING = 0b00000000000010 << 4 +const READ_PRIMARY = 0b00000000000100 << 4 +const READ_QUEUED = 0b00000000001000 << 4 +const READ_RESUMED = 0b00000000010000 << 4 +const READ_PIPE_DRAINED = 0b00000000100000 << 4 +const READ_ENDING = 0b00000001000000 << 4 +const READ_EMIT_DATA = 0b00000010000000 << 4 +const READ_EMIT_READABLE = 0b00000100000000 << 4 +const READ_EMITTED_READABLE = 0b00001000000000 << 4 +const READ_DONE = 0b00010000000000 << 4 +const READ_NEXT_TICK = 0b00100000000000 << 4 +const READ_NEEDS_PUSH = 0b01000000000000 << 4 +const READ_READ_AHEAD = 0b10000000000000 << 4 -// Read state -const READ_ACTIVE = 0b0000000000001 << 3 -const READ_PRIMARY = 0b0000000000010 << 3 -const READ_SYNC = 0b0000000000100 << 3 -const READ_QUEUED = 0b0000000001000 << 3 -const READ_RESUMED = 0b0000000010000 << 3 -const READ_PIPE_DRAINED = 0b0000000100000 << 3 -const READ_ENDING = 0b0000001000000 << 3 -const READ_EMIT_DATA = 0b0000010000000 << 3 -const READ_EMIT_READABLE = 0b0000100000000 << 3 -const READ_EMITTED_READABLE = 0b0001000000000 << 3 -const READ_DONE = 0b0010000000000 << 3 -const READ_NEXT_TICK = 0b0100000000001 << 3 // also active -const READ_NEEDS_PUSH = 0b1000000000000 << 3 +// Combined read state +const READ_FLOWING = READ_RESUMED | READ_PIPE_DRAINED +const READ_ACTIVE_AND_NEEDS_PUSH = READ_ACTIVE | READ_NEEDS_PUSH +const READ_PRIMARY_AND_ACTIVE = READ_PRIMARY | READ_ACTIVE +const READ_EMIT_READABLE_AND_QUEUED = READ_EMIT_READABLE | READ_QUEUED +const READ_RESUMED_READ_AHEAD = READ_RESUMED | READ_READ_AHEAD const READ_NOT_ACTIVE = MAX ^ READ_ACTIVE const READ_NON_PRIMARY = MAX ^ READ_PRIMARY const READ_NON_PRIMARY_AND_PUSHED = MAX ^ (READ_PRIMARY | READ_NEEDS_PUSH) -const READ_NOT_SYNC = MAX ^ READ_SYNC const READ_PUSHED = MAX ^ READ_NEEDS_PUSH const READ_PAUSED = MAX ^ READ_RESUMED const READ_NOT_QUEUED = MAX ^ (READ_QUEUED | READ_EMITTED_READABLE) const READ_NOT_ENDING = MAX ^ READ_ENDING -const READ_PIPE_NOT_DRAINED = MAX ^ (READ_RESUMED | READ_PIPE_DRAINED) +const READ_PIPE_NOT_DRAINED = MAX ^ READ_FLOWING const READ_NOT_NEXT_TICK = MAX ^ READ_NEXT_TICK - -// Write state -const WRITE_ACTIVE = 0b000000001 << 16 -const WRITE_PRIMARY = 0b000000010 << 16 -const WRITE_SYNC = 0b000000100 << 16 -const WRITE_QUEUED = 0b000001000 << 16 -const WRITE_UNDRAINED = 0b000010000 << 16 -const WRITE_DONE = 0b000100000 << 16 -const WRITE_EMIT_DRAIN = 0b001000000 << 16 -const WRITE_NEXT_TICK = 0b010000001 << 16 // also active -const WRITE_FINISHING = 0b100000000 << 16 - -const WRITE_NOT_ACTIVE = MAX ^ WRITE_ACTIVE -const WRITE_NOT_SYNC = MAX ^ WRITE_SYNC +const READ_NOT_UPDATING = MAX ^ READ_UPDATING +const READ_NO_READ_AHEAD = MAX ^ READ_READ_AHEAD +const READ_PAUSED_NO_READ_AHEAD = MAX ^ READ_RESUMED_READ_AHEAD + +// Write state (18 bit offset, 4 bit offset from shared state and 13 from read state) +const WRITE_ACTIVE = 0b0000000001 << 18 +const WRITE_UPDATING = 0b0000000010 << 18 +const WRITE_PRIMARY = 0b0000000100 << 18 +const WRITE_QUEUED = 0b0000001000 << 18 +const WRITE_UNDRAINED = 0b0000010000 << 18 +const WRITE_DONE = 0b0000100000 << 18 +const WRITE_EMIT_DRAIN = 0b0001000000 << 18 +const WRITE_NEXT_TICK = 0b0010000000 << 18 +const WRITE_WRITING = 0b0100000000 << 18 +const WRITE_FINISHING = 0b1000000000 << 18 + +const WRITE_NOT_ACTIVE = MAX ^ (WRITE_ACTIVE | WRITE_WRITING) const WRITE_NON_PRIMARY = MAX ^ WRITE_PRIMARY const WRITE_NOT_FINISHING = MAX ^ WRITE_FINISHING const WRITE_DRAINED = MAX ^ WRITE_UNDRAINED const WRITE_NOT_QUEUED = MAX ^ WRITE_QUEUED const WRITE_NOT_NEXT_TICK = MAX ^ WRITE_NEXT_TICK +const WRITE_NOT_UPDATING = MAX ^ WRITE_UPDATING // Combined shared state const ACTIVE = READ_ACTIVE | WRITE_ACTIVE const NOT_ACTIVE = MAX ^ ACTIVE const DONE = READ_DONE | WRITE_DONE -const DESTROY_STATUS = DESTROYING | DESTROYED +const DESTROY_STATUS = DESTROYING | DESTROYED | PREDESTROYING const OPEN_STATUS = DESTROY_STATUS | OPENING const AUTO_DESTROY = DESTROY_STATUS | DONE const NON_PRIMARY = WRITE_NON_PRIMARY & READ_NON_PRIMARY -const TICKING = (WRITE_NEXT_TICK | READ_NEXT_TICK) & NOT_ACTIVE -const ACTIVE_OR_TICKING = ACTIVE | TICKING +const ACTIVE_OR_TICKING = WRITE_NEXT_TICK | READ_NEXT_TICK +const TICKING = ACTIVE_OR_TICKING & NOT_ACTIVE const IS_OPENING = OPEN_STATUS | TICKING -// Combined read state +// Combined shared state and read state const READ_PRIMARY_STATUS = OPEN_STATUS | READ_ENDING | READ_DONE const READ_STATUS = OPEN_STATUS | READ_DONE | READ_QUEUED -const READ_FLOWING = READ_RESUMED | READ_PIPE_DRAINED -const READ_ACTIVE_AND_SYNC = READ_ACTIVE | READ_SYNC -const READ_ACTIVE_AND_SYNC_AND_NEEDS_PUSH = READ_ACTIVE | READ_SYNC | READ_NEEDS_PUSH -const READ_PRIMARY_AND_ACTIVE = READ_PRIMARY | READ_ACTIVE const READ_ENDING_STATUS = OPEN_STATUS | READ_ENDING | READ_QUEUED -const READ_EMIT_READABLE_AND_QUEUED = READ_EMIT_READABLE | READ_QUEUED const READ_READABLE_STATUS = OPEN_STATUS | READ_EMIT_READABLE | READ_QUEUED | READ_EMITTED_READABLE -const SHOULD_NOT_READ = OPEN_STATUS | READ_ACTIVE | READ_ENDING | READ_DONE | READ_NEEDS_PUSH +const SHOULD_NOT_READ = OPEN_STATUS | READ_ACTIVE | READ_ENDING | READ_DONE | READ_NEEDS_PUSH | READ_READ_AHEAD const READ_BACKPRESSURE_STATUS = DESTROY_STATUS | READ_ENDING | READ_DONE +const READ_UPDATE_SYNC_STATUS = READ_UPDATING | OPEN_STATUS | READ_NEXT_TICK | READ_PRIMARY // Combined write state const WRITE_PRIMARY_STATUS = OPEN_STATUS | WRITE_FINISHING | WRITE_DONE @@ -183,11 +220,12 @@ const WRITE_QUEUED_AND_ACTIVE = WRITE_QUEUED | WRITE_ACTIVE const WRITE_DRAIN_STATUS = WRITE_QUEUED | WRITE_UNDRAINED | OPEN_STATUS | WRITE_ACTIVE const WRITE_STATUS = OPEN_STATUS | WRITE_ACTIVE | WRITE_QUEUED const WRITE_PRIMARY_AND_ACTIVE = WRITE_PRIMARY | WRITE_ACTIVE -const WRITE_ACTIVE_AND_SYNC = WRITE_ACTIVE | WRITE_SYNC -const WRITE_FINISHING_STATUS = OPEN_STATUS | WRITE_FINISHING | WRITE_QUEUED +const WRITE_ACTIVE_AND_WRITING = WRITE_ACTIVE | WRITE_WRITING +const WRITE_FINISHING_STATUS = OPEN_STATUS | WRITE_FINISHING | WRITE_QUEUED_AND_ACTIVE | WRITE_DONE const WRITE_BACKPRESSURE_STATUS = WRITE_UNDRAINED | DESTROY_STATUS | WRITE_FINISHING | WRITE_DONE +const WRITE_UPDATE_SYNC_STATUS = WRITE_UPDATING | OPEN_STATUS | WRITE_NEXT_TICK | WRITE_PRIMARY -const asyncIterator = Symbol.asyncIterator || Symbol.for('asyncIterator') +const asyncIterator = Symbol.asyncIterator || Symbol('asyncIterator') export class WritableState { constructor (stream, { highWaterMark = 16384, map = null, mapWritable, byteLength, byteLengthWritable } = {}) { @@ -197,6 +235,7 @@ export class WritableState { this.buffered = 0 this.error = null this.pipeline = null + this.drains = null // if we add more seldomly used helpers we might them into a subobject so its a single ptr this.byteLength = byteLengthWritable || byteLength || defaultByteLength this.map = mapWritable || map this.afterWrite = afterWrite.bind(this) @@ -224,10 +263,9 @@ export class WritableState { shift () { const data = this.queue.shift() - const stream = this.stream this.buffered -= this.byteLength(data) - if (this.buffered === 0) stream._duplexState &= WRITE_NOT_QUEUED + if (this.buffered === 0) this.stream._duplexState &= WRITE_NOT_QUEUED return data } @@ -254,14 +292,19 @@ export class WritableState { update () { const stream = this.stream - while ((stream._duplexState & WRITE_STATUS) === WRITE_QUEUED) { - const data = this.shift() - stream._duplexState |= WRITE_ACTIVE_AND_SYNC - stream._write(data, this.afterWrite) - stream._duplexState &= WRITE_NOT_SYNC - } + stream._duplexState |= WRITE_UPDATING + + do { + while ((stream._duplexState & WRITE_STATUS) === WRITE_QUEUED) { + const data = this.shift() + stream._duplexState |= WRITE_ACTIVE_AND_WRITING + stream._write(data, this.afterWrite) + } + + if ((stream._duplexState & WRITE_PRIMARY_AND_ACTIVE) === 0) this.updateNonPrimary() + } while (this.continueUpdate() === true) - if ((stream._duplexState & WRITE_PRIMARY_AND_ACTIVE) === 0) this.updateNonPrimary() + stream._duplexState &= WRITE_NOT_UPDATING } updateNonPrimary () { @@ -287,10 +330,21 @@ export class WritableState { } } + continueUpdate () { + if ((this.stream._duplexState & WRITE_NEXT_TICK) === 0) return false + this.stream._duplexState &= WRITE_NOT_NEXT_TICK + return true + } + + updateCallback () { + if ((this.stream._duplexState & WRITE_UPDATE_SYNC_STATUS) === WRITE_PRIMARY) this.update() + else this.updateNextTick() + } + updateNextTick () { if ((this.stream._duplexState & WRITE_NEXT_TICK) !== 0) return this.stream._duplexState |= WRITE_NEXT_TICK - queueMicrotask(this.afterUpdateNextTick) + if ((this.stream._duplexState & WRITE_UPDATING) === 0) queueTick(this.afterUpdateNextTick) } } @@ -298,8 +352,9 @@ export class ReadableState { constructor (stream, { highWaterMark = 16384, map = null, mapReadable, byteLength, byteLengthReadable } = {}) { this.stream = stream this.queue = new FIFO() - this.highWaterMark = highWaterMark + this.highWaterMark = highWaterMark === 0 ? 1 : highWaterMark this.buffered = 0 + this.readAhead = highWaterMark > 0 this.error = null this.pipeline = null this.byteLength = byteLengthReadable || byteLength || defaultByteLength @@ -315,10 +370,11 @@ export class ReadableState { pipe (pipeTo, cb) { if (this.pipeTo !== null) throw new Error('Can only pipe to one destination') + if (typeof cb !== 'function') cb = null this.stream._duplexState |= READ_PIPE_DRAINED this.pipeTo = pipeTo - this.pipeline = new Pipeline(this.stream, pipeTo, cb || null) + this.pipeline = new Pipeline(this.stream, pipeTo, cb) if (cb) this.stream.on('error', noop) // We already error handle this so supress crashes @@ -366,18 +422,16 @@ export class ReadableState { } unshift (data) { - let tail - const pending = [] + const pending = [this.map !== null ? this.map(data) : data] + while (this.buffered > 0) pending.push(this.shift()) - while ((tail = this.queue.shift()) !== undefined) { - pending.push(tail) + for (let i = 0; i < pending.length - 1; i++) { + const data = pending[i] + this.buffered += this.byteLength(data) + this.queue.push(data) } - this.push(data) - - for (let i = 0; i < pending.length; i++) { - this.queue.push(pending[i]) - } + this.push(pending[pending.length - 1]) } read () { @@ -390,6 +444,11 @@ export class ReadableState { return data } + if (this.readAhead === false) { + stream._duplexState |= READ_READ_AHEAD + this.updateNextTick() + } + return null } @@ -406,21 +465,26 @@ export class ReadableState { update () { const stream = this.stream - this.drain() + stream._duplexState |= READ_UPDATING - while (this.buffered < this.highWaterMark && (stream._duplexState & SHOULD_NOT_READ) === 0) { - stream._duplexState |= READ_ACTIVE_AND_SYNC_AND_NEEDS_PUSH - stream._read(this.afterRead) - stream._duplexState &= READ_NOT_SYNC - if ((stream._duplexState & READ_ACTIVE) === 0) this.drain() - } + do { + this.drain() - if ((stream._duplexState & READ_READABLE_STATUS) === READ_EMIT_READABLE_AND_QUEUED) { - stream._duplexState |= READ_EMITTED_READABLE - stream.emit('readable') - } + while (this.buffered < this.highWaterMark && (stream._duplexState & SHOULD_NOT_READ) === READ_READ_AHEAD) { + stream._duplexState |= READ_ACTIVE_AND_NEEDS_PUSH + stream._read(this.afterRead) + this.drain() + } + + if ((stream._duplexState & READ_READABLE_STATUS) === READ_EMIT_READABLE_AND_QUEUED) { + stream._duplexState |= READ_EMITTED_READABLE + stream.emit('readable') + } + + if ((stream._duplexState & READ_PRIMARY_AND_ACTIVE) === 0) this.updateNonPrimary() + } while (this.continueUpdate() === true) - if ((stream._duplexState & READ_PRIMARY_AND_ACTIVE) === 0) this.updateNonPrimary() + stream._duplexState &= READ_NOT_UPDATING } updateNonPrimary () { @@ -447,10 +511,21 @@ export class ReadableState { } } + continueUpdate () { + if ((this.stream._duplexState & READ_NEXT_TICK) === 0) return false + this.stream._duplexState &= READ_NOT_NEXT_TICK + return true + } + + updateCallback () { + if ((this.stream._duplexState & READ_UPDATE_SYNC_STATUS) === READ_PRIMARY) this.update() + else this.updateNextTick() + } + updateNextTick () { if ((this.stream._duplexState & READ_NEXT_TICK) !== 0) return this.stream._duplexState |= READ_NEXT_TICK - queueMicrotask(this.afterUpdateNextTick) + if ((this.stream._duplexState & READ_UPDATING) === 0) queueTick(this.afterUpdateNextTick) } } @@ -507,7 +582,7 @@ export class Pipeline { function afterDrain () { this.stream._duplexState |= READ_PIPE_DRAINED - if ((this.stream._duplexState & READ_ACTIVE_AND_SYNC) === 0) this.updateNextTick() + this.updateCallback() } function afterFinal (err) { @@ -522,7 +597,10 @@ function afterFinal (err) { } stream._duplexState &= WRITE_NOT_ACTIVE - this.update() + + // no need to wait the extra tick here, so we short circuit that + if ((stream._duplexState & WRITE_UPDATING) === 0) this.update() + else this.updateNextTick() } function afterDestroy (err) { @@ -537,7 +615,11 @@ function afterDestroy (err) { const ws = stream._writableState if (rs !== null && rs.pipeline !== null) rs.pipeline.done(stream, err) - if (ws !== null && ws.pipeline !== null) ws.pipeline.done(stream, err) + + if (ws !== null) { + while (ws.drains !== null && ws.drains.length > 0) ws.drains.shift().resolve(false) + if (ws.pipeline !== null) ws.pipeline.done(stream, err) + } } function afterWrite (err) { @@ -546,6 +628,8 @@ function afterWrite (err) { if (err) stream.destroy(err) stream._duplexState &= WRITE_NOT_ACTIVE + if (this.drains !== null) tickDrains(this.drains) + if ((stream._duplexState & WRITE_DRAIN_STATUS) === WRITE_UNDRAINED) { stream._duplexState &= WRITE_DRAINED if ((stream._duplexState & WRITE_EMIT_DRAIN) === WRITE_EMIT_DRAIN) { @@ -553,23 +637,38 @@ function afterWrite (err) { } } - if ((stream._duplexState & WRITE_SYNC) === 0) this.update() + this.updateCallback() } function afterRead (err) { if (err) this.stream.destroy(err) this.stream._duplexState &= READ_NOT_ACTIVE - if ((this.stream._duplexState & READ_SYNC) === 0) this.update() + if (this.readAhead === false && (this.stream._duplexState & READ_RESUMED) === 0) this.stream._duplexState &= READ_NO_READ_AHEAD + this.updateCallback() } function updateReadNT () { - this.stream._duplexState &= READ_NOT_NEXT_TICK - this.update() + if ((this.stream._duplexState & READ_UPDATING) === 0) { + this.stream._duplexState &= READ_NOT_NEXT_TICK + this.update() + } } function updateWriteNT () { - this.stream._duplexState &= WRITE_NOT_NEXT_TICK - this.update() + if ((this.stream._duplexState & WRITE_UPDATING) === 0) { + this.stream._duplexState &= WRITE_NOT_NEXT_TICK + this.update() + } +} + +function tickDrains (drains) { + for (let i = 0; i < drains.length; i++) { + // drains.writes are monotonic, so if one is 0 its always the first one + if (--drains[i].writes === 0) { + drains.shift().resolve(true) + i-- + } + } } function afterOpen (err) { @@ -586,11 +685,11 @@ function afterOpen (err) { stream._duplexState &= NOT_ACTIVE if (stream._writableState !== null) { - stream._writableState.update() + stream._writableState.updateCallback() } if (stream._readableState !== null) { - stream._readableState.update() + stream._readableState.updateCallback() } } @@ -599,6 +698,26 @@ function afterTransform (err, data) { this._writableState.afterWrite(err) } +function newListener (name) { + if (this._readableState !== null) { + if (name === 'data') { + this._duplexState |= (READ_EMIT_DATA | READ_RESUMED_READ_AHEAD) + this._readableState.updateNextTick() + } + if (name === 'readable') { + this._duplexState |= READ_EMIT_READABLE + this._readableState.updateNextTick() + } + } + + if (this._writableState !== null) { + if (name === 'drain') { + this._duplexState |= WRITE_EMIT_DRAIN + this._writableState.updateNextTick() + } + } +} + export class Stream extends EventEmitter { constructor (opts) { super() @@ -615,6 +734,8 @@ export class Stream extends EventEmitter { opts.signal.addEventListener('abort', abort.bind(this)) } } + + this.on('newListener', newListener) } _open (cb) { @@ -649,38 +770,23 @@ export class Stream extends EventEmitter { if ((this._duplexState & DESTROY_STATUS) === 0) { if (!err) err = STREAM_DESTROYED this._duplexState = (this._duplexState | DESTROYING) & NON_PRIMARY + if (this._readableState !== null) { + this._readableState.highWaterMark = 0 this._readableState.error = err - this._readableState.updateNextTick() } if (this._writableState !== null) { + this._writableState.highWaterMark = 0 this._writableState.error = err - this._writableState.updateNextTick() } - this._predestroy() - } - } - on (name, fn) { - if (this._readableState !== null) { - if (name === 'data') { - this._duplexState |= (READ_EMIT_DATA | READ_RESUMED) - this._readableState.updateNextTick() - } - if (name === 'readable') { - this._duplexState |= READ_EMIT_READABLE - this._readableState.updateNextTick() - } - } + this._duplexState |= PREDESTROYING + this._predestroy() + this._duplexState &= NOT_PREDESTROYING - if (this._writableState !== null) { - if (name === 'drain') { - this._duplexState |= WRITE_EMIT_DRAIN - this._writableState.updateNextTick() - } + if (this._readableState !== null) this._readableState.updateNextTick() + if (this._writableState !== null) this._writableState.updateNextTick() } - - return super.on(name, fn) } } @@ -688,12 +794,13 @@ export class Readable extends Stream { constructor (opts) { super(opts) - this._duplexState |= OPENING | WRITE_DONE + this._duplexState |= OPENING | WRITE_DONE | READ_READ_AHEAD this._readableState = new ReadableState(this, opts) if (opts) { + if (this._readableState.readAhead === false) this._duplexState &= READ_NO_READ_AHEAD if (opts.read) this._read = opts.read - if (opts.eagerOpen) this.resume().pause() + if (opts.eagerOpen) this._readableState.updateNextTick() } } @@ -702,8 +809,8 @@ export class Readable extends Stream { } pipe (dest, cb) { - this._readableState.pipe(dest, cb) this._readableState.updateNextTick() + this._readableState.pipe(dest, cb) return dest } @@ -723,13 +830,13 @@ export class Readable extends Stream { } resume () { - this._duplexState |= READ_RESUMED + this._duplexState |= READ_RESUMED_READ_AHEAD this._readableState.updateNextTick() return this } pause () { - this._duplexState &= READ_PAUSED + this._duplexState &= (this._readableState.readAhead === false ? READ_PAUSED_NO_READ_AHEAD : READ_PAUSED) return this } @@ -745,6 +852,7 @@ export class Readable extends Stream { destroy = ite.return() }, destroy (cb) { + if (!destroy) return cb(null) destroy.then(cb.bind(null, null)).catch(cb) } }) @@ -852,6 +960,7 @@ export class Writable extends Stream { if (opts.writev) this._writev = opts.writev if (opts.write) this._write = opts.write if (opts.final) this._final = opts.final + if (opts.eagerOpen) this._writableState.updateNextTick() } } @@ -871,6 +980,18 @@ export class Writable extends Stream { return (ws._duplexState & WRITE_BACKPRESSURE_STATUS) !== 0 } + static drained (ws) { + if (ws.destroyed) return Promise.resolve(false) + const state = ws._writableState + const pending = (isWritev(ws) ? Math.min(1, state.queue.length) : state.queue.length) + const writes = pending + ((ws._duplexState & WRITE_WRITING) ? 1 : 0) + if (writes === 0) return Promise.resolve(true) + if (state.drains === null) state.drains = [] + return new Promise((resolve) => { + state.drains.push({ writes, resolve }) + }) + } + write (data) { this._writableState.updateNextTick() return this._writableState.push(data) @@ -887,7 +1008,7 @@ export class Duplex extends Readable { // and Writable constructor (opts) { super(opts) - this._duplexState = OPENING + this._duplexState = OPENING | (this._duplexState & READ_READ_AHEAD) this._writableState = new WritableState(this, opts) if (opts) { @@ -951,6 +1072,14 @@ export class Transform extends Duplex { } } + destroy (err) { + super.destroy(err) + if (this._transformState.data !== null) { + this._transformState.data = null + this._transformState.afterTransform() + } + } + _transform (data, cb) { cb(null, data) } @@ -1010,9 +1139,20 @@ export function pipeline (stream, ...streams) { if (done) { let fin = false - dest.on('finish', () => { fin = true }) - dest.on('error', err => { error = error || err }) - dest.on('close', () => done(error || (fin ? null : PREMATURE_CLOSE))) + const autoDestroy = isStreamx(dest) || !!(dest._writableState && dest._writableState.autoDestroy) + + dest.on('error', (err) => { + if (error === null) error = err + }) + + dest.on('finish', () => { + fin = true + if (!autoDestroy) done(error) + }) + + if (autoDestroy) { + dest.on('close', () => done(error || (fin ? null : PREMATURE_CLOSE))) + } } return dest @@ -1045,10 +1185,28 @@ export function isStreamx (stream) { return typeof stream._duplexState === 'number' && isStream(stream) } +export function getStreamError (stream) { + const err = (stream._readableState && stream._readableState.error) || (stream._writableState && stream._writableState.error) + return err === STREAM_DESTROYED ? null : err // only explicit errors +} + export function isReadStreamx (stream) { return isStreamx(stream) && stream.readable } +export default Object.assign(Stream, { + web, + Readable, + Writable, + Duplex, + Transform, + PassThrough, + pipeline: Object.assign(pipeline, { + [Symbol.for('nodejs.util.promisify.custom')]: pipelinePromise, + [Symbol.for('socket.runtime.util.promisify.custom')]: pipelinePromise + }) +}) + function isTypedArray (data) { return typeof data === 'object' && data !== null && typeof data.byteLength === 'number' } @@ -1062,3 +1220,7 @@ function noop () {} function abort () { this.destroy(new Error('Stream aborted.')) } + +function isWritev (s) { + return s._writev !== Writable.prototype._writev && s._writev !== Duplex.prototype._writev +} diff --git a/api/stream/web.js b/api/stream/web.js new file mode 100644 index 0000000000..a5cb13aa1b --- /dev/null +++ b/api/stream/web.js @@ -0,0 +1,48 @@ +import { + ReadableStream, + ReadableStreamBYOBReader, + ReadableByteStreamController, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + + TransformStream, + TransformStreamDefaultController, + + ByteLengthQueuingStrategy, + CountQueuingStrategy +} from '../internal/streams.js' + +import * as exports from './web.js' + +class UnsupportedStreamInterface {} + +export { + ReadableStream, + ReadableStreamBYOBReader, + ReadableByteStreamController, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + + TransformStream, + TransformStreamDefaultController, + + ByteLengthQueuingStrategy, + CountQueuingStrategy +} + +export const TextEncoderStream = globalThis.TextEncoderStream ?? UnsupportedStreamInterface +export const TextDecoderStream = globalThis.TextDecoderStream ?? UnsupportedStreamInterface +export const CompressionStream = globalThis.CompressionStream ?? UnsupportedStreamInterface +export const DecompressionStream = globalThis.DecompressionStream ?? UnsupportedStreamInterface + +export default exports diff --git a/api/string_decoder.js b/api/string_decoder.js new file mode 100644 index 0000000000..de3ce55852 --- /dev/null +++ b/api/string_decoder.js @@ -0,0 +1,298 @@ +/* eslint-disable */ +// Copyright Joyent, Inc. and other Node contributors. +// +// 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. + +'use strict'; + +/*<replacement>*/ + +import { Buffer } from './buffer.js' +/*</replacement>*/ + +var isEncoding = Buffer.isEncoding || function (encoding) { + encoding = '' + encoding; + switch (encoding && encoding.toLowerCase()) { + case 'hex':case 'utf8':case 'utf-8':case 'ascii':case 'binary':case 'base64':case 'ucs2':case 'ucs-2':case 'utf16le':case 'utf-16le':case 'raw': + return true; + default: + return false; + } +}; + +function _normalizeEncoding(enc) { + if (!enc) return 'utf8'; + var retried; + while (true) { + switch (enc) { + case 'utf8': + case 'utf-8': + return 'utf8'; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return 'utf16le'; + case 'latin1': + case 'binary': + return 'latin1'; + case 'base64': + case 'ascii': + case 'hex': + return enc; + default: + if (retried) return; // undefined + enc = ('' + enc).toLowerCase(); + retried = true; + } + } +}; + +// Do not cache `Buffer.isEncoding` when checking encoding names as some +// modules monkey-patch it to support additional encodings +function normalizeEncoding(enc) { + var nenc = _normalizeEncoding(enc); + if (typeof nenc !== 'string' && (Buffer.isEncoding === isEncoding || !isEncoding(enc))) throw new Error('Unknown encoding: ' + enc); + return nenc || enc; +} + +// StringDecoder provides an interface for efficiently splitting a series of +// buffers into a series of JS strings without breaking apart multi-byte +// characters. +export function StringDecoder (encoding) { + this.encoding = normalizeEncoding(encoding); + var nb; + switch (this.encoding) { + case 'utf16le': + this.text = utf16Text; + this.end = utf16End; + nb = 4; + break; + case 'utf8': + this.fillLast = utf8FillLast; + nb = 4; + break; + case 'base64': + this.text = base64Text; + this.end = base64End; + nb = 3; + break; + default: + this.write = simpleWrite; + this.end = simpleEnd; + return; + } + this.lastNeed = 0; + this.lastTotal = 0; + this.lastChar = Buffer.allocUnsafe(nb); +} + +StringDecoder.prototype.write = function (buf) { + if (buf.length === 0) return ''; + var r; + var i; + if (this.lastNeed) { + r = this.fillLast(buf); + if (r === undefined) return ''; + i = this.lastNeed; + this.lastNeed = 0; + } else { + i = 0; + } + if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i); + return r || ''; +}; + +StringDecoder.prototype.end = utf8End; + +// Returns only complete characters in a Buffer +StringDecoder.prototype.text = utf8Text; + +// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer +StringDecoder.prototype.fillLast = function (buf) { + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length); + this.lastNeed -= buf.length; +}; + +// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a +// continuation byte. If an invalid byte is detected, -2 is returned. +function utf8CheckByte(byte) { + if (byte <= 0x7F) return 0;else if (byte >> 5 === 0x06) return 2;else if (byte >> 4 === 0x0E) return 3;else if (byte >> 3 === 0x1E) return 4; + return byte >> 6 === 0x02 ? -1 : -2; +} + +// Checks at most 3 bytes at the end of a Buffer in order to detect an +// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4) +// needed to complete the UTF-8 character (if applicable) are returned. +function utf8CheckIncomplete(self, buf, i) { + var j = buf.length - 1; + if (j < i) return 0; + var nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 1; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 2; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) { + if (nb === 2) nb = 0;else self.lastNeed = nb - 3; + } + return nb; + } + return 0; +} + +// Validates as many continuation bytes for a multi-byte UTF-8 character as +// needed or are available. If we see a non-continuation byte where we expect +// one, we "replace" the validated continuation bytes we've seen so far with +// a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding +// behavior. The continuation byte check is included three times in the case +// where all of the continuation bytes for a character exist in the same buffer. +// It is also done this way as a slight performance increase instead of using a +// loop. +function utf8CheckExtraBytes(self, buf, p) { + if ((buf[0] & 0xC0) !== 0x80) { + self.lastNeed = 0; + return '\ufffd'; + } + if (self.lastNeed > 1 && buf.length > 1) { + if ((buf[1] & 0xC0) !== 0x80) { + self.lastNeed = 1; + return '\ufffd'; + } + if (self.lastNeed > 2 && buf.length > 2) { + if ((buf[2] & 0xC0) !== 0x80) { + self.lastNeed = 2; + return '\ufffd'; + } + } + } +} + +// Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer. +function utf8FillLast(buf) { + var p = this.lastTotal - this.lastNeed; + var r = utf8CheckExtraBytes(this, buf, p); + if (r !== undefined) return r; + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, p, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, p, 0, buf.length); + this.lastNeed -= buf.length; +} + +// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a +// partial character, the character's bytes are buffered until the required +// number of bytes are available. +function utf8Text(buf, i) { + var total = utf8CheckIncomplete(this, buf, i); + if (!this.lastNeed) return buf.toString('utf8', i); + this.lastTotal = total; + var end = buf.length - (total - this.lastNeed); + buf.copy(this.lastChar, 0, end); + return buf.toString('utf8', i, end); +} + +// For UTF-8, a replacement character is added when ending on a partial +// character. +function utf8End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) return r + '\ufffd'; + return r; +} + +// UTF-16LE typically needs two bytes per character, but even if we have an even +// number of bytes available, we need to check if we end on a leading/high +// surrogate. In that case, we need to wait for the next two bytes in order to +// decode the last character properly. +function utf16Text(buf, i) { + if ((buf.length - i) % 2 === 0) { + var r = buf.toString('utf16le', i); + if (r) { + var c = r.charCodeAt(r.length - 1); + if (c >= 0xD800 && c <= 0xDBFF) { + this.lastNeed = 2; + this.lastTotal = 4; + this.lastChar[0] = buf[buf.length - 2]; + this.lastChar[1] = buf[buf.length - 1]; + return r.slice(0, -1); + } + } + return r; + } + this.lastNeed = 1; + this.lastTotal = 2; + this.lastChar[0] = buf[buf.length - 1]; + return buf.toString('utf16le', i, buf.length - 1); +} + +// For UTF-16LE we do not explicitly append special replacement characters if we +// end on a partial character, we simply let v8 handle that. +function utf16End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) { + var end = this.lastTotal - this.lastNeed; + return r + this.lastChar.toString('utf16le', 0, end); + } + return r; +} + +function base64Text(buf, i) { + var n = (buf.length - i) % 3; + if (n === 0) return buf.toString('base64', i); + this.lastNeed = 3 - n; + this.lastTotal = 3; + if (n === 1) { + this.lastChar[0] = buf[buf.length - 1]; + } else { + this.lastChar[0] = buf[buf.length - 2]; + this.lastChar[1] = buf[buf.length - 1]; + } + return buf.toString('base64', i, buf.length - n); +} + +function base64End(buf) { + var r = buf && buf.length ? this.write(buf) : ''; + if (this.lastNeed) return r + this.lastChar.toString('base64', 0, 3 - this.lastNeed); + return r; +} + +// Pass bytes on through for single-byte encodings (e.g. ascii, latin1, hex) +function simpleWrite(buf) { + return buf.toString(this.encoding); +} + +function simpleEnd(buf) { + return buf && buf.length ? this.write(buf) : ''; +} + +export default StringDecoder diff --git a/api/test/context.js b/api/test/context.js index c87c04e6ac..428869476c 100644 --- a/api/test/context.js +++ b/api/test/context.js @@ -30,8 +30,10 @@ export default function (GLOBAL_TEST_RUNNER) { function onerror (e) { const err = e.error || e.stack || e.reason || e.message || e - if (err.ignore || err[Symbol.for('socket.test.error.ignore')]) return - console.error(err) + if (err.ignore || err[Symbol.for('socket.runtime.test.error.ignore')]) return + if (globalThis.RUNTIME_TEST_FILENAME || GLOBAL_TEST_RUNNER.length > 0) { + console.error(err) + } if (finishing || process.env.DEBUG) { return diff --git a/api/test/dom-helpers.js b/api/test/dom-helpers.js index 94fa46432b..837b4bbca6 100644 --- a/api/test/dom-helpers.js +++ b/api/test/dom-helpers.js @@ -1,6 +1,6 @@ // @ts-check /** - * @module Test.DOM-helpers + * @module test.dom-helpers * * Provides a test runner for Socket Runtime. * @@ -26,12 +26,20 @@ const defaultTimeout = 5 * SECOND * */ export function toElement (selector) { - if (typeof selector === 'string') selector = document.querySelector(selector) - if (!( - selector instanceof window.HTMLElement || - selector instanceof window.Element - )) throw new Error('stringOrElement needs to be an instance of HTMLElement or a querySelector that resolves to a HTMLElement') - return selector + if (globalThis.document) { + if (typeof selector === 'string') { + selector = globalThis.document.querySelector(selector) + } + + if (!( + selector instanceof globalThis.HTMLElement || + selector instanceof globalThis.Element + )) { + throw new Error('stringOrElement needs to be an instance of HTMLElement or a querySelector that resolves to a HTMLElement') + } + + return selector + } } /** @@ -64,7 +72,7 @@ export function waitFor (args, lambda) { } = args if (!lambda && selector) { - lambda = () => document.querySelector(selector) + lambda = () => globalThis.document?.querySelector?.(selector) ?? null } const interval = setInterval(() => { @@ -185,11 +193,11 @@ export function waitForText (args) { export function event (args) { let { event, - element = window + element = globalThis } = args if (typeof event === 'string') { - event = new window.CustomEvent(event) + event = new globalThis.CustomEvent(event) } if (typeof event !== 'object') { diff --git a/api/test/index.js b/api/test/index.js index 4a8ff42326..07f2d8d617 100644 --- a/api/test/index.js +++ b/api/test/index.js @@ -1,6 +1,6 @@ // @ts-check /** - * @module Test + * @module test * * Provides a test runner for Socket Runtime. * @@ -329,9 +329,9 @@ export class Test { * ``` */ async requestAnimationFrame (msg = null) { - if (document.hasFocus()) { + if (globalThis.document && globalThis.document.hasFocus()) { // RAF only works when the window is focused - await new Promise(resolve => window.requestAnimationFrame(resolve)) + await new Promise(resolve => globalThis.requestAnimationFrame(resolve)) } else { await new Promise((resolve) => setTimeout(resolve, 0)) } @@ -355,7 +355,11 @@ export class Test { msg = msg || `Clicked on ${typeof selector === 'string' ? selector : 'element'}` const el = toElement(selector) - if (!(el instanceof window.HTMLElement)) throw new Error('selector needs to be instance of HTMLElement or resolve to one') + if (globalThis.HTMLElement && !(el instanceof globalThis.HTMLElement)) { + throw new Error('selector needs to be instance of HTMLElement or resolve to one') + } + + // @ts-ignore el.click() await this.requestAnimationFrame() this.pass(msg) @@ -377,7 +381,7 @@ export class Test { const element = toElement(selector) msg = msg || `Fired click event on ${typeof selector === 'string' ? selector : 'element'}` dispatchEventHelper({ - event: new window.MouseEvent('click', { + event: new globalThis.MouseEvent('click', { bubbles: true, cancelable: true, button: 0 @@ -424,7 +428,10 @@ export class Test { async focus (selector, msg) { msg = msg || `Focused on ${typeof selector === 'string' ? selector : 'element'}` const el = toElement(selector) - if (!(el instanceof window.HTMLElement)) throw new Error('selector needs to be instance of HTMLElement or resolve to one') + if (globalThis.HTMLElement && !(el instanceof globalThis.HTMLElement)) { + throw new Error('selector needs to be instance of HTMLElement or resolve to one') + } + // @ts-ignore el.focus() await this.requestAnimationFrame() this.pass(msg) @@ -445,7 +452,10 @@ export class Test { async blur (selector, msg) { msg = msg || `Blurred from ${typeof selector === 'string' ? selector : 'element'}` const el = toElement(selector) - if (!(el instanceof window.HTMLElement)) throw new Error('selector needs to be instance of HTMLElement or resolve to one') + if (globalThis.HTMLElement && !(el instanceof globalThis.HTMLElement)) { + throw new Error('selector needs to be instance of HTMLElement or resolve to one') + } + // @ts-ignore el.blur() await this.requestAnimationFrame() this.pass(msg) @@ -680,7 +690,7 @@ export class Test { * ``` */ querySelector (selector, msg) { - const el = document.querySelector(selector) + const el = globalThis.document?.querySelector?.(selector) ?? null msg = msg || `querySelector(${selector})` this.ok(el, msg) return el @@ -699,7 +709,7 @@ export class Test { * ``` */ querySelectorAll (selector, msg) { - const elems = document.querySelectorAll(selector) + const elems = globalThis.document?.querySelectorAll?.(selector) ?? [] const elementArray = Array.from(elems) msg = msg || `querySelectorAll(${selector})` this.ok(elementArray.length, msg) @@ -1124,6 +1134,78 @@ test.skip = skip export default test +// os types +test.linux = function (name, fn) { + if (os.type() === 'Linux') { + return test(name, fn) + } +} + +test.windows = +test.win32 = function (name, fn) { + if (os.platform() === 'win32') { + return test(name, fn) + } +} + +test.unix = function (name, fn) { + if (os.host() === 'unix') { + return test(name, fn) + } +} + +test.macosx = +test.macos = +test.mac = function (name, fn) { + if (os.host() === 'macosx') { + return test(name, fn) + } +} + +test.darwin = function (name, fn) { + if (os.type() === 'Darwin') { + return test(name, fn) + } +} + +test.iphone = +test.ios = function (name, fn) { + if (os.host() === 'iphoneos') { + return test(name, fn) + } +} + +test.iphone.simulator = +test.ios.simulator = function (name, fn) { + if (os.host() === 'iphone-simulator') { + return test(name, fn) + } +} + +test.android = function (name, fn) { + if (os.host() === 'androidos') { + return test(name, fn) + } +} + +test.android.emulator = function (name, fn) { + if (os.host() === 'android-emulator') { + return test(name, fn) + } +} + +test.desktop = function (name, fn) { + if (/linux|macosx|unix|win32/.test(os.host())) { + return test(name, fn) + } +} + +test.mobile = function (name, fn) { + if (/android|android-emulator|iphoneos|iphone-simulator/.test(os.host())) { + return test(name, fn) + } +} + /** * @param {Error} err * @returns {void} diff --git a/api/timers.js b/api/timers.js new file mode 100644 index 0000000000..d53d48da6f --- /dev/null +++ b/api/timers.js @@ -0,0 +1,7 @@ +/** + * @notice This is a re-exports of `timers/index.js` so consumers will + * need to only `import * as timers from 'socket:timers'` + */ +import * as exports from './timers/index.js' +export * from './timers/index.js' +export default exports diff --git a/api/timers/index.js b/api/timers/index.js new file mode 100644 index 0000000000..6b35c434f3 --- /dev/null +++ b/api/timers/index.js @@ -0,0 +1,81 @@ +import { Timeout, Immediate, Interval } from './timer.js' +import scheduler from './scheduler.js' +import platform from './platform.js' +import promises from './promises.js' +import ipc from '../ipc.js' + +export { platform } + +export function setTimeout (callback, delay, ...args) { + return Timeout.from(callback, delay, ...args) +} + +export function clearTimeout (timeout) { + if (timeout instanceof Timeout) { + timeout.close() + } else { + platform.clearTimeout(timeout) + } +} + +export function setInterval (callback, delay, ...args) { + return Interval.from(callback, delay, ...args) +} + +export function clearInterval (interval) { + if (interval instanceof Interval) { + interval.close() + } else { + platform.clearInterval(interval) + } +} + +export function setImmediate (callback, ...args) { + return Immediate.from(callback, ...args) +} + +export function clearImmediate (immediate) { + if (immediate instanceof Immediate) { + immediate.close() + } else { + platform.clearImmediate(immediate) + } +} + +/** + * Pause async execution for `timeout` milliseconds. + * @param {number} timeout + * @return {Promise} + */ +export async function sleep (timeout) { + await new Promise((resolve) => setTimeout(resolve, timeout)) +} + +/** + * Pause sync execution for `timeout` milliseconds. + * @param {number} timeout + */ +sleep.sync = function (timeout) { + ipc.sendSync('timers.setTimeout', { timeout, wait: true }) +} + +setTimeout[Symbol.for('nodejs.util.promisify.custom')] = promises.setTimeout +setTimeout[Symbol.for('socket.runtime.util.promisify.custom')] = promises.setTimeout + +setInterval[Symbol.for('nodejs.util.promisify.custom')] = promises.setInterval +setInterval[Symbol.for('socket.runtime.util.promisify.custom')] = promises.setInterval + +setImmediate[Symbol.for('nodejs.util.promisify.custom')] = promises.setImmediate +setImmediate[Symbol.for('socket.runtime.util.promisify.custom')] = promises.setImmediate + +export default { + platform, + promises, + scheduler, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + setImmediate, + clearImmediate +} diff --git a/api/timers/platform.js b/api/timers/platform.js new file mode 100644 index 0000000000..78e5b939cf --- /dev/null +++ b/api/timers/platform.js @@ -0,0 +1,14 @@ +export const platform = { + setTimeout: globalThis.setTimeout.bind(globalThis), + setInterval: globalThis.setInterval.bind(globalThis), + setImmediate: globalThis.setTimeout.bind(globalThis), + clearTimeout: globalThis.clearTimeout.bind(globalThis), + clearInterval: globalThis.clearInterval.bind(globalThis), + clearImmediate: globalThis.clearTimeout.bind(globalThis), + postTask: ( + globalThis.scheduler?.postTask?.bind?.(globalThis?.scheduler) ?? + async function notSupported () {} + ) +} + +export default platform diff --git a/api/timers/promises.js b/api/timers/promises.js new file mode 100644 index 0000000000..c705009d3e --- /dev/null +++ b/api/timers/promises.js @@ -0,0 +1,72 @@ +import { Timeout, Immediate } from './timer.js' + +export async function setTimeout (delay = 1, value = undefined, options = null) { + return await new Promise((resolve, reject) => { + const signal = options?.signal + + if (signal?.aborted) { + return reject(new DOMException('This operation was aborted', 'AbortError')) + } + + const timeout = Timeout.from(callback, delay) + + if (signal) { + signal.addEventListener('abort', () => { + timeout.close() + reject(new DOMException('This operation was aborted', 'AbortError')) + }) + } + + function callback () { + resolve(value) + } + }) +} + +export async function * setInterval (delay = 1, value = undefined, options = null) { + const signal = options?.signal + + while (true) { + yield await new Promise((resolve, reject) => { + const timeout = Timeout.from(callback, delay) + + if (signal?.aborted) { + timeout.close() + return reject(new DOMException('This operation was aborted', 'AbortError')) + } + + function callback () { + resolve(value) + } + }) + } +} + +export async function setImmediate (value = undefined, options = null) { + return await new Promise((resolve, reject) => { + const signal = options?.signal + + if (signal?.aborted) { + return reject(new DOMException('This operation was aborted', 'AbortError')) + } + + const immediate = Immediate.from(callback, 0) + + if (signal) { + signal.addEventListener('abort', () => { + immediate.close() + reject(new DOMException('This operation was aborted', 'AbortError')) + }) + } + + function callback () { + resolve(value) + } + }) +} + +export default { + setImmediate, + setInterval, + setTimeout +} diff --git a/api/timers/scheduler.js b/api/timers/scheduler.js new file mode 100644 index 0000000000..01a53b6b6b --- /dev/null +++ b/api/timers/scheduler.js @@ -0,0 +1,16 @@ +import { setImmediate, setTimeout } from './promises.js' +import platform from './platform.js' + +export async function wait (delay, options = null) { + return await setTimeout(delay, undefined, options) +} + +export async function postTask (callback, options = null) { + return await platform.postTask(callback, options) +} + +export default { + postTask, + yield: setImmediate, + wait +} diff --git a/api/timers/timer.js b/api/timers/timer.js new file mode 100644 index 0000000000..f31c2b3fc2 --- /dev/null +++ b/api/timers/timer.js @@ -0,0 +1,136 @@ +import { AsyncResource } from '../async/resource.js' +import platform from './platform.js' +import gc from '../gc.js' + +export class Timer extends AsyncResource { + #id = 0 + #create = null + #destroy = null + #destroyed = false + + static from (...args) { + const timer = new this() + return timer.init(...args) + } + + constructor (type, create, destroy) { + super(type, { requireManualDestroy: true }) + + if (typeof create !== 'function') { + throw new TypeError('Timer creator must be a function') + } + + if (typeof destroy !== 'function') { + throw new TypeError('Timer destroyer must be a function') + } + + this.#create = (callback, ...args) => { + return create((...args) => this.runInAsyncScope(callback, globalThis, ...args), ...args) + } + + this.#destroy = (...args) => { + if (typeof destroy === 'function') { + destroy(...args) + } + + this.#destroyed = true + this.emitDestroy() + } + } + + get id () { + return this.#id + } + + get destroyed () { + return this.#destroyed + } + + init (...args) { + this.#id = this.#create(...args) + gc.ref(this) + return this + } + + close () { + if (this.#id) { + this.#destroy(this.#id) + this.#id = 0 + return true + } + + return false + } + + [Symbol.toPrimitive] () { + return this.#id + } + + [gc.finalizer] () { + const finalizer = super[gc.finalizer] ? super[gc.finalizer]() : null + return { + args: [this.id, this.#destroy], + handle (id, destroy) { + destroy(id) + if (finalizer?.handle && finalizer?.args) { + finalizer.handle(...finalizer.args) + } else if (finalizer?.handle) { + finalizer.handle() + } + } + } + } +} + +export class Timeout extends Timer { + constructor () { + super( + 'Timeout', + (callback, delay, ...args) => { + return platform.setTimeout( + (...args) => { + this.close() + // eslint-disable-next-line + callback(...args) + }, + delay, + ...args + ) + }, + platform.clearTimeout + ) + } +} + +export class Interval extends Timer { + constructor () { + super('Interval', platform.setInterval, platform.clearInterval) + } +} + +export class Immediate extends Timer { + constructor () { + super( + 'Immediate', + (callback, _, ...args) => { + return platform.setImmediate( + (...args) => { + this.close() + // eslint-disable-next-line + callback(...args) + }, + 0, + ...args + ) + }, + platform.clearImmediate + ) + } +} + +export default { + Timer, + Immediate, + Timeout, + Interval +} diff --git a/api/tty.js b/api/tty.js new file mode 100644 index 0000000000..77f68a751a --- /dev/null +++ b/api/tty.js @@ -0,0 +1,77 @@ +import { Writable, Readable } from './stream.js' +import ipc from './ipc.js' + +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +export function WriteStream (fd) { + if (fd !== 1 && fd !== 2) { + throw new TypeError('"fd" must be 1 or 2.') + } + + const stream = new Writable({ + async write (data, cb) { + if (data && typeof data === 'object') { + data = textDecoder.decode(data) + } + + for (const value of data.split('\n')) { + if (fd === 1) { + ipc.send('stdout', { resolve: false, value }) + } else if (fd === 2) { + ipc.send('stderr', { resolve: false, value }) + } + } + + cb(null) + } + }) + + return stream +} + +export function ReadStream (fd) { + if (fd !== 0) { + throw new TypeError('"fd" must be 0.') + } + + const stream = new Readable() + + if (!globalThis.process?.versions?.node) { + globalThis.addEventListener('process.stdin', function onData (event) { + if (stream.destroyed) { + return globalThis.removeEventListener('process.stdin', onData) + } + + if (event.detail && typeof event.detail === 'object') { + process.stdin.push(Buffer.from(JSON.stringify(event.detail))) + } else if (event.detail) { + process.stdin.push(Buffer.from(textEncoder.encode(event.detail))) + } + }) + } + + return stream +} + +export function isatty (fd) { + if (fd === 0) { + return globalThis.__args?.argv?.includes?.('--stdin') !== true + } + + if (fd === 1) { + return true + } + + if (fd === 2) { + return true + } + + return false +} + +export default { + WriteStream, + ReadStream, + isatty +} diff --git a/api/url/index.js b/api/url/index.js index 8c78888769..53022f8c8f 100644 --- a/api/url/index.js +++ b/api/url/index.js @@ -1,5 +1,6 @@ import { URLPattern } from './urlpattern/urlpattern.js' import url from './url/url.js' +import qs from '../querystring.js' const { URL, @@ -16,8 +17,169 @@ for (const key in globalThis.URL) { } URL.resolve = resolve +URL.parse = parse +URL.format = format -export const parse = parseURL +URL.prototype[Symbol.for('socket.runtime.util.inspect.custom')] = function () { + return [ + 'URL {', + ` protocol: ${this.protocol || null},`, + ` username: ${this.username || null},`, + ` password: ${this.password || null},`, + ` hostname: ${this.hostname || null},`, + ` pathname: ${this.pathname || null},`, + ` search: ${this.search || null},`, + ` hash: ${this.hash || null}`, + '}' + ].join('\n') +} + +/** + * @type {Set & { handlers: Set<string> }} + */ +export const protocols = new Set([ + 'socket:', + 'node:', + 'npm:', + 'ipc:', + + // android + 'android.resource:', + 'content:', + + // web standard & reserved + 'bitcoin:', + 'file:', + 'ftp:', + 'ftps:', + 'geo:', + 'git:', + 'http:', + 'https:', + 'im:', + 'ipfs:', + 'irc:', + 'ircs:', + 'magnet:', + 'mailto:', + 'matrix:', + 'mms:', + 'news:', + 'nntp:', + 'openpgp4fpr:', + 'sftp:', + 'sip:', + 'sms:', + 'smsto:', + 'ssh:', + 'tel:', + 'urn:', + 'webcal:', + 'wtai:', + 'xmpp:' +]) + +protocols.handlers = new Set() +if (globalThis.__args?.config && typeof globalThis.__args.config === 'object') { + const protocolHandlers = String(globalThis.__args.config['webview_protocol-handlers'] || '') + .split(' ') + .filter(Boolean) + + const webviewURLProtocols = String(globalThis.__args.config.webview_url_protocols || '') + .split(' ') + .filter(Boolean) + + for (const value of webviewURLProtocols) { + const scheme = value.replace(':', '') + if (scheme) { + protocols.add(scheme + ':') + protocols.handlers.add(scheme) + } + } + + for (const value of protocolHandlers) { + const scheme = value.replace(':', '') + if (scheme) { + protocols.add(scheme + ':') + protocols.handlers.add(scheme) + } + } + + for (const key in globalThis.__args.config) { + if (key.startsWith('webview_protocol-handlers_')) { + const scheme = key.replace('webview_protocol-handlers_', '').replace(':', '') + if (scheme) { + protocols.add(scheme + ':') + protocols.handlers.add(scheme) + } + } + } +} + +export function parse (input, options = null) { + let parsed = null + if (URL.canParse(input)) { + parsed = new URL(input) + } + + if (options?.strict === true && !URL.canParse(input)) { + return null + } + + if (URL.canParse(input, 'socket://')) { + parsed = new URL(input, globalThis.location.origin) + } + + if (!parsed) { + return null + } + + parsed = { + hash: parsed.hash || null, + host: parsed.hostname || null, + hostname: parsed.hostname || null, + origin: parsed.origin || null, + auth: [parsed.username, parsed.password].filter(Boolean).join(':') || null, + password: parsed.password || null, + pathname: parsed.pathname || null, + path: parsed.pathname || null, + port: parsed.port || null, + protocol: parsed.protocol || null, + search: parsed.search || null, + searchParams: parsed.searchParams, + username: parsed.username || null, + [Symbol.toStringTag]: 'URL (Parsed)' + } + + if (options === true) { + // for nodejs compat + parsed.query = Object.fromEntries(parsed.searchParams.entries()) + } else if (parsed.search) { + parsed.query = parsed.search.slice(1) ?? null + } + + if (!input.startsWith(parsed.protocol)) { + parsed.protocol = null + parsed.hostname = null + parsed.origin = null + parsed.host = null + parsed.href = `${parsed.pathname || ''}${parsed.search || ''}${parsed.hash || ''}` + } else { + parsed.href = `${parsed.protocol}//` + + if (parsed.username) { + parsed.href += [parsed.username, parsed.password].filter(Boolean).join(':') + + if (parsed.hostname) { + parsed.href += '@' + } + } + + parsed.href += `${parsed.hostname || ''}${parsed.pathname || ''}${parsed.search || ''}${parsed.hash || ''}` + } + + return parsed +} // lifted from node export function resolve (from, to) { @@ -31,6 +193,92 @@ export function resolve (from, to) { return resolved.toString() } +export function format (input) { + if (!input || (typeof input !== 'string' && typeof input !== 'object')) { + throw new TypeError( + `The 'input' argument must be one of type object or string. Received: ${input}` + ) + } + + if (typeof input === 'string') { + if (!URL.canParse(input)) { + return '' + } + + return new URL(input).toString() + } + + let formatted = '' + + if (!input.hostname) { + return '' + } + + if (input.protocol) { + formatted += `${input.protocol}//` + } + + if (input.username) { + formatted += encodeURIComponent(input.username) + + if (input.password) { + formatted += `:${encodeURIComponent(input.password)}` + } + + formatted += '@' + } + + formatted += input.hostname + + if (input.port) { + formatted += `:${input.port}` + } + + if (input.pathname) { + formatted += input.pathname + } + + if (input.query && typeof input.query === 'object') { + formatted += `?${qs.stringify(input.query)}` + } else if (input.query && typeof input.query === 'string') { + if (!input.query.startsWith('?')) { + formatted += '?' + } + + formatted += encodeURIComponent(decodeURIComponent(input.query)) + } + + if (input.hash && typeof input.hash === 'string') { + if (!input.hash.startsWith('#')) { + formatted += '#' + } + + formatted += input.hash + } + + return formatted +} + +export function fileURLToPath (url) { + if (typeof url === 'string') { + url = new URL(url, globalThis.location.origin) + } + + if (!(url instanceof URL)) { + throw new TypeError( + `Expecting 'url' to be a URL or string. Received: ${url}` + ) + } + + if (url.protocol !== 'file:' && url.protocol !== 'socket:') { + throw new TypeError( + `Expecting 'url' to have a 'file:' or 'socket:' URL scheme. Received: ${url.protocol}` + ) + } + + return url.pathname +} + url.serializeURLOrigin = function (input) { const { scheme, protocol, host } = input @@ -50,11 +298,11 @@ url.serializeURLOrigin = function (input) { } } - if (protocol === 'socket:' || protocol === 'ipc:') { + if (protocols.has(protocol)) { return `${protocol}//${serializeHost(host)}` } - if (scheme === 'socket' || scheme === 'ipc') { + if (protocols.has(`${scheme}:`)) { return `${scheme}://${serializeHost(host)}` } @@ -71,4 +319,4 @@ Object.defineProperties(URL.prototype, { }) export default URL -export { URLPattern, URL, URLSearchParams, parseURL } +export { URL, URLSearchParams, parseURL, URLPattern } diff --git a/api/url/url/url.js b/api/url/url/url.js index 0a3da16c62..dffe00b3ca 100644 --- a/api/url/url/url.js +++ b/api/url/url/url.js @@ -732,7 +732,7 @@ var require_punycode = __commonJS({ * @memberOf punycode * @type String */ - "version": "2.1.0", + "version": "2.3.1", /** * An object of methods to convert from JavaScript's internal character * representation (UCS-2) to Unicode code points, and back. @@ -758,17 +758,17 @@ var require_regexes = __commonJS({ "node_modules/tr46/lib/regexes.js"(exports, module) { "use strict"; var combiningMarks = /[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3C\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CF3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D81-\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u180F\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA82C\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11000}-\u{11002}\u{11038}-\u{11046}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11082}\u{110B0}-\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{11134}\u{11145}\u{11146}\u{11173}\u{11180}-\u{11182}\u{111B3}-\u{111C0}\u{111C9}-\u{111CC}\u{111CE}\u{111CF}\u{1122C}-\u{11237}\u{1123E}\u{11241}\u{112DF}-\u{112EA}\u{11300}-\u{11303}\u{1133B}\u{1133C}\u{1133E}-\u{11344}\u{11347}\u{11348}\u{1134B}-\u{1134D}\u{11357}\u{11362}\u{11363}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11435}-\u{11446}\u{1145E}\u{114B0}-\u{114C3}\u{115AF}-\u{115B5}\u{115B8}-\u{115C0}\u{115DC}\u{115DD}\u{11630}-\u{11640}\u{116AB}-\u{116B7}\u{1171D}-\u{1172B}\u{1182C}-\u{1183A}\u{11930}-\u{11935}\u{11937}\u{11938}\u{1193B}-\u{1193E}\u{11940}\u{11942}\u{11943}\u{119D1}-\u{119D7}\u{119DA}-\u{119E0}\u{119E4}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A39}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A5B}\u{11A8A}-\u{11A99}\u{11C2F}-\u{11C36}\u{11C38}-\u{11C3F}\u{11C92}-\u{11CA7}\u{11CA9}-\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D8A}-\u{11D8E}\u{11D90}\u{11D91}\u{11D93}-\u{11D97}\u{11EF3}-\u{11EF6}\u{11F00}\u{11F01}\u{11F03}\u{11F34}-\u{11F3A}\u{11F3E}-\u{11F42}\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F51}-\u{16F87}\u{16F8F}-\u{16F92}\u{16FE4}\u{16FF0}\u{16FF1}\u{1BC9D}\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D165}-\u{1D169}\u{1D16D}-\u{1D172}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{E0100}-\u{E01EF}]/u; - var combiningClassVirama = /[\u094D\u09CD\u0A4D\u0ACD\u0B4D\u0BCD\u0C4D\u0CCD\u0D3B\u0D3C\u0D4D\u0DCA\u0E3A\u0EBA\u0F84\u1039\u103A\u1714\u1734\u17D2\u1A60\u1B44\u1BAA\u1BAB\u1BF2\u1BF3\u2D7F\uA806\uA8C4\uA953\uA9C0\uAAF6\uABED\u{10A3F}\u{11046}\u{1107F}\u{110B9}\u{11133}\u{11134}\u{111C0}\u{11235}\u{112EA}\u{1134D}\u{11442}\u{114C2}\u{115BF}\u{1163F}\u{116B6}\u{1172B}\u{11839}\u{119E0}\u{11A34}\u{11A47}\u{11A99}\u{11C3F}\u{11D44}\u{11D45}\u{11D97}]/u; - var validZWNJ = /[\u0620\u0626\u0628\u062A-\u062E\u0633-\u063F\u0641-\u0647\u0649\u064A\u066E\u066F\u0678-\u0687\u069A-\u06BF\u06C1\u06C2\u06CC\u06CE\u06D0\u06D1\u06FA-\u06FC\u06FF\u0712-\u0714\u071A-\u071D\u071F-\u0727\u0729\u072B\u072D\u072E\u074E-\u0758\u075C-\u076A\u076D-\u0770\u0772\u0775-\u0777\u077A-\u077F\u07CA-\u07EA\u0841-\u0845\u0848\u084A-\u0853\u0855\u0860\u0862-\u0865\u0868\u08A0-\u08A9\u08AF\u08B0\u08B3\u08B4\u08B6-\u08B8\u08BA-\u08BD\u1807\u1820-\u1878\u1887-\u18A8\u18AA\uA840-\uA872\u{10AC0}-\u{10AC4}\u{10ACD}\u{10AD3}-\u{10ADC}\u{10ADE}-\u{10AE0}\u{10AEB}-\u{10AEE}\u{10B80}\u{10B82}\u{10B86}-\u{10B88}\u{10B8A}\u{10B8B}\u{10B8D}\u{10B90}\u{10BAD}\u{10BAE}\u{10D00}-\u{10D21}\u{10D23}\u{10F30}-\u{10F32}\u{10F34}-\u{10F44}\u{10F51}-\u{10F53}\u{1E900}-\u{1E943}][\xAD\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u061C\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u070F\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C04\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC6\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u206A-\u206F\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFEFF\uFFF9-\uFFFB\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10F46}-\u{10F50}\u{11001}\u{11038}-\u{11046}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{13430}-\u{13438}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{1BC9D}\u{1BC9E}\u{1BCA0}-\u{1BCA3}\u{1D167}-\u{1D169}\u{1D173}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E130}-\u{1E136}\u{1E2EC}-\u{1E2EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94B}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]*\u200C[\xAD\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u061C\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u070F\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C04\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC6\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u206A-\u206F\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFEFF\uFFF9-\uFFFB\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10F46}-\u{10F50}\u{11001}\u{11038}-\u{11046}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{13430}-\u{13438}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{1BC9D}\u{1BC9E}\u{1BCA0}-\u{1BCA3}\u{1D167}-\u{1D169}\u{1D173}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E130}-\u{1E136}\u{1E2EC}-\u{1E2EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94B}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]*[\u0620\u0622-\u063F\u0641-\u064A\u066E\u066F\u0671-\u0673\u0675-\u06D3\u06D5\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u077F\u07CA-\u07EA\u0840-\u0855\u0860\u0862-\u0865\u0867-\u086A\u08A0-\u08AC\u08AE-\u08B4\u08B6-\u08BD\u1807\u1820-\u1878\u1887-\u18A8\u18AA\uA840-\uA871\u{10AC0}-\u{10AC5}\u{10AC7}\u{10AC9}\u{10ACA}\u{10ACE}-\u{10AD6}\u{10AD8}-\u{10AE1}\u{10AE4}\u{10AEB}-\u{10AEF}\u{10B80}-\u{10B91}\u{10BA9}-\u{10BAE}\u{10D01}-\u{10D23}\u{10F30}-\u{10F44}\u{10F51}-\u{10F54}\u{1E900}-\u{1E943}]/u; + var combiningClassVirama = /[\u094D\u09CD\u0A4D\u0ACD\u0B4D\u0BCD\u0C4D\u0CCD\u0D3B\u0D3C\u0D4D\u0DCA\u0E3A\u0EBA\u0F84\u1039\u103A\u1714\u1715\u1734\u17D2\u1A60\u1B44\u1BAA\u1BAB\u1BF2\u1BF3\u2D7F\uA806\uA82C\uA8C4\uA953\uA9C0\uAAF6\uABED\u{10A3F}\u{11046}\u{11070}\u{1107F}\u{110B9}\u{11133}\u{11134}\u{111C0}\u{11235}\u{112EA}\u{1134D}\u{11442}\u{114C2}\u{115BF}\u{1163F}\u{116B6}\u{1172B}\u{11839}\u{1193D}\u{1193E}\u{119E0}\u{11A34}\u{11A47}\u{11A99}\u{11C3F}\u{11D44}\u{11D45}\u{11D97}\u{11F41}\u{11F42}]/u; + var validZWNJ = /[\u0620\u0626\u0628\u062A-\u062E\u0633-\u063F\u0641-\u0647\u0649\u064A\u066E\u066F\u0678-\u0687\u069A-\u06BF\u06C1\u06C2\u06CC\u06CE\u06D0\u06D1\u06FA-\u06FC\u06FF\u0712-\u0714\u071A-\u071D\u071F-\u0727\u0729\u072B\u072D\u072E\u074E-\u0758\u075C-\u076A\u076D-\u0770\u0772\u0775-\u0777\u077A-\u077F\u07CA-\u07EA\u0841-\u0845\u0848\u084A-\u0853\u0855\u0860\u0862-\u0865\u0868\u0886\u0889-\u088D\u08A0-\u08A9\u08AF\u08B0\u08B3-\u08B8\u08BA-\u08C8\u1807\u1820-\u1878\u1887-\u18A8\u18AA\uA840-\uA872\u{10AC0}-\u{10AC4}\u{10ACD}\u{10AD3}-\u{10ADC}\u{10ADE}-\u{10AE0}\u{10AEB}-\u{10AEE}\u{10B80}\u{10B82}\u{10B86}-\u{10B88}\u{10B8A}\u{10B8B}\u{10B8D}\u{10B90}\u{10BAD}\u{10BAE}\u{10D00}-\u{10D21}\u{10D23}\u{10F30}-\u{10F32}\u{10F34}-\u{10F44}\u{10F51}-\u{10F53}\u{10F70}-\u{10F73}\u{10F76}-\u{10F81}\u{10FB0}\u{10FB2}\u{10FB3}\u{10FB8}\u{10FBB}\u{10FBC}\u{10FBE}\u{10FBF}\u{10FC1}\u{10FC4}\u{10FCA}\u{10FCB}\u{1E900}-\u{1E943}][\xAD\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u061C\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u070F\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B55\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC6\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0D81\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732\u1733\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u180F\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DFF\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u206A-\u206F\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFEFF\uFFF9-\uFFFB\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{1193B}\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{11F00}\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{13430}-\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{1BC9D}\u{1BC9E}\u{1BCA0}-\u{1BCA3}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D173}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94B}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]*\u200C[\xAD\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u061C\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u070F\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B55\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC6\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0D81\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732\u1733\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u180F\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DFF\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u206A-\u206F\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFEFF\uFFF9-\uFFFB\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{1193B}\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{11F00}\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{13430}-\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{1BC9D}\u{1BC9E}\u{1BCA0}-\u{1BCA3}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D173}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94B}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]*[\u0620\u0622-\u063F\u0641-\u064A\u066E\u066F\u0671-\u0673\u0675-\u06D3\u06D5\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u077F\u07CA-\u07EA\u0840-\u0858\u0860\u0862-\u0865\u0867-\u086A\u0870-\u0882\u0886\u0889-\u088E\u08A0-\u08AC\u08AE-\u08C8\u1807\u1820-\u1878\u1887-\u18A8\u18AA\uA840-\uA871\u{10AC0}-\u{10AC5}\u{10AC7}\u{10AC9}\u{10ACA}\u{10ACE}-\u{10AD6}\u{10AD8}-\u{10AE1}\u{10AE4}\u{10AEB}-\u{10AEF}\u{10B80}-\u{10B91}\u{10BA9}-\u{10BAE}\u{10D01}-\u{10D23}\u{10F30}-\u{10F44}\u{10F51}-\u{10F54}\u{10F70}-\u{10F81}\u{10FB0}\u{10FB2}-\u{10FB6}\u{10FB8}-\u{10FBF}\u{10FC1}-\u{10FC4}\u{10FC9}\u{10FCA}\u{1E900}-\u{1E943}]/u; var bidiDomain = /[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05EF-\u05F4\u0600-\u0605\u0608\u060B\u060D\u061B-\u064A\u0660-\u0669\u066B-\u066F\u0671-\u06D5\u06DD\u06E5\u06E6\u06EE\u06EF\u06FA-\u070D\u070F\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u07FE-\u0815\u081A\u0824\u0828\u0830-\u083E\u0840-\u0858\u085E\u0860-\u086A\u0870-\u088E\u0890\u0891\u08A0-\u08C9\u08E2\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBC2\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFC\uFE70-\uFE74\uFE76-\uFEFC\u{10800}-\u{10805}\u{10808}\u{1080A}-\u{10835}\u{10837}\u{10838}\u{1083C}\u{1083F}-\u{10855}\u{10857}-\u{1089E}\u{108A7}-\u{108AF}\u{108E0}-\u{108F2}\u{108F4}\u{108F5}\u{108FB}-\u{1091B}\u{10920}-\u{10939}\u{1093F}\u{10980}-\u{109B7}\u{109BC}-\u{109CF}\u{109D2}-\u{10A00}\u{10A10}-\u{10A13}\u{10A15}-\u{10A17}\u{10A19}-\u{10A35}\u{10A40}-\u{10A48}\u{10A50}-\u{10A58}\u{10A60}-\u{10A9F}\u{10AC0}-\u{10AE4}\u{10AEB}-\u{10AF6}\u{10B00}-\u{10B35}\u{10B40}-\u{10B55}\u{10B58}-\u{10B72}\u{10B78}-\u{10B91}\u{10B99}-\u{10B9C}\u{10BA9}-\u{10BAF}\u{10C00}-\u{10C48}\u{10C80}-\u{10CB2}\u{10CC0}-\u{10CF2}\u{10CFA}-\u{10D23}\u{10D30}-\u{10D39}\u{10E60}-\u{10E7E}\u{10E80}-\u{10EA9}\u{10EAD}\u{10EB0}\u{10EB1}\u{10F00}-\u{10F27}\u{10F30}-\u{10F45}\u{10F51}-\u{10F59}\u{10F70}-\u{10F81}\u{10F86}-\u{10F89}\u{10FB0}-\u{10FCB}\u{10FE0}-\u{10FF6}\u{1E800}-\u{1E8C4}\u{1E8C7}-\u{1E8CF}\u{1E900}-\u{1E943}\u{1E94B}\u{1E950}-\u{1E959}\u{1E95E}\u{1E95F}\u{1EC71}-\u{1ECB4}\u{1ED01}-\u{1ED3D}\u{1EE00}-\u{1EE03}\u{1EE05}-\u{1EE1F}\u{1EE21}\u{1EE22}\u{1EE24}\u{1EE27}\u{1EE29}-\u{1EE32}\u{1EE34}-\u{1EE37}\u{1EE39}\u{1EE3B}\u{1EE42}\u{1EE47}\u{1EE49}\u{1EE4B}\u{1EE4D}-\u{1EE4F}\u{1EE51}\u{1EE52}\u{1EE54}\u{1EE57}\u{1EE59}\u{1EE5B}\u{1EE5D}\u{1EE5F}\u{1EE61}\u{1EE62}\u{1EE64}\u{1EE67}-\u{1EE6A}\u{1EE6C}-\u{1EE72}\u{1EE74}-\u{1EE77}\u{1EE79}-\u{1EE7C}\u{1EE7E}\u{1EE80}-\u{1EE89}\u{1EE8B}-\u{1EE9B}\u{1EEA1}-\u{1EEA3}\u{1EEA5}-\u{1EEA9}\u{1EEAB}-\u{1EEBB}]/u; - var bidiS1LTR = /[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02BB-\u02C1\u02D0\u02D1\u02E0-\u02E4\u02EE\u0370-\u0373\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0482\u048A-\u052F\u0531-\u0556\u0559-\u0589\u0903-\u0939\u093B\u093D-\u0940\u0949-\u094C\u094E-\u0950\u0958-\u0961\u0964-\u0980\u0982\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD-\u09C0\u09C7\u09C8\u09CB\u09CC\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09FA\u09FC\u09FD\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3E-\u0A40\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A76\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD-\u0AC0\u0AC9\u0ACB\u0ACC\u0AD0\u0AE0\u0AE1\u0AE6-\u0AF0\u0AF9\u0B02\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B3E\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0B57\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0BD0\u0BD7\u0BE6-\u0BF2\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C41-\u0C44\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C77\u0C7F\u0C80\u0C82-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD-\u0CC4\u0CC6-\u0CC8\u0CCA\u0CCB\u0CD5\u0CD6\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1-\u0CF3\u0D02-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D4E\u0D4F\u0D54-\u0D61\u0D66-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCF-\u0DD1\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E4F-\u0E5B\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00-\u0F17\u0F1A-\u0F34\u0F36\u0F38\u0F3E-\u0F47\u0F49-\u0F6C\u0F7F\u0F85\u0F88-\u0F8C\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE-\u0FDA\u1000-\u102C\u1031\u1038\u103B\u103C\u103F-\u1057\u105A-\u105D\u1061-\u1070\u1075-\u1081\u1083\u1084\u1087-\u108C\u108E-\u109C\u109E-\u10C5\u10C7\u10CD\u10D0-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1360-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u167F\u1681-\u169A\u16A0-\u16F8\u1700-\u1711\u1715\u171F-\u1731\u1734-\u1736\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17B6\u17BE-\u17C5\u17C7\u17C8\u17D4-\u17DA\u17DC\u17E0-\u17E9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A19\u1A1A\u1A1E-\u1A55\u1A57\u1A61\u1A63\u1A64\u1A6D-\u1A72\u1A80-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD\u1B04-\u1B33\u1B35\u1B3B\u1B3D-\u1B41\u1B43-\u1B4C\u1B50-\u1B6A\u1B74-\u1B7E\u1B82-\u1BA1\u1BA6\u1BA7\u1BAA\u1BAE-\u1BE5\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1BFC-\u1C2B\u1C34\u1C35\u1C3B-\u1C49\u1C4D-\u1C88\u1C90-\u1CBA\u1CBD-\u1CC7\u1CD3\u1CE1\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5-\u1CF7\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200E\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u214F\u2160-\u2188\u2336-\u237A\u2395\u249C-\u24E9\u26AC\u2800-\u28FF\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D70\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u302E\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3190-\u31BF\u31F0-\u321C\u3220-\u324F\u3260-\u327B\u327F-\u32B0\u32C0-\u32CB\u32D0-\u3376\u337B-\u33DD\u33E0-\u33FE\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA60C\uA610-\uA62B\uA640-\uA66E\uA680-\uA69D\uA6A0-\uA6EF\uA6F2-\uA6F7\uA722-\uA787\uA789-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA824\uA827\uA830-\uA837\uA840-\uA873\uA880-\uA8C3\uA8CE-\uA8D9\uA8F2-\uA8FE\uA900-\uA925\uA92E-\uA946\uA952\uA953\uA95F-\uA97C\uA983-\uA9B2\uA9B4\uA9B5\uA9BA\uA9BB\uA9BE-\uA9CD\uA9CF-\uA9D9\uA9DE-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA2F\uAA30\uAA33\uAA34\uAA40-\uAA42\uAA44-\uAA4B\uAA4D\uAA50-\uAA59\uAA5C-\uAA7B\uAA7D-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAAEB\uAAEE-\uAAF5\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB69\uAB70-\uABE4\uABE6\uABE7\uABE9-\uABEC\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uD800-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\u{10000}-\u{1000B}\u{1000D}-\u{10026}\u{10028}-\u{1003A}\u{1003C}\u{1003D}\u{1003F}-\u{1004D}\u{10050}-\u{1005D}\u{10080}-\u{100FA}\u{10100}\u{10102}\u{10107}-\u{10133}\u{10137}-\u{1013F}\u{1018D}\u{1018E}\u{101D0}-\u{101FC}\u{10280}-\u{1029C}\u{102A0}-\u{102D0}\u{10300}-\u{10323}\u{1032D}-\u{1034A}\u{10350}-\u{10375}\u{10380}-\u{1039D}\u{1039F}-\u{103C3}\u{103C8}-\u{103D5}\u{10400}-\u{1049D}\u{104A0}-\u{104A9}\u{104B0}-\u{104D3}\u{104D8}-\u{104FB}\u{10500}-\u{10527}\u{10530}-\u{10563}\u{1056F}-\u{1057A}\u{1057C}-\u{1058A}\u{1058C}-\u{10592}\u{10594}\u{10595}\u{10597}-\u{105A1}\u{105A3}-\u{105B1}\u{105B3}-\u{105B9}\u{105BB}\u{105BC}\u{10600}-\u{10736}\u{10740}-\u{10755}\u{10760}-\u{10767}\u{10780}-\u{10785}\u{10787}-\u{107B0}\u{107B2}-\u{107BA}\u{11000}\u{11002}-\u{11037}\u{11047}-\u{1104D}\u{11066}-\u{1106F}\u{11071}\u{11072}\u{11075}\u{11082}-\u{110B2}\u{110B7}\u{110B8}\u{110BB}-\u{110C1}\u{110CD}\u{110D0}-\u{110E8}\u{110F0}-\u{110F9}\u{11103}-\u{11126}\u{1112C}\u{11136}-\u{11147}\u{11150}-\u{11172}\u{11174}-\u{11176}\u{11182}-\u{111B5}\u{111BF}-\u{111C8}\u{111CD}\u{111CE}\u{111D0}-\u{111DF}\u{111E1}-\u{111F4}\u{11200}-\u{11211}\u{11213}-\u{1122E}\u{11232}\u{11233}\u{11235}\u{11238}-\u{1123D}\u{1123F}\u{11240}\u{11280}-\u{11286}\u{11288}\u{1128A}-\u{1128D}\u{1128F}-\u{1129D}\u{1129F}-\u{112A9}\u{112B0}-\u{112DE}\u{112E0}-\u{112E2}\u{112F0}-\u{112F9}\u{11302}\u{11303}\u{11305}-\u{1130C}\u{1130F}\u{11310}\u{11313}-\u{11328}\u{1132A}-\u{11330}\u{11332}\u{11333}\u{11335}-\u{11339}\u{1133D}-\u{1133F}\u{11341}-\u{11344}\u{11347}\u{11348}\u{1134B}-\u{1134D}\u{11350}\u{11357}\u{1135D}-\u{11363}\u{11400}-\u{11437}\u{11440}\u{11441}\u{11445}\u{11447}-\u{1145B}\u{1145D}\u{1145F}-\u{11461}\u{11480}-\u{114B2}\u{114B9}\u{114BB}-\u{114BE}\u{114C1}\u{114C4}-\u{114C7}\u{114D0}-\u{114D9}\u{11580}-\u{115B1}\u{115B8}-\u{115BB}\u{115BE}\u{115C1}-\u{115DB}\u{11600}-\u{11632}\u{1163B}\u{1163C}\u{1163E}\u{11641}-\u{11644}\u{11650}-\u{11659}\u{11680}-\u{116AA}\u{116AC}\u{116AE}\u{116AF}\u{116B6}\u{116B8}\u{116B9}\u{116C0}-\u{116C9}\u{11700}-\u{1171A}\u{11720}\u{11721}\u{11726}\u{11730}-\u{11746}\u{11800}-\u{1182E}\u{11838}\u{1183B}\u{118A0}-\u{118F2}\u{118FF}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}\u{11916}\u{11918}-\u{11935}\u{11937}\u{11938}\u{1193D}\u{1193F}-\u{11942}\u{11944}-\u{11946}\u{11950}-\u{11959}\u{119A0}-\u{119A7}\u{119AA}-\u{119D3}\u{119DC}-\u{119DF}\u{119E1}-\u{119E4}\u{11A00}\u{11A07}\u{11A08}\u{11A0B}-\u{11A32}\u{11A39}\u{11A3A}\u{11A3F}-\u{11A46}\u{11A50}\u{11A57}\u{11A58}\u{11A5C}-\u{11A89}\u{11A97}\u{11A9A}-\u{11AA2}\u{11AB0}-\u{11AF8}\u{11B00}-\u{11B09}\u{11C00}-\u{11C08}\u{11C0A}-\u{11C2F}\u{11C3E}-\u{11C45}\u{11C50}-\u{11C6C}\u{11C70}-\u{11C8F}\u{11CA9}\u{11CB1}\u{11CB4}\u{11D00}-\u{11D06}\u{11D08}\u{11D09}\u{11D0B}-\u{11D30}\u{11D46}\u{11D50}-\u{11D59}\u{11D60}-\u{11D65}\u{11D67}\u{11D68}\u{11D6A}-\u{11D8E}\u{11D93}\u{11D94}\u{11D96}\u{11D98}\u{11DA0}-\u{11DA9}\u{11EE0}-\u{11EF2}\u{11EF5}-\u{11EF8}\u{11F02}-\u{11F10}\u{11F12}-\u{11F35}\u{11F3E}\u{11F3F}\u{11F41}\u{11F43}-\u{11F59}\u{11FB0}\u{11FC0}-\u{11FD4}\u{11FFF}-\u{12399}\u{12400}-\u{1246E}\u{12470}-\u{12474}\u{12480}-\u{12543}\u{12F90}-\u{12FF2}\u{13000}-\u{1343F}\u{13441}-\u{13446}\u{14400}-\u{14646}\u{16800}-\u{16A38}\u{16A40}-\u{16A5E}\u{16A60}-\u{16A69}\u{16A6E}-\u{16ABE}\u{16AC0}-\u{16AC9}\u{16AD0}-\u{16AED}\u{16AF5}\u{16B00}-\u{16B2F}\u{16B37}-\u{16B45}\u{16B50}-\u{16B59}\u{16B5B}-\u{16B61}\u{16B63}-\u{16B77}\u{16B7D}-\u{16B8F}\u{16E40}-\u{16E9A}\u{16F00}-\u{16F4A}\u{16F50}-\u{16F87}\u{16F93}-\u{16F9F}\u{16FE0}\u{16FE1}\u{16FE3}\u{16FF0}\u{16FF1}\u{17000}-\u{187F7}\u{18800}-\u{18CD5}\u{18D00}-\u{18D08}\u{1AFF0}-\u{1AFF3}\u{1AFF5}-\u{1AFFB}\u{1AFFD}\u{1AFFE}\u{1B000}-\u{1B122}\u{1B132}\u{1B150}-\u{1B152}\u{1B155}\u{1B164}-\u{1B167}\u{1B170}-\u{1B2FB}\u{1BC00}-\u{1BC6A}\u{1BC70}-\u{1BC7C}\u{1BC80}-\u{1BC88}\u{1BC90}-\u{1BC99}\u{1BC9C}\u{1BC9F}\u{1CF50}-\u{1CFC3}\u{1D000}-\u{1D0F5}\u{1D100}-\u{1D126}\u{1D129}-\u{1D166}\u{1D16A}-\u{1D172}\u{1D183}\u{1D184}\u{1D18C}-\u{1D1A9}\u{1D1AE}-\u{1D1E8}\u{1D2C0}-\u{1D2D3}\u{1D2E0}-\u{1D2F3}\u{1D360}-\u{1D378}\u{1D400}-\u{1D454}\u{1D456}-\u{1D49C}\u{1D49E}\u{1D49F}\u{1D4A2}\u{1D4A5}\u{1D4A6}\u{1D4A9}-\u{1D4AC}\u{1D4AE}-\u{1D4B9}\u{1D4BB}\u{1D4BD}-\u{1D4C3}\u{1D4C5}-\u{1D505}\u{1D507}-\u{1D50A}\u{1D50D}-\u{1D514}\u{1D516}-\u{1D51C}\u{1D51E}-\u{1D539}\u{1D53B}-\u{1D53E}\u{1D540}-\u{1D544}\u{1D546}\u{1D54A}-\u{1D550}\u{1D552}-\u{1D6A5}\u{1D6A8}-\u{1D6DA}\u{1D6DC}-\u{1D714}\u{1D716}-\u{1D74E}\u{1D750}-\u{1D788}\u{1D78A}-\u{1D7C2}\u{1D7C4}-\u{1D7CB}\u{1D800}-\u{1D9FF}\u{1DA37}-\u{1DA3A}\u{1DA6D}-\u{1DA74}\u{1DA76}-\u{1DA83}\u{1DA85}-\u{1DA8B}\u{1DF00}-\u{1DF1E}\u{1DF25}-\u{1DF2A}\u{1E030}-\u{1E06D}\u{1E100}-\u{1E12C}\u{1E137}-\u{1E13D}\u{1E140}-\u{1E149}\u{1E14E}\u{1E14F}\u{1E290}-\u{1E2AD}\u{1E2C0}-\u{1E2EB}\u{1E2F0}-\u{1E2F9}\u{1E4D0}-\u{1E4EB}\u{1E4F0}-\u{1E4F9}\u{1E7E0}-\u{1E7E6}\u{1E7E8}-\u{1E7EB}\u{1E7ED}\u{1E7EE}\u{1E7F0}-\u{1E7FE}\u{1F110}-\u{1F12E}\u{1F130}-\u{1F169}\u{1F170}-\u{1F1AC}\u{1F1E6}-\u{1F202}\u{1F210}-\u{1F23B}\u{1F240}-\u{1F248}\u{1F250}\u{1F251}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B739}\u{2B740}-\u{2B81D}\u{2B820}-\u{2CEA1}\u{2CEB0}-\u{2EBE0}\u{2F800}-\u{2FA1D}\u{30000}-\u{3134A}\u{31350}-\u{323AF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u; + var bidiS1LTR = /[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02BB-\u02C1\u02D0\u02D1\u02E0-\u02E4\u02EE\u0370-\u0373\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0482\u048A-\u052F\u0531-\u0556\u0559-\u0589\u0903-\u0939\u093B\u093D-\u0940\u0949-\u094C\u094E-\u0950\u0958-\u0961\u0964-\u0980\u0982\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD-\u09C0\u09C7\u09C8\u09CB\u09CC\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09FA\u09FC\u09FD\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3E-\u0A40\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A76\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD-\u0AC0\u0AC9\u0ACB\u0ACC\u0AD0\u0AE0\u0AE1\u0AE6-\u0AF0\u0AF9\u0B02\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B3E\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0B57\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0BD0\u0BD7\u0BE6-\u0BF2\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C41-\u0C44\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C77\u0C7F\u0C80\u0C82-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD-\u0CC4\u0CC6-\u0CC8\u0CCA\u0CCB\u0CD5\u0CD6\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1-\u0CF3\u0D02-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D4E\u0D4F\u0D54-\u0D61\u0D66-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCF-\u0DD1\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E4F-\u0E5B\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00-\u0F17\u0F1A-\u0F34\u0F36\u0F38\u0F3E-\u0F47\u0F49-\u0F6C\u0F7F\u0F85\u0F88-\u0F8C\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE-\u0FDA\u1000-\u102C\u1031\u1038\u103B\u103C\u103F-\u1057\u105A-\u105D\u1061-\u1070\u1075-\u1081\u1083\u1084\u1087-\u108C\u108E-\u109C\u109E-\u10C5\u10C7\u10CD\u10D0-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1360-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u167F\u1681-\u169A\u16A0-\u16F8\u1700-\u1711\u1715\u171F-\u1731\u1734-\u1736\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17B6\u17BE-\u17C5\u17C7\u17C8\u17D4-\u17DA\u17DC\u17E0-\u17E9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A19\u1A1A\u1A1E-\u1A55\u1A57\u1A61\u1A63\u1A64\u1A6D-\u1A72\u1A80-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD\u1B04-\u1B33\u1B35\u1B3B\u1B3D-\u1B41\u1B43-\u1B4C\u1B50-\u1B6A\u1B74-\u1B7E\u1B82-\u1BA1\u1BA6\u1BA7\u1BAA\u1BAE-\u1BE5\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1BFC-\u1C2B\u1C34\u1C35\u1C3B-\u1C49\u1C4D-\u1C88\u1C90-\u1CBA\u1CBD-\u1CC7\u1CD3\u1CE1\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5-\u1CF7\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200E\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u214F\u2160-\u2188\u2336-\u237A\u2395\u249C-\u24E9\u26AC\u2800-\u28FF\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D70\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u302E\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3190-\u31BF\u31F0-\u321C\u3220-\u324F\u3260-\u327B\u327F-\u32B0\u32C0-\u32CB\u32D0-\u3376\u337B-\u33DD\u33E0-\u33FE\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA60C\uA610-\uA62B\uA640-\uA66E\uA680-\uA69D\uA6A0-\uA6EF\uA6F2-\uA6F7\uA722-\uA787\uA789-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA824\uA827\uA830-\uA837\uA840-\uA873\uA880-\uA8C3\uA8CE-\uA8D9\uA8F2-\uA8FE\uA900-\uA925\uA92E-\uA946\uA952\uA953\uA95F-\uA97C\uA983-\uA9B2\uA9B4\uA9B5\uA9BA\uA9BB\uA9BE-\uA9CD\uA9CF-\uA9D9\uA9DE-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA2F\uAA30\uAA33\uAA34\uAA40-\uAA42\uAA44-\uAA4B\uAA4D\uAA50-\uAA59\uAA5C-\uAA7B\uAA7D-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAAEB\uAAEE-\uAAF5\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB69\uAB70-\uABE4\uABE6\uABE7\uABE9-\uABEC\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uD800-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\u{10000}-\u{1000B}\u{1000D}-\u{10026}\u{10028}-\u{1003A}\u{1003C}\u{1003D}\u{1003F}-\u{1004D}\u{10050}-\u{1005D}\u{10080}-\u{100FA}\u{10100}\u{10102}\u{10107}-\u{10133}\u{10137}-\u{1013F}\u{1018D}\u{1018E}\u{101D0}-\u{101FC}\u{10280}-\u{1029C}\u{102A0}-\u{102D0}\u{10300}-\u{10323}\u{1032D}-\u{1034A}\u{10350}-\u{10375}\u{10380}-\u{1039D}\u{1039F}-\u{103C3}\u{103C8}-\u{103D5}\u{10400}-\u{1049D}\u{104A0}-\u{104A9}\u{104B0}-\u{104D3}\u{104D8}-\u{104FB}\u{10500}-\u{10527}\u{10530}-\u{10563}\u{1056F}-\u{1057A}\u{1057C}-\u{1058A}\u{1058C}-\u{10592}\u{10594}\u{10595}\u{10597}-\u{105A1}\u{105A3}-\u{105B1}\u{105B3}-\u{105B9}\u{105BB}\u{105BC}\u{10600}-\u{10736}\u{10740}-\u{10755}\u{10760}-\u{10767}\u{10780}-\u{10785}\u{10787}-\u{107B0}\u{107B2}-\u{107BA}\u{11000}\u{11002}-\u{11037}\u{11047}-\u{1104D}\u{11066}-\u{1106F}\u{11071}\u{11072}\u{11075}\u{11082}-\u{110B2}\u{110B7}\u{110B8}\u{110BB}-\u{110C1}\u{110CD}\u{110D0}-\u{110E8}\u{110F0}-\u{110F9}\u{11103}-\u{11126}\u{1112C}\u{11136}-\u{11147}\u{11150}-\u{11172}\u{11174}-\u{11176}\u{11182}-\u{111B5}\u{111BF}-\u{111C8}\u{111CD}\u{111CE}\u{111D0}-\u{111DF}\u{111E1}-\u{111F4}\u{11200}-\u{11211}\u{11213}-\u{1122E}\u{11232}\u{11233}\u{11235}\u{11238}-\u{1123D}\u{1123F}\u{11240}\u{11280}-\u{11286}\u{11288}\u{1128A}-\u{1128D}\u{1128F}-\u{1129D}\u{1129F}-\u{112A9}\u{112B0}-\u{112DE}\u{112E0}-\u{112E2}\u{112F0}-\u{112F9}\u{11302}\u{11303}\u{11305}-\u{1130C}\u{1130F}\u{11310}\u{11313}-\u{11328}\u{1132A}-\u{11330}\u{11332}\u{11333}\u{11335}-\u{11339}\u{1133D}-\u{1133F}\u{11341}-\u{11344}\u{11347}\u{11348}\u{1134B}-\u{1134D}\u{11350}\u{11357}\u{1135D}-\u{11363}\u{11400}-\u{11437}\u{11440}\u{11441}\u{11445}\u{11447}-\u{1145B}\u{1145D}\u{1145F}-\u{11461}\u{11480}-\u{114B2}\u{114B9}\u{114BB}-\u{114BE}\u{114C1}\u{114C4}-\u{114C7}\u{114D0}-\u{114D9}\u{11580}-\u{115B1}\u{115B8}-\u{115BB}\u{115BE}\u{115C1}-\u{115DB}\u{11600}-\u{11632}\u{1163B}\u{1163C}\u{1163E}\u{11641}-\u{11644}\u{11650}-\u{11659}\u{11680}-\u{116AA}\u{116AC}\u{116AE}\u{116AF}\u{116B6}\u{116B8}\u{116B9}\u{116C0}-\u{116C9}\u{11700}-\u{1171A}\u{11720}\u{11721}\u{11726}\u{11730}-\u{11746}\u{11800}-\u{1182E}\u{11838}\u{1183B}\u{118A0}-\u{118F2}\u{118FF}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}\u{11916}\u{11918}-\u{11935}\u{11937}\u{11938}\u{1193D}\u{1193F}-\u{11942}\u{11944}-\u{11946}\u{11950}-\u{11959}\u{119A0}-\u{119A7}\u{119AA}-\u{119D3}\u{119DC}-\u{119DF}\u{119E1}-\u{119E4}\u{11A00}\u{11A07}\u{11A08}\u{11A0B}-\u{11A32}\u{11A39}\u{11A3A}\u{11A3F}-\u{11A46}\u{11A50}\u{11A57}\u{11A58}\u{11A5C}-\u{11A89}\u{11A97}\u{11A9A}-\u{11AA2}\u{11AB0}-\u{11AF8}\u{11B00}-\u{11B09}\u{11C00}-\u{11C08}\u{11C0A}-\u{11C2F}\u{11C3E}-\u{11C45}\u{11C50}-\u{11C6C}\u{11C70}-\u{11C8F}\u{11CA9}\u{11CB1}\u{11CB4}\u{11D00}-\u{11D06}\u{11D08}\u{11D09}\u{11D0B}-\u{11D30}\u{11D46}\u{11D50}-\u{11D59}\u{11D60}-\u{11D65}\u{11D67}\u{11D68}\u{11D6A}-\u{11D8E}\u{11D93}\u{11D94}\u{11D96}\u{11D98}\u{11DA0}-\u{11DA9}\u{11EE0}-\u{11EF2}\u{11EF5}-\u{11EF8}\u{11F02}-\u{11F10}\u{11F12}-\u{11F35}\u{11F3E}\u{11F3F}\u{11F41}\u{11F43}-\u{11F59}\u{11FB0}\u{11FC0}-\u{11FD4}\u{11FFF}-\u{12399}\u{12400}-\u{1246E}\u{12470}-\u{12474}\u{12480}-\u{12543}\u{12F90}-\u{12FF2}\u{13000}-\u{1343F}\u{13441}-\u{13446}\u{14400}-\u{14646}\u{16800}-\u{16A38}\u{16A40}-\u{16A5E}\u{16A60}-\u{16A69}\u{16A6E}-\u{16ABE}\u{16AC0}-\u{16AC9}\u{16AD0}-\u{16AED}\u{16AF5}\u{16B00}-\u{16B2F}\u{16B37}-\u{16B45}\u{16B50}-\u{16B59}\u{16B5B}-\u{16B61}\u{16B63}-\u{16B77}\u{16B7D}-\u{16B8F}\u{16E40}-\u{16E9A}\u{16F00}-\u{16F4A}\u{16F50}-\u{16F87}\u{16F93}-\u{16F9F}\u{16FE0}\u{16FE1}\u{16FE3}\u{16FF0}\u{16FF1}\u{17000}-\u{187F7}\u{18800}-\u{18CD5}\u{18D00}-\u{18D08}\u{1AFF0}-\u{1AFF3}\u{1AFF5}-\u{1AFFB}\u{1AFFD}\u{1AFFE}\u{1B000}-\u{1B122}\u{1B132}\u{1B150}-\u{1B152}\u{1B155}\u{1B164}-\u{1B167}\u{1B170}-\u{1B2FB}\u{1BC00}-\u{1BC6A}\u{1BC70}-\u{1BC7C}\u{1BC80}-\u{1BC88}\u{1BC90}-\u{1BC99}\u{1BC9C}\u{1BC9F}\u{1CF50}-\u{1CFC3}\u{1D000}-\u{1D0F5}\u{1D100}-\u{1D126}\u{1D129}-\u{1D166}\u{1D16A}-\u{1D172}\u{1D183}\u{1D184}\u{1D18C}-\u{1D1A9}\u{1D1AE}-\u{1D1E8}\u{1D2C0}-\u{1D2D3}\u{1D2E0}-\u{1D2F3}\u{1D360}-\u{1D378}\u{1D400}-\u{1D454}\u{1D456}-\u{1D49C}\u{1D49E}\u{1D49F}\u{1D4A2}\u{1D4A5}\u{1D4A6}\u{1D4A9}-\u{1D4AC}\u{1D4AE}-\u{1D4B9}\u{1D4BB}\u{1D4BD}-\u{1D4C3}\u{1D4C5}-\u{1D505}\u{1D507}-\u{1D50A}\u{1D50D}-\u{1D514}\u{1D516}-\u{1D51C}\u{1D51E}-\u{1D539}\u{1D53B}-\u{1D53E}\u{1D540}-\u{1D544}\u{1D546}\u{1D54A}-\u{1D550}\u{1D552}-\u{1D6A5}\u{1D6A8}-\u{1D6DA}\u{1D6DC}-\u{1D714}\u{1D716}-\u{1D74E}\u{1D750}-\u{1D788}\u{1D78A}-\u{1D7C2}\u{1D7C4}-\u{1D7CB}\u{1D800}-\u{1D9FF}\u{1DA37}-\u{1DA3A}\u{1DA6D}-\u{1DA74}\u{1DA76}-\u{1DA83}\u{1DA85}-\u{1DA8B}\u{1DF00}-\u{1DF1E}\u{1DF25}-\u{1DF2A}\u{1E030}-\u{1E06D}\u{1E100}-\u{1E12C}\u{1E137}-\u{1E13D}\u{1E140}-\u{1E149}\u{1E14E}\u{1E14F}\u{1E290}-\u{1E2AD}\u{1E2C0}-\u{1E2EB}\u{1E2F0}-\u{1E2F9}\u{1E4D0}-\u{1E4EB}\u{1E4F0}-\u{1E4F9}\u{1E7E0}-\u{1E7E6}\u{1E7E8}-\u{1E7EB}\u{1E7ED}\u{1E7EE}\u{1E7F0}-\u{1E7FE}\u{1F110}-\u{1F12E}\u{1F130}-\u{1F169}\u{1F170}-\u{1F1AC}\u{1F1E6}-\u{1F202}\u{1F210}-\u{1F23B}\u{1F240}-\u{1F248}\u{1F250}\u{1F251}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B739}\u{2B740}-\u{2B81D}\u{2B820}-\u{2CEA1}\u{2CEB0}-\u{2EBE0}\u{2EBF0}-\u{2EE5D}\u{2F800}-\u{2FA1D}\u{30000}-\u{3134A}\u{31350}-\u{323AF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u; var bidiS1RTL = /[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05EF-\u05F4\u0608\u060B\u060D\u061B-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u070D\u070F\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u07FE-\u0815\u081A\u0824\u0828\u0830-\u083E\u0840-\u0858\u085E\u0860-\u086A\u0870-\u088E\u08A0-\u08C9\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBC2\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFC\uFE70-\uFE74\uFE76-\uFEFC\u{10800}-\u{10805}\u{10808}\u{1080A}-\u{10835}\u{10837}\u{10838}\u{1083C}\u{1083F}-\u{10855}\u{10857}-\u{1089E}\u{108A7}-\u{108AF}\u{108E0}-\u{108F2}\u{108F4}\u{108F5}\u{108FB}-\u{1091B}\u{10920}-\u{10939}\u{1093F}\u{10980}-\u{109B7}\u{109BC}-\u{109CF}\u{109D2}-\u{10A00}\u{10A10}-\u{10A13}\u{10A15}-\u{10A17}\u{10A19}-\u{10A35}\u{10A40}-\u{10A48}\u{10A50}-\u{10A58}\u{10A60}-\u{10A9F}\u{10AC0}-\u{10AE4}\u{10AEB}-\u{10AF6}\u{10B00}-\u{10B35}\u{10B40}-\u{10B55}\u{10B58}-\u{10B72}\u{10B78}-\u{10B91}\u{10B99}-\u{10B9C}\u{10BA9}-\u{10BAF}\u{10C00}-\u{10C48}\u{10C80}-\u{10CB2}\u{10CC0}-\u{10CF2}\u{10CFA}-\u{10D23}\u{10E80}-\u{10EA9}\u{10EAD}\u{10EB0}\u{10EB1}\u{10F00}-\u{10F27}\u{10F30}-\u{10F45}\u{10F51}-\u{10F59}\u{10F70}-\u{10F81}\u{10F86}-\u{10F89}\u{10FB0}-\u{10FCB}\u{10FE0}-\u{10FF6}\u{1E800}-\u{1E8C4}\u{1E8C7}-\u{1E8CF}\u{1E900}-\u{1E943}\u{1E94B}\u{1E950}-\u{1E959}\u{1E95E}\u{1E95F}\u{1EC71}-\u{1ECB4}\u{1ED01}-\u{1ED3D}\u{1EE00}-\u{1EE03}\u{1EE05}-\u{1EE1F}\u{1EE21}\u{1EE22}\u{1EE24}\u{1EE27}\u{1EE29}-\u{1EE32}\u{1EE34}-\u{1EE37}\u{1EE39}\u{1EE3B}\u{1EE42}\u{1EE47}\u{1EE49}\u{1EE4B}\u{1EE4D}-\u{1EE4F}\u{1EE51}\u{1EE52}\u{1EE54}\u{1EE57}\u{1EE59}\u{1EE5B}\u{1EE5D}\u{1EE5F}\u{1EE61}\u{1EE62}\u{1EE64}\u{1EE67}-\u{1EE6A}\u{1EE6C}-\u{1EE72}\u{1EE74}-\u{1EE77}\u{1EE79}-\u{1EE7C}\u{1EE7E}\u{1EE80}-\u{1EE89}\u{1EE8B}-\u{1EE9B}\u{1EEA1}-\u{1EEA3}\u{1EEA5}-\u{1EEA9}\u{1EEAB}-\u{1EEBB}]/u; - var bidiS2 = /^[\0-\x08\x0E-\x1B!-@\[-`\{-\x84\x86-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02B9\u02BA\u02C2-\u02CF\u02D2-\u02DF\u02E5-\u02ED\u02EF-\u036F\u0374\u0375\u037E\u0384\u0385\u0387\u03F6\u0483-\u0489\u058A\u058D-\u058F\u0591-\u05C7\u05D0-\u05EA\u05EF-\u05F4\u0600-\u070D\u070F-\u074A\u074D-\u07B1\u07C0-\u07FA\u07FD-\u082D\u0830-\u083E\u0840-\u085B\u085E\u0860-\u086A\u0870-\u088E\u0890\u0891\u0898-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09F2\u09F3\u09FB\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AF1\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B55\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0BF3-\u0BFA\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C78-\u0C7E\u0C81\u0CBC\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0D81\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E3F\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39-\u0F3D\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1390-\u1399\u1400\u169B\u169C\u1712-\u1714\u1732\u1733\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DB\u17DD\u17F0-\u17F9\u1800-\u180F\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1940\u1944\u1945\u19DE-\u19FF\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DFF\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u200B-\u200D\u200F-\u2027\u202F-\u205E\u2060-\u2064\u206A-\u2070\u2074-\u207E\u2080-\u208E\u20A0-\u20C0\u20D0-\u20F0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u2150-\u215F\u2189-\u218B\u2190-\u2335\u237B-\u2394\u2396-\u2426\u2440-\u244A\u2460-\u249B\u24EA-\u26AB\u26AD-\u27FF\u2900-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2CEF-\u2CF1\u2CF9-\u2CFF\u2D7F\u2DE0-\u2E5D\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\u3004\u3008-\u3020\u302A-\u302D\u3030\u3036\u3037\u303D-\u303F\u3099-\u309C\u30A0\u30FB\u31C0-\u31E3\u321D\u321E\u3250-\u325F\u327C-\u327E\u32B1-\u32BF\u32CC-\u32CF\u3377-\u337A\u33DE\u33DF\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA60D-\uA60F\uA66F-\uA67F\uA69E\uA69F\uA6F0\uA6F1\uA700-\uA721\uA788\uA802\uA806\uA80B\uA825\uA826\uA828-\uA82C\uA838\uA839\uA874-\uA877\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uAB6A\uAB6B\uABE5\uABE8\uABED\uFB1D-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBC2\uFBD3-\uFD8F\uFD92-\uFDC7\uFDCF\uFDF0-\uFE19\uFE20-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFE70-\uFE74\uFE76-\uFEFC\uFEFF\uFF01-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD\u{10101}\u{10140}-\u{1018C}\u{10190}-\u{1019C}\u{101A0}\u{101FD}\u{102E0}-\u{102FB}\u{10376}-\u{1037A}\u{10800}-\u{10805}\u{10808}\u{1080A}-\u{10835}\u{10837}\u{10838}\u{1083C}\u{1083F}-\u{10855}\u{10857}-\u{1089E}\u{108A7}-\u{108AF}\u{108E0}-\u{108F2}\u{108F4}\u{108F5}\u{108FB}-\u{1091B}\u{1091F}-\u{10939}\u{1093F}\u{10980}-\u{109B7}\u{109BC}-\u{109CF}\u{109D2}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A13}\u{10A15}-\u{10A17}\u{10A19}-\u{10A35}\u{10A38}-\u{10A3A}\u{10A3F}-\u{10A48}\u{10A50}-\u{10A58}\u{10A60}-\u{10A9F}\u{10AC0}-\u{10AE6}\u{10AEB}-\u{10AF6}\u{10B00}-\u{10B35}\u{10B39}-\u{10B55}\u{10B58}-\u{10B72}\u{10B78}-\u{10B91}\u{10B99}-\u{10B9C}\u{10BA9}-\u{10BAF}\u{10C00}-\u{10C48}\u{10C80}-\u{10CB2}\u{10CC0}-\u{10CF2}\u{10CFA}-\u{10D27}\u{10D30}-\u{10D39}\u{10E60}-\u{10E7E}\u{10E80}-\u{10EA9}\u{10EAB}-\u{10EAD}\u{10EB0}\u{10EB1}\u{10EFD}-\u{10F27}\u{10F30}-\u{10F59}\u{10F70}-\u{10F89}\u{10FB0}-\u{10FCB}\u{10FE0}-\u{10FF6}\u{11001}\u{11038}-\u{11046}\u{11052}-\u{11065}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{11660}-\u{1166C}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{1193B}\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A06}\u{11A09}\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{11F00}\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{11FD5}-\u{11FF1}\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE2}\u{16FE4}\u{1BC9D}\u{1BC9E}\u{1BCA0}-\u{1BCA3}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D173}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D1E9}\u{1D1EA}\u{1D200}-\u{1D245}\u{1D300}-\u{1D356}\u{1D6DB}\u{1D715}\u{1D74F}\u{1D789}\u{1D7C3}\u{1D7CE}-\u{1D7FF}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E2FF}\u{1E4EC}-\u{1E4EF}\u{1E800}-\u{1E8C4}\u{1E8C7}-\u{1E8D6}\u{1E900}-\u{1E94B}\u{1E950}-\u{1E959}\u{1E95E}\u{1E95F}\u{1EC71}-\u{1ECB4}\u{1ED01}-\u{1ED3D}\u{1EE00}-\u{1EE03}\u{1EE05}-\u{1EE1F}\u{1EE21}\u{1EE22}\u{1EE24}\u{1EE27}\u{1EE29}-\u{1EE32}\u{1EE34}-\u{1EE37}\u{1EE39}\u{1EE3B}\u{1EE42}\u{1EE47}\u{1EE49}\u{1EE4B}\u{1EE4D}-\u{1EE4F}\u{1EE51}\u{1EE52}\u{1EE54}\u{1EE57}\u{1EE59}\u{1EE5B}\u{1EE5D}\u{1EE5F}\u{1EE61}\u{1EE62}\u{1EE64}\u{1EE67}-\u{1EE6A}\u{1EE6C}-\u{1EE72}\u{1EE74}-\u{1EE77}\u{1EE79}-\u{1EE7C}\u{1EE7E}\u{1EE80}-\u{1EE89}\u{1EE8B}-\u{1EE9B}\u{1EEA1}-\u{1EEA3}\u{1EEA5}-\u{1EEA9}\u{1EEAB}-\u{1EEBB}\u{1EEF0}\u{1EEF1}\u{1F000}-\u{1F02B}\u{1F030}-\u{1F093}\u{1F0A0}-\u{1F0AE}\u{1F0B1}-\u{1F0BF}\u{1F0C1}-\u{1F0CF}\u{1F0D1}-\u{1F0F5}\u{1F100}-\u{1F10F}\u{1F12F}\u{1F16A}-\u{1F16F}\u{1F1AD}\u{1F260}-\u{1F265}\u{1F300}-\u{1F6D7}\u{1F6DC}-\u{1F6EC}\u{1F6F0}-\u{1F6FC}\u{1F700}-\u{1F776}\u{1F77B}-\u{1F7D9}\u{1F7E0}-\u{1F7EB}\u{1F7F0}\u{1F800}-\u{1F80B}\u{1F810}-\u{1F847}\u{1F850}-\u{1F859}\u{1F860}-\u{1F887}\u{1F890}-\u{1F8AD}\u{1F8B0}\u{1F8B1}\u{1F900}-\u{1FA53}\u{1FA60}-\u{1FA6D}\u{1FA70}-\u{1FA7C}\u{1FA80}-\u{1FA88}\u{1FA90}-\u{1FABD}\u{1FABF}-\u{1FAC5}\u{1FACE}-\u{1FADB}\u{1FAE0}-\u{1FAE8}\u{1FAF0}-\u{1FAF8}\u{1FB00}-\u{1FB92}\u{1FB94}-\u{1FBCA}\u{1FBF0}-\u{1FBF9}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]*$/u; + var bidiS2 = /^[\0-\x08\x0E-\x1B!-@\[-`\{-\x84\x86-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02B9\u02BA\u02C2-\u02CF\u02D2-\u02DF\u02E5-\u02ED\u02EF-\u036F\u0374\u0375\u037E\u0384\u0385\u0387\u03F6\u0483-\u0489\u058A\u058D-\u058F\u0591-\u05C7\u05D0-\u05EA\u05EF-\u05F4\u0600-\u070D\u070F-\u074A\u074D-\u07B1\u07C0-\u07FA\u07FD-\u082D\u0830-\u083E\u0840-\u085B\u085E\u0860-\u086A\u0870-\u088E\u0890\u0891\u0898-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09F2\u09F3\u09FB\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AF1\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B55\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0BF3-\u0BFA\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C78-\u0C7E\u0C81\u0CBC\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0D81\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E3F\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39-\u0F3D\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1390-\u1399\u1400\u169B\u169C\u1712-\u1714\u1732\u1733\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DB\u17DD\u17F0-\u17F9\u1800-\u180F\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1940\u1944\u1945\u19DE-\u19FF\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DFF\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u200B-\u200D\u200F-\u2027\u202F-\u205E\u2060-\u2064\u206A-\u2070\u2074-\u207E\u2080-\u208E\u20A0-\u20C0\u20D0-\u20F0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u2150-\u215F\u2189-\u218B\u2190-\u2335\u237B-\u2394\u2396-\u2426\u2440-\u244A\u2460-\u249B\u24EA-\u26AB\u26AD-\u27FF\u2900-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2CEF-\u2CF1\u2CF9-\u2CFF\u2D7F\u2DE0-\u2E5D\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3001-\u3004\u3008-\u3020\u302A-\u302D\u3030\u3036\u3037\u303D-\u303F\u3099-\u309C\u30A0\u30FB\u31C0-\u31E3\u31EF\u321D\u321E\u3250-\u325F\u327C-\u327E\u32B1-\u32BF\u32CC-\u32CF\u3377-\u337A\u33DE\u33DF\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA60D-\uA60F\uA66F-\uA67F\uA69E\uA69F\uA6F0\uA6F1\uA700-\uA721\uA788\uA802\uA806\uA80B\uA825\uA826\uA828-\uA82C\uA838\uA839\uA874-\uA877\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uAB6A\uAB6B\uABE5\uABE8\uABED\uFB1D-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBC2\uFBD3-\uFD8F\uFD92-\uFDC7\uFDCF\uFDF0-\uFE19\uFE20-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFE70-\uFE74\uFE76-\uFEFC\uFEFF\uFF01-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD\u{10101}\u{10140}-\u{1018C}\u{10190}-\u{1019C}\u{101A0}\u{101FD}\u{102E0}-\u{102FB}\u{10376}-\u{1037A}\u{10800}-\u{10805}\u{10808}\u{1080A}-\u{10835}\u{10837}\u{10838}\u{1083C}\u{1083F}-\u{10855}\u{10857}-\u{1089E}\u{108A7}-\u{108AF}\u{108E0}-\u{108F2}\u{108F4}\u{108F5}\u{108FB}-\u{1091B}\u{1091F}-\u{10939}\u{1093F}\u{10980}-\u{109B7}\u{109BC}-\u{109CF}\u{109D2}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A13}\u{10A15}-\u{10A17}\u{10A19}-\u{10A35}\u{10A38}-\u{10A3A}\u{10A3F}-\u{10A48}\u{10A50}-\u{10A58}\u{10A60}-\u{10A9F}\u{10AC0}-\u{10AE6}\u{10AEB}-\u{10AF6}\u{10B00}-\u{10B35}\u{10B39}-\u{10B55}\u{10B58}-\u{10B72}\u{10B78}-\u{10B91}\u{10B99}-\u{10B9C}\u{10BA9}-\u{10BAF}\u{10C00}-\u{10C48}\u{10C80}-\u{10CB2}\u{10CC0}-\u{10CF2}\u{10CFA}-\u{10D27}\u{10D30}-\u{10D39}\u{10E60}-\u{10E7E}\u{10E80}-\u{10EA9}\u{10EAB}-\u{10EAD}\u{10EB0}\u{10EB1}\u{10EFD}-\u{10F27}\u{10F30}-\u{10F59}\u{10F70}-\u{10F89}\u{10FB0}-\u{10FCB}\u{10FE0}-\u{10FF6}\u{11001}\u{11038}-\u{11046}\u{11052}-\u{11065}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{11660}-\u{1166C}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{1193B}\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A06}\u{11A09}\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{11F00}\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{11FD5}-\u{11FF1}\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE2}\u{16FE4}\u{1BC9D}\u{1BC9E}\u{1BCA0}-\u{1BCA3}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D173}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D1E9}\u{1D1EA}\u{1D200}-\u{1D245}\u{1D300}-\u{1D356}\u{1D6DB}\u{1D715}\u{1D74F}\u{1D789}\u{1D7C3}\u{1D7CE}-\u{1D7FF}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E2FF}\u{1E4EC}-\u{1E4EF}\u{1E800}-\u{1E8C4}\u{1E8C7}-\u{1E8D6}\u{1E900}-\u{1E94B}\u{1E950}-\u{1E959}\u{1E95E}\u{1E95F}\u{1EC71}-\u{1ECB4}\u{1ED01}-\u{1ED3D}\u{1EE00}-\u{1EE03}\u{1EE05}-\u{1EE1F}\u{1EE21}\u{1EE22}\u{1EE24}\u{1EE27}\u{1EE29}-\u{1EE32}\u{1EE34}-\u{1EE37}\u{1EE39}\u{1EE3B}\u{1EE42}\u{1EE47}\u{1EE49}\u{1EE4B}\u{1EE4D}-\u{1EE4F}\u{1EE51}\u{1EE52}\u{1EE54}\u{1EE57}\u{1EE59}\u{1EE5B}\u{1EE5D}\u{1EE5F}\u{1EE61}\u{1EE62}\u{1EE64}\u{1EE67}-\u{1EE6A}\u{1EE6C}-\u{1EE72}\u{1EE74}-\u{1EE77}\u{1EE79}-\u{1EE7C}\u{1EE7E}\u{1EE80}-\u{1EE89}\u{1EE8B}-\u{1EE9B}\u{1EEA1}-\u{1EEA3}\u{1EEA5}-\u{1EEA9}\u{1EEAB}-\u{1EEBB}\u{1EEF0}\u{1EEF1}\u{1F000}-\u{1F02B}\u{1F030}-\u{1F093}\u{1F0A0}-\u{1F0AE}\u{1F0B1}-\u{1F0BF}\u{1F0C1}-\u{1F0CF}\u{1F0D1}-\u{1F0F5}\u{1F100}-\u{1F10F}\u{1F12F}\u{1F16A}-\u{1F16F}\u{1F1AD}\u{1F260}-\u{1F265}\u{1F300}-\u{1F6D7}\u{1F6DC}-\u{1F6EC}\u{1F6F0}-\u{1F6FC}\u{1F700}-\u{1F776}\u{1F77B}-\u{1F7D9}\u{1F7E0}-\u{1F7EB}\u{1F7F0}\u{1F800}-\u{1F80B}\u{1F810}-\u{1F847}\u{1F850}-\u{1F859}\u{1F860}-\u{1F887}\u{1F890}-\u{1F8AD}\u{1F8B0}\u{1F8B1}\u{1F900}-\u{1FA53}\u{1FA60}-\u{1FA6D}\u{1FA70}-\u{1FA7C}\u{1FA80}-\u{1FA88}\u{1FA90}-\u{1FABD}\u{1FABF}-\u{1FAC5}\u{1FACE}-\u{1FADB}\u{1FAE0}-\u{1FAE8}\u{1FAF0}-\u{1FAF8}\u{1FB00}-\u{1FB92}\u{1FB94}-\u{1FBCA}\u{1FBF0}-\u{1FBF9}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]*$/u; var bidiS3 = /[0-9\xB2\xB3\xB9\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05EF-\u05F4\u0600-\u0605\u0608\u060B\u060D\u061B-\u064A\u0660-\u0669\u066B-\u066F\u0671-\u06D5\u06DD\u06E5\u06E6\u06EE-\u070D\u070F\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u07FE-\u0815\u081A\u0824\u0828\u0830-\u083E\u0840-\u0858\u085E\u0860-\u086A\u0870-\u088E\u0890\u0891\u08A0-\u08C9\u08E2\u200F\u2070\u2074-\u2079\u2080-\u2089\u2488-\u249B\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBC2\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFC\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\u{102E1}-\u{102FB}\u{10800}-\u{10805}\u{10808}\u{1080A}-\u{10835}\u{10837}\u{10838}\u{1083C}\u{1083F}-\u{10855}\u{10857}-\u{1089E}\u{108A7}-\u{108AF}\u{108E0}-\u{108F2}\u{108F4}\u{108F5}\u{108FB}-\u{1091B}\u{10920}-\u{10939}\u{1093F}\u{10980}-\u{109B7}\u{109BC}-\u{109CF}\u{109D2}-\u{10A00}\u{10A10}-\u{10A13}\u{10A15}-\u{10A17}\u{10A19}-\u{10A35}\u{10A40}-\u{10A48}\u{10A50}-\u{10A58}\u{10A60}-\u{10A9F}\u{10AC0}-\u{10AE4}\u{10AEB}-\u{10AF6}\u{10B00}-\u{10B35}\u{10B40}-\u{10B55}\u{10B58}-\u{10B72}\u{10B78}-\u{10B91}\u{10B99}-\u{10B9C}\u{10BA9}-\u{10BAF}\u{10C00}-\u{10C48}\u{10C80}-\u{10CB2}\u{10CC0}-\u{10CF2}\u{10CFA}-\u{10D23}\u{10D30}-\u{10D39}\u{10E60}-\u{10E7E}\u{10E80}-\u{10EA9}\u{10EAD}\u{10EB0}\u{10EB1}\u{10F00}-\u{10F27}\u{10F30}-\u{10F45}\u{10F51}-\u{10F59}\u{10F70}-\u{10F81}\u{10F86}-\u{10F89}\u{10FB0}-\u{10FCB}\u{10FE0}-\u{10FF6}\u{1D7CE}-\u{1D7FF}\u{1E800}-\u{1E8C4}\u{1E8C7}-\u{1E8CF}\u{1E900}-\u{1E943}\u{1E94B}\u{1E950}-\u{1E959}\u{1E95E}\u{1E95F}\u{1EC71}-\u{1ECB4}\u{1ED01}-\u{1ED3D}\u{1EE00}-\u{1EE03}\u{1EE05}-\u{1EE1F}\u{1EE21}\u{1EE22}\u{1EE24}\u{1EE27}\u{1EE29}-\u{1EE32}\u{1EE34}-\u{1EE37}\u{1EE39}\u{1EE3B}\u{1EE42}\u{1EE47}\u{1EE49}\u{1EE4B}\u{1EE4D}-\u{1EE4F}\u{1EE51}\u{1EE52}\u{1EE54}\u{1EE57}\u{1EE59}\u{1EE5B}\u{1EE5D}\u{1EE5F}\u{1EE61}\u{1EE62}\u{1EE64}\u{1EE67}-\u{1EE6A}\u{1EE6C}-\u{1EE72}\u{1EE74}-\u{1EE77}\u{1EE79}-\u{1EE7C}\u{1EE7E}\u{1EE80}-\u{1EE89}\u{1EE8B}-\u{1EE9B}\u{1EEA1}-\u{1EEA3}\u{1EEA5}-\u{1EEA9}\u{1EEAB}-\u{1EEBB}\u{1F100}-\u{1F10A}\u{1FBF0}-\u{1FBF9}][\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B55\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0D81\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732\u1733\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u180F\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{1193B}\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A06}\u{11A09}\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{11F00}\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{1BC9D}\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{E0100}-\u{E01EF}]*$/u; var bidiS4EN = /[0-9\xB2\xB3\xB9\u06F0-\u06F9\u2070\u2074-\u2079\u2080-\u2089\u2488-\u249B\uFF10-\uFF19\u{102E1}-\u{102FB}\u{1D7CE}-\u{1D7FF}\u{1F100}-\u{1F10A}\u{1FBF0}-\u{1FBF9}]/u; var bidiS4AN = /[\u0600-\u0605\u0660-\u0669\u066B\u066C\u06DD\u0890\u0891\u08E2\u{10D30}-\u{10D39}\u{10E60}-\u{10E7E}]/u; - var bidiS5 = /^[\0-\x08\x0E-\x1B!-\x84\x86-\u0377\u037A-\u037F\u0384-\u038A\u038C\u038E-\u03A1\u03A3-\u052F\u0531-\u0556\u0559-\u058A\u058D-\u058F\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0606\u0607\u0609\u060A\u060C\u060E-\u061A\u064B-\u065F\u066A\u0670\u06D6-\u06DC\u06DE-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07F6-\u07F9\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09FE\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A76\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AF1\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B55-\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B77\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BFA\u0C00-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3C-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C5D\u0C60-\u0C63\u0C66-\u0C6F\u0C77-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDD\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1-\u0CF3\u0D00-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4F\u0D54-\u0D63\u0D66-\u0D7F\u0D81-\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4\u0E01-\u0E3A\u0E3F-\u0E5B\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECE\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00-\u0F47\u0F49-\u0F6C\u0F71-\u0F97\u0F99-\u0FBC\u0FBE-\u0FCC\u0FCE-\u0FDA\u1000-\u10C5\u10C7\u10CD\u10D0-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u137C\u1380-\u1399\u13A0-\u13F5\u13F8-\u13FD\u1400-\u167F\u1681-\u169C\u16A0-\u16F8\u1700-\u1715\u171F-\u1736\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17DD\u17E0-\u17E9\u17F0-\u17F9\u1800-\u1819\u1820-\u1878\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1940\u1944-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u19DE-\u1A1B\u1A1E-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD\u1AB0-\u1ACE\u1B00-\u1B4C\u1B50-\u1B7E\u1B80-\u1BF3\u1BFC-\u1C37\u1C3B-\u1C49\u1C4D-\u1C88\u1C90-\u1CBA\u1CBD-\u1CC7\u1CD0-\u1CFA\u1D00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FC4\u1FC6-\u1FD3\u1FD6-\u1FDB\u1FDD-\u1FEF\u1FF2-\u1FF4\u1FF6-\u1FFE\u200B-\u200E\u2010-\u2027\u202F-\u205E\u2060-\u2064\u206A-\u2071\u2074-\u208E\u2090-\u209C\u20A0-\u20C0\u20D0-\u20F0\u2100-\u218B\u2190-\u2426\u2440-\u244A\u2460-\u2B73\u2B76-\u2B95\u2B97-\u2CF3\u2CF9-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D70\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2E5D\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\u303F\u3041-\u3096\u3099-\u30FF\u3105-\u312F\u3131-\u318E\u3190-\u31E3\u31F0-\u321E\u3220-\uA48C\uA490-\uA4C6\uA4D0-\uA62B\uA640-\uA6F7\uA700-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA82C\uA830-\uA839\uA840-\uA877\uA880-\uA8C5\uA8CE-\uA8D9\uA8E0-\uA953\uA95F-\uA97C\uA980-\uA9CD\uA9CF-\uA9D9\uA9DE-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA5C-\uAAC2\uAADB-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB6B\uAB70-\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uD800-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1E\uFB29\uFD3E-\uFD4F\uFDCF\uFDFD-\uFE19\uFE20-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFEFF\uFF01-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD\u{10000}-\u{1000B}\u{1000D}-\u{10026}\u{10028}-\u{1003A}\u{1003C}\u{1003D}\u{1003F}-\u{1004D}\u{10050}-\u{1005D}\u{10080}-\u{100FA}\u{10100}-\u{10102}\u{10107}-\u{10133}\u{10137}-\u{1018E}\u{10190}-\u{1019C}\u{101A0}\u{101D0}-\u{101FD}\u{10280}-\u{1029C}\u{102A0}-\u{102D0}\u{102E0}-\u{102FB}\u{10300}-\u{10323}\u{1032D}-\u{1034A}\u{10350}-\u{1037A}\u{10380}-\u{1039D}\u{1039F}-\u{103C3}\u{103C8}-\u{103D5}\u{10400}-\u{1049D}\u{104A0}-\u{104A9}\u{104B0}-\u{104D3}\u{104D8}-\u{104FB}\u{10500}-\u{10527}\u{10530}-\u{10563}\u{1056F}-\u{1057A}\u{1057C}-\u{1058A}\u{1058C}-\u{10592}\u{10594}\u{10595}\u{10597}-\u{105A1}\u{105A3}-\u{105B1}\u{105B3}-\u{105B9}\u{105BB}\u{105BC}\u{10600}-\u{10736}\u{10740}-\u{10755}\u{10760}-\u{10767}\u{10780}-\u{10785}\u{10787}-\u{107B0}\u{107B2}-\u{107BA}\u{1091F}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10B39}-\u{10B3F}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11000}-\u{1104D}\u{11052}-\u{11075}\u{1107F}-\u{110C2}\u{110CD}\u{110D0}-\u{110E8}\u{110F0}-\u{110F9}\u{11100}-\u{11134}\u{11136}-\u{11147}\u{11150}-\u{11176}\u{11180}-\u{111DF}\u{111E1}-\u{111F4}\u{11200}-\u{11211}\u{11213}-\u{11241}\u{11280}-\u{11286}\u{11288}\u{1128A}-\u{1128D}\u{1128F}-\u{1129D}\u{1129F}-\u{112A9}\u{112B0}-\u{112EA}\u{112F0}-\u{112F9}\u{11300}-\u{11303}\u{11305}-\u{1130C}\u{1130F}\u{11310}\u{11313}-\u{11328}\u{1132A}-\u{11330}\u{11332}\u{11333}\u{11335}-\u{11339}\u{1133B}-\u{11344}\u{11347}\u{11348}\u{1134B}-\u{1134D}\u{11350}\u{11357}\u{1135D}-\u{11363}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11400}-\u{1145B}\u{1145D}-\u{11461}\u{11480}-\u{114C7}\u{114D0}-\u{114D9}\u{11580}-\u{115B5}\u{115B8}-\u{115DD}\u{11600}-\u{11644}\u{11650}-\u{11659}\u{11660}-\u{1166C}\u{11680}-\u{116B9}\u{116C0}-\u{116C9}\u{11700}-\u{1171A}\u{1171D}-\u{1172B}\u{11730}-\u{11746}\u{11800}-\u{1183B}\u{118A0}-\u{118F2}\u{118FF}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}\u{11916}\u{11918}-\u{11935}\u{11937}\u{11938}\u{1193B}-\u{11946}\u{11950}-\u{11959}\u{119A0}-\u{119A7}\u{119AA}-\u{119D7}\u{119DA}-\u{119E4}\u{11A00}-\u{11A47}\u{11A50}-\u{11AA2}\u{11AB0}-\u{11AF8}\u{11B00}-\u{11B09}\u{11C00}-\u{11C08}\u{11C0A}-\u{11C36}\u{11C38}-\u{11C45}\u{11C50}-\u{11C6C}\u{11C70}-\u{11C8F}\u{11C92}-\u{11CA7}\u{11CA9}-\u{11CB6}\u{11D00}-\u{11D06}\u{11D08}\u{11D09}\u{11D0B}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D47}\u{11D50}-\u{11D59}\u{11D60}-\u{11D65}\u{11D67}\u{11D68}\u{11D6A}-\u{11D8E}\u{11D90}\u{11D91}\u{11D93}-\u{11D98}\u{11DA0}-\u{11DA9}\u{11EE0}-\u{11EF8}\u{11F00}-\u{11F10}\u{11F12}-\u{11F3A}\u{11F3E}-\u{11F59}\u{11FB0}\u{11FC0}-\u{11FF1}\u{11FFF}-\u{12399}\u{12400}-\u{1246E}\u{12470}-\u{12474}\u{12480}-\u{12543}\u{12F90}-\u{12FF2}\u{13000}-\u{13455}\u{14400}-\u{14646}\u{16800}-\u{16A38}\u{16A40}-\u{16A5E}\u{16A60}-\u{16A69}\u{16A6E}-\u{16ABE}\u{16AC0}-\u{16AC9}\u{16AD0}-\u{16AED}\u{16AF0}-\u{16AF5}\u{16B00}-\u{16B45}\u{16B50}-\u{16B59}\u{16B5B}-\u{16B61}\u{16B63}-\u{16B77}\u{16B7D}-\u{16B8F}\u{16E40}-\u{16E9A}\u{16F00}-\u{16F4A}\u{16F4F}-\u{16F87}\u{16F8F}-\u{16F9F}\u{16FE0}-\u{16FE4}\u{16FF0}\u{16FF1}\u{17000}-\u{187F7}\u{18800}-\u{18CD5}\u{18D00}-\u{18D08}\u{1AFF0}-\u{1AFF3}\u{1AFF5}-\u{1AFFB}\u{1AFFD}\u{1AFFE}\u{1B000}-\u{1B122}\u{1B132}\u{1B150}-\u{1B152}\u{1B155}\u{1B164}-\u{1B167}\u{1B170}-\u{1B2FB}\u{1BC00}-\u{1BC6A}\u{1BC70}-\u{1BC7C}\u{1BC80}-\u{1BC88}\u{1BC90}-\u{1BC99}\u{1BC9C}-\u{1BCA3}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1CF50}-\u{1CFC3}\u{1D000}-\u{1D0F5}\u{1D100}-\u{1D126}\u{1D129}-\u{1D1EA}\u{1D200}-\u{1D245}\u{1D2C0}-\u{1D2D3}\u{1D2E0}-\u{1D2F3}\u{1D300}-\u{1D356}\u{1D360}-\u{1D378}\u{1D400}-\u{1D454}\u{1D456}-\u{1D49C}\u{1D49E}\u{1D49F}\u{1D4A2}\u{1D4A5}\u{1D4A6}\u{1D4A9}-\u{1D4AC}\u{1D4AE}-\u{1D4B9}\u{1D4BB}\u{1D4BD}-\u{1D4C3}\u{1D4C5}-\u{1D505}\u{1D507}-\u{1D50A}\u{1D50D}-\u{1D514}\u{1D516}-\u{1D51C}\u{1D51E}-\u{1D539}\u{1D53B}-\u{1D53E}\u{1D540}-\u{1D544}\u{1D546}\u{1D54A}-\u{1D550}\u{1D552}-\u{1D6A5}\u{1D6A8}-\u{1D7CB}\u{1D7CE}-\u{1DA8B}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1DF00}-\u{1DF1E}\u{1DF25}-\u{1DF2A}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E030}-\u{1E06D}\u{1E08F}\u{1E100}-\u{1E12C}\u{1E130}-\u{1E13D}\u{1E140}-\u{1E149}\u{1E14E}\u{1E14F}\u{1E290}-\u{1E2AE}\u{1E2C0}-\u{1E2F9}\u{1E2FF}\u{1E4D0}-\u{1E4F9}\u{1E7E0}-\u{1E7E6}\u{1E7E8}-\u{1E7EB}\u{1E7ED}\u{1E7EE}\u{1E7F0}-\u{1E7FE}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{1EEF0}\u{1EEF1}\u{1F000}-\u{1F02B}\u{1F030}-\u{1F093}\u{1F0A0}-\u{1F0AE}\u{1F0B1}-\u{1F0BF}\u{1F0C1}-\u{1F0CF}\u{1F0D1}-\u{1F0F5}\u{1F100}-\u{1F1AD}\u{1F1E6}-\u{1F202}\u{1F210}-\u{1F23B}\u{1F240}-\u{1F248}\u{1F250}\u{1F251}\u{1F260}-\u{1F265}\u{1F300}-\u{1F6D7}\u{1F6DC}-\u{1F6EC}\u{1F6F0}-\u{1F6FC}\u{1F700}-\u{1F776}\u{1F77B}-\u{1F7D9}\u{1F7E0}-\u{1F7EB}\u{1F7F0}\u{1F800}-\u{1F80B}\u{1F810}-\u{1F847}\u{1F850}-\u{1F859}\u{1F860}-\u{1F887}\u{1F890}-\u{1F8AD}\u{1F8B0}\u{1F8B1}\u{1F900}-\u{1FA53}\u{1FA60}-\u{1FA6D}\u{1FA70}-\u{1FA7C}\u{1FA80}-\u{1FA88}\u{1FA90}-\u{1FABD}\u{1FABF}-\u{1FAC5}\u{1FACE}-\u{1FADB}\u{1FAE0}-\u{1FAE8}\u{1FAF0}-\u{1FAF8}\u{1FB00}-\u{1FB92}\u{1FB94}-\u{1FBCA}\u{1FBF0}-\u{1FBF9}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B739}\u{2B740}-\u{2B81D}\u{2B820}-\u{2CEA1}\u{2CEB0}-\u{2EBE0}\u{2F800}-\u{2FA1D}\u{30000}-\u{3134A}\u{31350}-\u{323AF}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]*$/u; - var bidiS6 = /[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02BB-\u02C1\u02D0\u02D1\u02E0-\u02E4\u02EE\u0370-\u0373\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0482\u048A-\u052F\u0531-\u0556\u0559-\u0589\u06F0-\u06F9\u0903-\u0939\u093B\u093D-\u0940\u0949-\u094C\u094E-\u0950\u0958-\u0961\u0964-\u0980\u0982\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD-\u09C0\u09C7\u09C8\u09CB\u09CC\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09FA\u09FC\u09FD\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3E-\u0A40\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A76\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD-\u0AC0\u0AC9\u0ACB\u0ACC\u0AD0\u0AE0\u0AE1\u0AE6-\u0AF0\u0AF9\u0B02\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B3E\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0B57\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0BD0\u0BD7\u0BE6-\u0BF2\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C41-\u0C44\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C77\u0C7F\u0C80\u0C82-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD-\u0CC4\u0CC6-\u0CC8\u0CCA\u0CCB\u0CD5\u0CD6\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1-\u0CF3\u0D02-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D4E\u0D4F\u0D54-\u0D61\u0D66-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCF-\u0DD1\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E4F-\u0E5B\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00-\u0F17\u0F1A-\u0F34\u0F36\u0F38\u0F3E-\u0F47\u0F49-\u0F6C\u0F7F\u0F85\u0F88-\u0F8C\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE-\u0FDA\u1000-\u102C\u1031\u1038\u103B\u103C\u103F-\u1057\u105A-\u105D\u1061-\u1070\u1075-\u1081\u1083\u1084\u1087-\u108C\u108E-\u109C\u109E-\u10C5\u10C7\u10CD\u10D0-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1360-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u167F\u1681-\u169A\u16A0-\u16F8\u1700-\u1711\u1715\u171F-\u1731\u1734-\u1736\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17B6\u17BE-\u17C5\u17C7\u17C8\u17D4-\u17DA\u17DC\u17E0-\u17E9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A19\u1A1A\u1A1E-\u1A55\u1A57\u1A61\u1A63\u1A64\u1A6D-\u1A72\u1A80-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD\u1B04-\u1B33\u1B35\u1B3B\u1B3D-\u1B41\u1B43-\u1B4C\u1B50-\u1B6A\u1B74-\u1B7E\u1B82-\u1BA1\u1BA6\u1BA7\u1BAA\u1BAE-\u1BE5\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1BFC-\u1C2B\u1C34\u1C35\u1C3B-\u1C49\u1C4D-\u1C88\u1C90-\u1CBA\u1CBD-\u1CC7\u1CD3\u1CE1\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5-\u1CF7\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200E\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u214F\u2160-\u2188\u2336-\u237A\u2395\u2488-\u24E9\u26AC\u2800-\u28FF\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D70\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u302E\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3190-\u31BF\u31F0-\u321C\u3220-\u324F\u3260-\u327B\u327F-\u32B0\u32C0-\u32CB\u32D0-\u3376\u337B-\u33DD\u33E0-\u33FE\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA60C\uA610-\uA62B\uA640-\uA66E\uA680-\uA69D\uA6A0-\uA6EF\uA6F2-\uA6F7\uA722-\uA787\uA789-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA824\uA827\uA830-\uA837\uA840-\uA873\uA880-\uA8C3\uA8CE-\uA8D9\uA8F2-\uA8FE\uA900-\uA925\uA92E-\uA946\uA952\uA953\uA95F-\uA97C\uA983-\uA9B2\uA9B4\uA9B5\uA9BA\uA9BB\uA9BE-\uA9CD\uA9CF-\uA9D9\uA9DE-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA2F\uAA30\uAA33\uAA34\uAA40-\uAA42\uAA44-\uAA4B\uAA4D\uAA50-\uAA59\uAA5C-\uAA7B\uAA7D-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAAEB\uAAEE-\uAAF5\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB69\uAB70-\uABE4\uABE6\uABE7\uABE9-\uABEC\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uD800-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\u{10000}-\u{1000B}\u{1000D}-\u{10026}\u{10028}-\u{1003A}\u{1003C}\u{1003D}\u{1003F}-\u{1004D}\u{10050}-\u{1005D}\u{10080}-\u{100FA}\u{10100}\u{10102}\u{10107}-\u{10133}\u{10137}-\u{1013F}\u{1018D}\u{1018E}\u{101D0}-\u{101FC}\u{10280}-\u{1029C}\u{102A0}-\u{102D0}\u{102E1}-\u{102FB}\u{10300}-\u{10323}\u{1032D}-\u{1034A}\u{10350}-\u{10375}\u{10380}-\u{1039D}\u{1039F}-\u{103C3}\u{103C8}-\u{103D5}\u{10400}-\u{1049D}\u{104A0}-\u{104A9}\u{104B0}-\u{104D3}\u{104D8}-\u{104FB}\u{10500}-\u{10527}\u{10530}-\u{10563}\u{1056F}-\u{1057A}\u{1057C}-\u{1058A}\u{1058C}-\u{10592}\u{10594}\u{10595}\u{10597}-\u{105A1}\u{105A3}-\u{105B1}\u{105B3}-\u{105B9}\u{105BB}\u{105BC}\u{10600}-\u{10736}\u{10740}-\u{10755}\u{10760}-\u{10767}\u{10780}-\u{10785}\u{10787}-\u{107B0}\u{107B2}-\u{107BA}\u{11000}\u{11002}-\u{11037}\u{11047}-\u{1104D}\u{11066}-\u{1106F}\u{11071}\u{11072}\u{11075}\u{11082}-\u{110B2}\u{110B7}\u{110B8}\u{110BB}-\u{110C1}\u{110CD}\u{110D0}-\u{110E8}\u{110F0}-\u{110F9}\u{11103}-\u{11126}\u{1112C}\u{11136}-\u{11147}\u{11150}-\u{11172}\u{11174}-\u{11176}\u{11182}-\u{111B5}\u{111BF}-\u{111C8}\u{111CD}\u{111CE}\u{111D0}-\u{111DF}\u{111E1}-\u{111F4}\u{11200}-\u{11211}\u{11213}-\u{1122E}\u{11232}\u{11233}\u{11235}\u{11238}-\u{1123D}\u{1123F}\u{11240}\u{11280}-\u{11286}\u{11288}\u{1128A}-\u{1128D}\u{1128F}-\u{1129D}\u{1129F}-\u{112A9}\u{112B0}-\u{112DE}\u{112E0}-\u{112E2}\u{112F0}-\u{112F9}\u{11302}\u{11303}\u{11305}-\u{1130C}\u{1130F}\u{11310}\u{11313}-\u{11328}\u{1132A}-\u{11330}\u{11332}\u{11333}\u{11335}-\u{11339}\u{1133D}-\u{1133F}\u{11341}-\u{11344}\u{11347}\u{11348}\u{1134B}-\u{1134D}\u{11350}\u{11357}\u{1135D}-\u{11363}\u{11400}-\u{11437}\u{11440}\u{11441}\u{11445}\u{11447}-\u{1145B}\u{1145D}\u{1145F}-\u{11461}\u{11480}-\u{114B2}\u{114B9}\u{114BB}-\u{114BE}\u{114C1}\u{114C4}-\u{114C7}\u{114D0}-\u{114D9}\u{11580}-\u{115B1}\u{115B8}-\u{115BB}\u{115BE}\u{115C1}-\u{115DB}\u{11600}-\u{11632}\u{1163B}\u{1163C}\u{1163E}\u{11641}-\u{11644}\u{11650}-\u{11659}\u{11680}-\u{116AA}\u{116AC}\u{116AE}\u{116AF}\u{116B6}\u{116B8}\u{116B9}\u{116C0}-\u{116C9}\u{11700}-\u{1171A}\u{11720}\u{11721}\u{11726}\u{11730}-\u{11746}\u{11800}-\u{1182E}\u{11838}\u{1183B}\u{118A0}-\u{118F2}\u{118FF}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}\u{11916}\u{11918}-\u{11935}\u{11937}\u{11938}\u{1193D}\u{1193F}-\u{11942}\u{11944}-\u{11946}\u{11950}-\u{11959}\u{119A0}-\u{119A7}\u{119AA}-\u{119D3}\u{119DC}-\u{119DF}\u{119E1}-\u{119E4}\u{11A00}\u{11A07}\u{11A08}\u{11A0B}-\u{11A32}\u{11A39}\u{11A3A}\u{11A3F}-\u{11A46}\u{11A50}\u{11A57}\u{11A58}\u{11A5C}-\u{11A89}\u{11A97}\u{11A9A}-\u{11AA2}\u{11AB0}-\u{11AF8}\u{11B00}-\u{11B09}\u{11C00}-\u{11C08}\u{11C0A}-\u{11C2F}\u{11C3E}-\u{11C45}\u{11C50}-\u{11C6C}\u{11C70}-\u{11C8F}\u{11CA9}\u{11CB1}\u{11CB4}\u{11D00}-\u{11D06}\u{11D08}\u{11D09}\u{11D0B}-\u{11D30}\u{11D46}\u{11D50}-\u{11D59}\u{11D60}-\u{11D65}\u{11D67}\u{11D68}\u{11D6A}-\u{11D8E}\u{11D93}\u{11D94}\u{11D96}\u{11D98}\u{11DA0}-\u{11DA9}\u{11EE0}-\u{11EF2}\u{11EF5}-\u{11EF8}\u{11F02}-\u{11F10}\u{11F12}-\u{11F35}\u{11F3E}\u{11F3F}\u{11F41}\u{11F43}-\u{11F59}\u{11FB0}\u{11FC0}-\u{11FD4}\u{11FFF}-\u{12399}\u{12400}-\u{1246E}\u{12470}-\u{12474}\u{12480}-\u{12543}\u{12F90}-\u{12FF2}\u{13000}-\u{1343F}\u{13441}-\u{13446}\u{14400}-\u{14646}\u{16800}-\u{16A38}\u{16A40}-\u{16A5E}\u{16A60}-\u{16A69}\u{16A6E}-\u{16ABE}\u{16AC0}-\u{16AC9}\u{16AD0}-\u{16AED}\u{16AF5}\u{16B00}-\u{16B2F}\u{16B37}-\u{16B45}\u{16B50}-\u{16B59}\u{16B5B}-\u{16B61}\u{16B63}-\u{16B77}\u{16B7D}-\u{16B8F}\u{16E40}-\u{16E9A}\u{16F00}-\u{16F4A}\u{16F50}-\u{16F87}\u{16F93}-\u{16F9F}\u{16FE0}\u{16FE1}\u{16FE3}\u{16FF0}\u{16FF1}\u{17000}-\u{187F7}\u{18800}-\u{18CD5}\u{18D00}-\u{18D08}\u{1AFF0}-\u{1AFF3}\u{1AFF5}-\u{1AFFB}\u{1AFFD}\u{1AFFE}\u{1B000}-\u{1B122}\u{1B132}\u{1B150}-\u{1B152}\u{1B155}\u{1B164}-\u{1B167}\u{1B170}-\u{1B2FB}\u{1BC00}-\u{1BC6A}\u{1BC70}-\u{1BC7C}\u{1BC80}-\u{1BC88}\u{1BC90}-\u{1BC99}\u{1BC9C}\u{1BC9F}\u{1CF50}-\u{1CFC3}\u{1D000}-\u{1D0F5}\u{1D100}-\u{1D126}\u{1D129}-\u{1D166}\u{1D16A}-\u{1D172}\u{1D183}\u{1D184}\u{1D18C}-\u{1D1A9}\u{1D1AE}-\u{1D1E8}\u{1D2C0}-\u{1D2D3}\u{1D2E0}-\u{1D2F3}\u{1D360}-\u{1D378}\u{1D400}-\u{1D454}\u{1D456}-\u{1D49C}\u{1D49E}\u{1D49F}\u{1D4A2}\u{1D4A5}\u{1D4A6}\u{1D4A9}-\u{1D4AC}\u{1D4AE}-\u{1D4B9}\u{1D4BB}\u{1D4BD}-\u{1D4C3}\u{1D4C5}-\u{1D505}\u{1D507}-\u{1D50A}\u{1D50D}-\u{1D514}\u{1D516}-\u{1D51C}\u{1D51E}-\u{1D539}\u{1D53B}-\u{1D53E}\u{1D540}-\u{1D544}\u{1D546}\u{1D54A}-\u{1D550}\u{1D552}-\u{1D6A5}\u{1D6A8}-\u{1D6DA}\u{1D6DC}-\u{1D714}\u{1D716}-\u{1D74E}\u{1D750}-\u{1D788}\u{1D78A}-\u{1D7C2}\u{1D7C4}-\u{1D7CB}\u{1D7CE}-\u{1D9FF}\u{1DA37}-\u{1DA3A}\u{1DA6D}-\u{1DA74}\u{1DA76}-\u{1DA83}\u{1DA85}-\u{1DA8B}\u{1DF00}-\u{1DF1E}\u{1DF25}-\u{1DF2A}\u{1E030}-\u{1E06D}\u{1E100}-\u{1E12C}\u{1E137}-\u{1E13D}\u{1E140}-\u{1E149}\u{1E14E}\u{1E14F}\u{1E290}-\u{1E2AD}\u{1E2C0}-\u{1E2EB}\u{1E2F0}-\u{1E2F9}\u{1E4D0}-\u{1E4EB}\u{1E4F0}-\u{1E4F9}\u{1E7E0}-\u{1E7E6}\u{1E7E8}-\u{1E7EB}\u{1E7ED}\u{1E7EE}\u{1E7F0}-\u{1E7FE}\u{1F100}-\u{1F10A}\u{1F110}-\u{1F12E}\u{1F130}-\u{1F169}\u{1F170}-\u{1F1AC}\u{1F1E6}-\u{1F202}\u{1F210}-\u{1F23B}\u{1F240}-\u{1F248}\u{1F250}\u{1F251}\u{1FBF0}-\u{1FBF9}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B739}\u{2B740}-\u{2B81D}\u{2B820}-\u{2CEA1}\u{2CEB0}-\u{2EBE0}\u{2F800}-\u{2FA1D}\u{30000}-\u{3134A}\u{31350}-\u{323AF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}][\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B55\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0D81\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732\u1733\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u180F\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{1193B}\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A06}\u{11A09}\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{11F00}\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{1BC9D}\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{E0100}-\u{E01EF}]*$/u; + var bidiS5 = /^[\0-\x08\x0E-\x1B!-\x84\x86-\u0377\u037A-\u037F\u0384-\u038A\u038C\u038E-\u03A1\u03A3-\u052F\u0531-\u0556\u0559-\u058A\u058D-\u058F\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0606\u0607\u0609\u060A\u060C\u060E-\u061A\u064B-\u065F\u066A\u0670\u06D6-\u06DC\u06DE-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07F6-\u07F9\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09FE\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A76\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AF1\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B55-\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B77\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BFA\u0C00-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3C-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C5D\u0C60-\u0C63\u0C66-\u0C6F\u0C77-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDD\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1-\u0CF3\u0D00-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4F\u0D54-\u0D63\u0D66-\u0D7F\u0D81-\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4\u0E01-\u0E3A\u0E3F-\u0E5B\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECE\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00-\u0F47\u0F49-\u0F6C\u0F71-\u0F97\u0F99-\u0FBC\u0FBE-\u0FCC\u0FCE-\u0FDA\u1000-\u10C5\u10C7\u10CD\u10D0-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u137C\u1380-\u1399\u13A0-\u13F5\u13F8-\u13FD\u1400-\u167F\u1681-\u169C\u16A0-\u16F8\u1700-\u1715\u171F-\u1736\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17DD\u17E0-\u17E9\u17F0-\u17F9\u1800-\u1819\u1820-\u1878\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1940\u1944-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u19DE-\u1A1B\u1A1E-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD\u1AB0-\u1ACE\u1B00-\u1B4C\u1B50-\u1B7E\u1B80-\u1BF3\u1BFC-\u1C37\u1C3B-\u1C49\u1C4D-\u1C88\u1C90-\u1CBA\u1CBD-\u1CC7\u1CD0-\u1CFA\u1D00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FC4\u1FC6-\u1FD3\u1FD6-\u1FDB\u1FDD-\u1FEF\u1FF2-\u1FF4\u1FF6-\u1FFE\u200B-\u200E\u2010-\u2027\u202F-\u205E\u2060-\u2064\u206A-\u2071\u2074-\u208E\u2090-\u209C\u20A0-\u20C0\u20D0-\u20F0\u2100-\u218B\u2190-\u2426\u2440-\u244A\u2460-\u2B73\u2B76-\u2B95\u2B97-\u2CF3\u2CF9-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D70\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2E5D\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3001-\u303F\u3041-\u3096\u3099-\u30FF\u3105-\u312F\u3131-\u318E\u3190-\u31E3\u31EF-\u321E\u3220-\uA48C\uA490-\uA4C6\uA4D0-\uA62B\uA640-\uA6F7\uA700-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA82C\uA830-\uA839\uA840-\uA877\uA880-\uA8C5\uA8CE-\uA8D9\uA8E0-\uA953\uA95F-\uA97C\uA980-\uA9CD\uA9CF-\uA9D9\uA9DE-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA5C-\uAAC2\uAADB-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB6B\uAB70-\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uD800-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1E\uFB29\uFD3E-\uFD4F\uFDCF\uFDFD-\uFE19\uFE20-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFEFF\uFF01-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD\u{10000}-\u{1000B}\u{1000D}-\u{10026}\u{10028}-\u{1003A}\u{1003C}\u{1003D}\u{1003F}-\u{1004D}\u{10050}-\u{1005D}\u{10080}-\u{100FA}\u{10100}-\u{10102}\u{10107}-\u{10133}\u{10137}-\u{1018E}\u{10190}-\u{1019C}\u{101A0}\u{101D0}-\u{101FD}\u{10280}-\u{1029C}\u{102A0}-\u{102D0}\u{102E0}-\u{102FB}\u{10300}-\u{10323}\u{1032D}-\u{1034A}\u{10350}-\u{1037A}\u{10380}-\u{1039D}\u{1039F}-\u{103C3}\u{103C8}-\u{103D5}\u{10400}-\u{1049D}\u{104A0}-\u{104A9}\u{104B0}-\u{104D3}\u{104D8}-\u{104FB}\u{10500}-\u{10527}\u{10530}-\u{10563}\u{1056F}-\u{1057A}\u{1057C}-\u{1058A}\u{1058C}-\u{10592}\u{10594}\u{10595}\u{10597}-\u{105A1}\u{105A3}-\u{105B1}\u{105B3}-\u{105B9}\u{105BB}\u{105BC}\u{10600}-\u{10736}\u{10740}-\u{10755}\u{10760}-\u{10767}\u{10780}-\u{10785}\u{10787}-\u{107B0}\u{107B2}-\u{107BA}\u{1091F}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10B39}-\u{10B3F}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11000}-\u{1104D}\u{11052}-\u{11075}\u{1107F}-\u{110C2}\u{110CD}\u{110D0}-\u{110E8}\u{110F0}-\u{110F9}\u{11100}-\u{11134}\u{11136}-\u{11147}\u{11150}-\u{11176}\u{11180}-\u{111DF}\u{111E1}-\u{111F4}\u{11200}-\u{11211}\u{11213}-\u{11241}\u{11280}-\u{11286}\u{11288}\u{1128A}-\u{1128D}\u{1128F}-\u{1129D}\u{1129F}-\u{112A9}\u{112B0}-\u{112EA}\u{112F0}-\u{112F9}\u{11300}-\u{11303}\u{11305}-\u{1130C}\u{1130F}\u{11310}\u{11313}-\u{11328}\u{1132A}-\u{11330}\u{11332}\u{11333}\u{11335}-\u{11339}\u{1133B}-\u{11344}\u{11347}\u{11348}\u{1134B}-\u{1134D}\u{11350}\u{11357}\u{1135D}-\u{11363}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11400}-\u{1145B}\u{1145D}-\u{11461}\u{11480}-\u{114C7}\u{114D0}-\u{114D9}\u{11580}-\u{115B5}\u{115B8}-\u{115DD}\u{11600}-\u{11644}\u{11650}-\u{11659}\u{11660}-\u{1166C}\u{11680}-\u{116B9}\u{116C0}-\u{116C9}\u{11700}-\u{1171A}\u{1171D}-\u{1172B}\u{11730}-\u{11746}\u{11800}-\u{1183B}\u{118A0}-\u{118F2}\u{118FF}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}\u{11916}\u{11918}-\u{11935}\u{11937}\u{11938}\u{1193B}-\u{11946}\u{11950}-\u{11959}\u{119A0}-\u{119A7}\u{119AA}-\u{119D7}\u{119DA}-\u{119E4}\u{11A00}-\u{11A47}\u{11A50}-\u{11AA2}\u{11AB0}-\u{11AF8}\u{11B00}-\u{11B09}\u{11C00}-\u{11C08}\u{11C0A}-\u{11C36}\u{11C38}-\u{11C45}\u{11C50}-\u{11C6C}\u{11C70}-\u{11C8F}\u{11C92}-\u{11CA7}\u{11CA9}-\u{11CB6}\u{11D00}-\u{11D06}\u{11D08}\u{11D09}\u{11D0B}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D47}\u{11D50}-\u{11D59}\u{11D60}-\u{11D65}\u{11D67}\u{11D68}\u{11D6A}-\u{11D8E}\u{11D90}\u{11D91}\u{11D93}-\u{11D98}\u{11DA0}-\u{11DA9}\u{11EE0}-\u{11EF8}\u{11F00}-\u{11F10}\u{11F12}-\u{11F3A}\u{11F3E}-\u{11F59}\u{11FB0}\u{11FC0}-\u{11FF1}\u{11FFF}-\u{12399}\u{12400}-\u{1246E}\u{12470}-\u{12474}\u{12480}-\u{12543}\u{12F90}-\u{12FF2}\u{13000}-\u{13455}\u{14400}-\u{14646}\u{16800}-\u{16A38}\u{16A40}-\u{16A5E}\u{16A60}-\u{16A69}\u{16A6E}-\u{16ABE}\u{16AC0}-\u{16AC9}\u{16AD0}-\u{16AED}\u{16AF0}-\u{16AF5}\u{16B00}-\u{16B45}\u{16B50}-\u{16B59}\u{16B5B}-\u{16B61}\u{16B63}-\u{16B77}\u{16B7D}-\u{16B8F}\u{16E40}-\u{16E9A}\u{16F00}-\u{16F4A}\u{16F4F}-\u{16F87}\u{16F8F}-\u{16F9F}\u{16FE0}-\u{16FE4}\u{16FF0}\u{16FF1}\u{17000}-\u{187F7}\u{18800}-\u{18CD5}\u{18D00}-\u{18D08}\u{1AFF0}-\u{1AFF3}\u{1AFF5}-\u{1AFFB}\u{1AFFD}\u{1AFFE}\u{1B000}-\u{1B122}\u{1B132}\u{1B150}-\u{1B152}\u{1B155}\u{1B164}-\u{1B167}\u{1B170}-\u{1B2FB}\u{1BC00}-\u{1BC6A}\u{1BC70}-\u{1BC7C}\u{1BC80}-\u{1BC88}\u{1BC90}-\u{1BC99}\u{1BC9C}-\u{1BCA3}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1CF50}-\u{1CFC3}\u{1D000}-\u{1D0F5}\u{1D100}-\u{1D126}\u{1D129}-\u{1D1EA}\u{1D200}-\u{1D245}\u{1D2C0}-\u{1D2D3}\u{1D2E0}-\u{1D2F3}\u{1D300}-\u{1D356}\u{1D360}-\u{1D378}\u{1D400}-\u{1D454}\u{1D456}-\u{1D49C}\u{1D49E}\u{1D49F}\u{1D4A2}\u{1D4A5}\u{1D4A6}\u{1D4A9}-\u{1D4AC}\u{1D4AE}-\u{1D4B9}\u{1D4BB}\u{1D4BD}-\u{1D4C3}\u{1D4C5}-\u{1D505}\u{1D507}-\u{1D50A}\u{1D50D}-\u{1D514}\u{1D516}-\u{1D51C}\u{1D51E}-\u{1D539}\u{1D53B}-\u{1D53E}\u{1D540}-\u{1D544}\u{1D546}\u{1D54A}-\u{1D550}\u{1D552}-\u{1D6A5}\u{1D6A8}-\u{1D7CB}\u{1D7CE}-\u{1DA8B}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1DF00}-\u{1DF1E}\u{1DF25}-\u{1DF2A}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E030}-\u{1E06D}\u{1E08F}\u{1E100}-\u{1E12C}\u{1E130}-\u{1E13D}\u{1E140}-\u{1E149}\u{1E14E}\u{1E14F}\u{1E290}-\u{1E2AE}\u{1E2C0}-\u{1E2F9}\u{1E2FF}\u{1E4D0}-\u{1E4F9}\u{1E7E0}-\u{1E7E6}\u{1E7E8}-\u{1E7EB}\u{1E7ED}\u{1E7EE}\u{1E7F0}-\u{1E7FE}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{1EEF0}\u{1EEF1}\u{1F000}-\u{1F02B}\u{1F030}-\u{1F093}\u{1F0A0}-\u{1F0AE}\u{1F0B1}-\u{1F0BF}\u{1F0C1}-\u{1F0CF}\u{1F0D1}-\u{1F0F5}\u{1F100}-\u{1F1AD}\u{1F1E6}-\u{1F202}\u{1F210}-\u{1F23B}\u{1F240}-\u{1F248}\u{1F250}\u{1F251}\u{1F260}-\u{1F265}\u{1F300}-\u{1F6D7}\u{1F6DC}-\u{1F6EC}\u{1F6F0}-\u{1F6FC}\u{1F700}-\u{1F776}\u{1F77B}-\u{1F7D9}\u{1F7E0}-\u{1F7EB}\u{1F7F0}\u{1F800}-\u{1F80B}\u{1F810}-\u{1F847}\u{1F850}-\u{1F859}\u{1F860}-\u{1F887}\u{1F890}-\u{1F8AD}\u{1F8B0}\u{1F8B1}\u{1F900}-\u{1FA53}\u{1FA60}-\u{1FA6D}\u{1FA70}-\u{1FA7C}\u{1FA80}-\u{1FA88}\u{1FA90}-\u{1FABD}\u{1FABF}-\u{1FAC5}\u{1FACE}-\u{1FADB}\u{1FAE0}-\u{1FAE8}\u{1FAF0}-\u{1FAF8}\u{1FB00}-\u{1FB92}\u{1FB94}-\u{1FBCA}\u{1FBF0}-\u{1FBF9}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B739}\u{2B740}-\u{2B81D}\u{2B820}-\u{2CEA1}\u{2CEB0}-\u{2EBE0}\u{2EBF0}-\u{2EE5D}\u{2F800}-\u{2FA1D}\u{30000}-\u{3134A}\u{31350}-\u{323AF}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]*$/u; + var bidiS6 = /[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02BB-\u02C1\u02D0\u02D1\u02E0-\u02E4\u02EE\u0370-\u0373\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0482\u048A-\u052F\u0531-\u0556\u0559-\u0589\u06F0-\u06F9\u0903-\u0939\u093B\u093D-\u0940\u0949-\u094C\u094E-\u0950\u0958-\u0961\u0964-\u0980\u0982\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD-\u09C0\u09C7\u09C8\u09CB\u09CC\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09FA\u09FC\u09FD\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3E-\u0A40\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A76\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD-\u0AC0\u0AC9\u0ACB\u0ACC\u0AD0\u0AE0\u0AE1\u0AE6-\u0AF0\u0AF9\u0B02\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B3E\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0B57\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0BD0\u0BD7\u0BE6-\u0BF2\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C41-\u0C44\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C77\u0C7F\u0C80\u0C82-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD-\u0CC4\u0CC6-\u0CC8\u0CCA\u0CCB\u0CD5\u0CD6\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1-\u0CF3\u0D02-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D4E\u0D4F\u0D54-\u0D61\u0D66-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCF-\u0DD1\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E4F-\u0E5B\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00-\u0F17\u0F1A-\u0F34\u0F36\u0F38\u0F3E-\u0F47\u0F49-\u0F6C\u0F7F\u0F85\u0F88-\u0F8C\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE-\u0FDA\u1000-\u102C\u1031\u1038\u103B\u103C\u103F-\u1057\u105A-\u105D\u1061-\u1070\u1075-\u1081\u1083\u1084\u1087-\u108C\u108E-\u109C\u109E-\u10C5\u10C7\u10CD\u10D0-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1360-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u167F\u1681-\u169A\u16A0-\u16F8\u1700-\u1711\u1715\u171F-\u1731\u1734-\u1736\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17B6\u17BE-\u17C5\u17C7\u17C8\u17D4-\u17DA\u17DC\u17E0-\u17E9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A19\u1A1A\u1A1E-\u1A55\u1A57\u1A61\u1A63\u1A64\u1A6D-\u1A72\u1A80-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD\u1B04-\u1B33\u1B35\u1B3B\u1B3D-\u1B41\u1B43-\u1B4C\u1B50-\u1B6A\u1B74-\u1B7E\u1B82-\u1BA1\u1BA6\u1BA7\u1BAA\u1BAE-\u1BE5\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1BFC-\u1C2B\u1C34\u1C35\u1C3B-\u1C49\u1C4D-\u1C88\u1C90-\u1CBA\u1CBD-\u1CC7\u1CD3\u1CE1\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5-\u1CF7\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200E\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u214F\u2160-\u2188\u2336-\u237A\u2395\u2488-\u24E9\u26AC\u2800-\u28FF\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D70\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u302E\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3190-\u31BF\u31F0-\u321C\u3220-\u324F\u3260-\u327B\u327F-\u32B0\u32C0-\u32CB\u32D0-\u3376\u337B-\u33DD\u33E0-\u33FE\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA60C\uA610-\uA62B\uA640-\uA66E\uA680-\uA69D\uA6A0-\uA6EF\uA6F2-\uA6F7\uA722-\uA787\uA789-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA824\uA827\uA830-\uA837\uA840-\uA873\uA880-\uA8C3\uA8CE-\uA8D9\uA8F2-\uA8FE\uA900-\uA925\uA92E-\uA946\uA952\uA953\uA95F-\uA97C\uA983-\uA9B2\uA9B4\uA9B5\uA9BA\uA9BB\uA9BE-\uA9CD\uA9CF-\uA9D9\uA9DE-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA2F\uAA30\uAA33\uAA34\uAA40-\uAA42\uAA44-\uAA4B\uAA4D\uAA50-\uAA59\uAA5C-\uAA7B\uAA7D-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAAEB\uAAEE-\uAAF5\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB69\uAB70-\uABE4\uABE6\uABE7\uABE9-\uABEC\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uD800-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\u{10000}-\u{1000B}\u{1000D}-\u{10026}\u{10028}-\u{1003A}\u{1003C}\u{1003D}\u{1003F}-\u{1004D}\u{10050}-\u{1005D}\u{10080}-\u{100FA}\u{10100}\u{10102}\u{10107}-\u{10133}\u{10137}-\u{1013F}\u{1018D}\u{1018E}\u{101D0}-\u{101FC}\u{10280}-\u{1029C}\u{102A0}-\u{102D0}\u{102E1}-\u{102FB}\u{10300}-\u{10323}\u{1032D}-\u{1034A}\u{10350}-\u{10375}\u{10380}-\u{1039D}\u{1039F}-\u{103C3}\u{103C8}-\u{103D5}\u{10400}-\u{1049D}\u{104A0}-\u{104A9}\u{104B0}-\u{104D3}\u{104D8}-\u{104FB}\u{10500}-\u{10527}\u{10530}-\u{10563}\u{1056F}-\u{1057A}\u{1057C}-\u{1058A}\u{1058C}-\u{10592}\u{10594}\u{10595}\u{10597}-\u{105A1}\u{105A3}-\u{105B1}\u{105B3}-\u{105B9}\u{105BB}\u{105BC}\u{10600}-\u{10736}\u{10740}-\u{10755}\u{10760}-\u{10767}\u{10780}-\u{10785}\u{10787}-\u{107B0}\u{107B2}-\u{107BA}\u{11000}\u{11002}-\u{11037}\u{11047}-\u{1104D}\u{11066}-\u{1106F}\u{11071}\u{11072}\u{11075}\u{11082}-\u{110B2}\u{110B7}\u{110B8}\u{110BB}-\u{110C1}\u{110CD}\u{110D0}-\u{110E8}\u{110F0}-\u{110F9}\u{11103}-\u{11126}\u{1112C}\u{11136}-\u{11147}\u{11150}-\u{11172}\u{11174}-\u{11176}\u{11182}-\u{111B5}\u{111BF}-\u{111C8}\u{111CD}\u{111CE}\u{111D0}-\u{111DF}\u{111E1}-\u{111F4}\u{11200}-\u{11211}\u{11213}-\u{1122E}\u{11232}\u{11233}\u{11235}\u{11238}-\u{1123D}\u{1123F}\u{11240}\u{11280}-\u{11286}\u{11288}\u{1128A}-\u{1128D}\u{1128F}-\u{1129D}\u{1129F}-\u{112A9}\u{112B0}-\u{112DE}\u{112E0}-\u{112E2}\u{112F0}-\u{112F9}\u{11302}\u{11303}\u{11305}-\u{1130C}\u{1130F}\u{11310}\u{11313}-\u{11328}\u{1132A}-\u{11330}\u{11332}\u{11333}\u{11335}-\u{11339}\u{1133D}-\u{1133F}\u{11341}-\u{11344}\u{11347}\u{11348}\u{1134B}-\u{1134D}\u{11350}\u{11357}\u{1135D}-\u{11363}\u{11400}-\u{11437}\u{11440}\u{11441}\u{11445}\u{11447}-\u{1145B}\u{1145D}\u{1145F}-\u{11461}\u{11480}-\u{114B2}\u{114B9}\u{114BB}-\u{114BE}\u{114C1}\u{114C4}-\u{114C7}\u{114D0}-\u{114D9}\u{11580}-\u{115B1}\u{115B8}-\u{115BB}\u{115BE}\u{115C1}-\u{115DB}\u{11600}-\u{11632}\u{1163B}\u{1163C}\u{1163E}\u{11641}-\u{11644}\u{11650}-\u{11659}\u{11680}-\u{116AA}\u{116AC}\u{116AE}\u{116AF}\u{116B6}\u{116B8}\u{116B9}\u{116C0}-\u{116C9}\u{11700}-\u{1171A}\u{11720}\u{11721}\u{11726}\u{11730}-\u{11746}\u{11800}-\u{1182E}\u{11838}\u{1183B}\u{118A0}-\u{118F2}\u{118FF}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}\u{11916}\u{11918}-\u{11935}\u{11937}\u{11938}\u{1193D}\u{1193F}-\u{11942}\u{11944}-\u{11946}\u{11950}-\u{11959}\u{119A0}-\u{119A7}\u{119AA}-\u{119D3}\u{119DC}-\u{119DF}\u{119E1}-\u{119E4}\u{11A00}\u{11A07}\u{11A08}\u{11A0B}-\u{11A32}\u{11A39}\u{11A3A}\u{11A3F}-\u{11A46}\u{11A50}\u{11A57}\u{11A58}\u{11A5C}-\u{11A89}\u{11A97}\u{11A9A}-\u{11AA2}\u{11AB0}-\u{11AF8}\u{11B00}-\u{11B09}\u{11C00}-\u{11C08}\u{11C0A}-\u{11C2F}\u{11C3E}-\u{11C45}\u{11C50}-\u{11C6C}\u{11C70}-\u{11C8F}\u{11CA9}\u{11CB1}\u{11CB4}\u{11D00}-\u{11D06}\u{11D08}\u{11D09}\u{11D0B}-\u{11D30}\u{11D46}\u{11D50}-\u{11D59}\u{11D60}-\u{11D65}\u{11D67}\u{11D68}\u{11D6A}-\u{11D8E}\u{11D93}\u{11D94}\u{11D96}\u{11D98}\u{11DA0}-\u{11DA9}\u{11EE0}-\u{11EF2}\u{11EF5}-\u{11EF8}\u{11F02}-\u{11F10}\u{11F12}-\u{11F35}\u{11F3E}\u{11F3F}\u{11F41}\u{11F43}-\u{11F59}\u{11FB0}\u{11FC0}-\u{11FD4}\u{11FFF}-\u{12399}\u{12400}-\u{1246E}\u{12470}-\u{12474}\u{12480}-\u{12543}\u{12F90}-\u{12FF2}\u{13000}-\u{1343F}\u{13441}-\u{13446}\u{14400}-\u{14646}\u{16800}-\u{16A38}\u{16A40}-\u{16A5E}\u{16A60}-\u{16A69}\u{16A6E}-\u{16ABE}\u{16AC0}-\u{16AC9}\u{16AD0}-\u{16AED}\u{16AF5}\u{16B00}-\u{16B2F}\u{16B37}-\u{16B45}\u{16B50}-\u{16B59}\u{16B5B}-\u{16B61}\u{16B63}-\u{16B77}\u{16B7D}-\u{16B8F}\u{16E40}-\u{16E9A}\u{16F00}-\u{16F4A}\u{16F50}-\u{16F87}\u{16F93}-\u{16F9F}\u{16FE0}\u{16FE1}\u{16FE3}\u{16FF0}\u{16FF1}\u{17000}-\u{187F7}\u{18800}-\u{18CD5}\u{18D00}-\u{18D08}\u{1AFF0}-\u{1AFF3}\u{1AFF5}-\u{1AFFB}\u{1AFFD}\u{1AFFE}\u{1B000}-\u{1B122}\u{1B132}\u{1B150}-\u{1B152}\u{1B155}\u{1B164}-\u{1B167}\u{1B170}-\u{1B2FB}\u{1BC00}-\u{1BC6A}\u{1BC70}-\u{1BC7C}\u{1BC80}-\u{1BC88}\u{1BC90}-\u{1BC99}\u{1BC9C}\u{1BC9F}\u{1CF50}-\u{1CFC3}\u{1D000}-\u{1D0F5}\u{1D100}-\u{1D126}\u{1D129}-\u{1D166}\u{1D16A}-\u{1D172}\u{1D183}\u{1D184}\u{1D18C}-\u{1D1A9}\u{1D1AE}-\u{1D1E8}\u{1D2C0}-\u{1D2D3}\u{1D2E0}-\u{1D2F3}\u{1D360}-\u{1D378}\u{1D400}-\u{1D454}\u{1D456}-\u{1D49C}\u{1D49E}\u{1D49F}\u{1D4A2}\u{1D4A5}\u{1D4A6}\u{1D4A9}-\u{1D4AC}\u{1D4AE}-\u{1D4B9}\u{1D4BB}\u{1D4BD}-\u{1D4C3}\u{1D4C5}-\u{1D505}\u{1D507}-\u{1D50A}\u{1D50D}-\u{1D514}\u{1D516}-\u{1D51C}\u{1D51E}-\u{1D539}\u{1D53B}-\u{1D53E}\u{1D540}-\u{1D544}\u{1D546}\u{1D54A}-\u{1D550}\u{1D552}-\u{1D6A5}\u{1D6A8}-\u{1D6DA}\u{1D6DC}-\u{1D714}\u{1D716}-\u{1D74E}\u{1D750}-\u{1D788}\u{1D78A}-\u{1D7C2}\u{1D7C4}-\u{1D7CB}\u{1D7CE}-\u{1D9FF}\u{1DA37}-\u{1DA3A}\u{1DA6D}-\u{1DA74}\u{1DA76}-\u{1DA83}\u{1DA85}-\u{1DA8B}\u{1DF00}-\u{1DF1E}\u{1DF25}-\u{1DF2A}\u{1E030}-\u{1E06D}\u{1E100}-\u{1E12C}\u{1E137}-\u{1E13D}\u{1E140}-\u{1E149}\u{1E14E}\u{1E14F}\u{1E290}-\u{1E2AD}\u{1E2C0}-\u{1E2EB}\u{1E2F0}-\u{1E2F9}\u{1E4D0}-\u{1E4EB}\u{1E4F0}-\u{1E4F9}\u{1E7E0}-\u{1E7E6}\u{1E7E8}-\u{1E7EB}\u{1E7ED}\u{1E7EE}\u{1E7F0}-\u{1E7FE}\u{1F100}-\u{1F10A}\u{1F110}-\u{1F12E}\u{1F130}-\u{1F169}\u{1F170}-\u{1F1AC}\u{1F1E6}-\u{1F202}\u{1F210}-\u{1F23B}\u{1F240}-\u{1F248}\u{1F250}\u{1F251}\u{1FBF0}-\u{1FBF9}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B739}\u{2B740}-\u{2B81D}\u{2B820}-\u{2CEA1}\u{2CEB0}-\u{2EBE0}\u{2EBF0}-\u{2EE5D}\u{2F800}-\u{2FA1D}\u{30000}-\u{3134A}\u{31350}-\u{323AF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}][\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0898-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B55\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CCC\u0CCD\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0D81\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732\u1733\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u180F\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ACE\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}\u{10AE6}\u{10D24}-\u{10D27}\u{10EAB}\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}\u{11301}\u{1133B}\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}\u{114C0}\u{114C2}\u{114C3}\u{115B2}-\u{115B5}\u{115BC}\u{115BD}\u{115BF}\u{115C0}\u{115DC}\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}\u{1183A}\u{1193B}\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}\u{119DB}\u{119E0}\u{11A01}-\u{11A06}\u{11A09}\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}\u{11CB3}\u{11CB5}\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}\u{11D91}\u{11D95}\u{11D97}\u{11EF3}\u{11EF4}\u{11F00}\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{1BC9D}\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{E0100}-\u{E01EF}]*$/u; module.exports = { combiningMarks, combiningClassVirama, @@ -789,7 +789,7 @@ var require_regexes = __commonJS({ // node_modules/tr46/lib/mappingTable.json var require_mappingTable = __commonJS({ "node_modules/tr46/lib/mappingTable.json"(exports, module) { - module.exports = [[[0, 44], 4], [[45, 46], 2], [47, 4], [[48, 57], 2], [[58, 64], 4], [65, 1, "a"], [66, 1, "b"], [67, 1, "c"], [68, 1, "d"], [69, 1, "e"], [70, 1, "f"], [71, 1, "g"], [72, 1, "h"], [73, 1, "i"], [74, 1, "j"], [75, 1, "k"], [76, 1, "l"], [77, 1, "m"], [78, 1, "n"], [79, 1, "o"], [80, 1, "p"], [81, 1, "q"], [82, 1, "r"], [83, 1, "s"], [84, 1, "t"], [85, 1, "u"], [86, 1, "v"], [87, 1, "w"], [88, 1, "x"], [89, 1, "y"], [90, 1, "z"], [[91, 96], 4], [[97, 122], 2], [[123, 127], 4], [[128, 159], 3], [160, 5, " "], [[161, 167], 2], [168, 5, " \u0308"], [169, 2], [170, 1, "a"], [[171, 172], 2], [173, 7], [174, 2], [175, 5, " \u0304"], [[176, 177], 2], [178, 1, "2"], [179, 1, "3"], [180, 5, " \u0301"], [181, 1, "\u03BC"], [182, 2], [183, 2], [184, 5, " \u0327"], [185, 1, "1"], [186, 1, "o"], [187, 2], [188, 1, "1\u20444"], [189, 1, "1\u20442"], [190, 1, "3\u20444"], [191, 2], [192, 1, "\xE0"], [193, 1, "\xE1"], [194, 1, "\xE2"], [195, 1, "\xE3"], [196, 1, "\xE4"], [197, 1, "\xE5"], [198, 1, "\xE6"], [199, 1, "\xE7"], [200, 1, "\xE8"], [201, 1, "\xE9"], [202, 1, "\xEA"], [203, 1, "\xEB"], [204, 1, "\xEC"], [205, 1, "\xED"], [206, 1, "\xEE"], [207, 1, "\xEF"], [208, 1, "\xF0"], [209, 1, "\xF1"], [210, 1, "\xF2"], [211, 1, "\xF3"], [212, 1, "\xF4"], [213, 1, "\xF5"], [214, 1, "\xF6"], [215, 2], [216, 1, "\xF8"], [217, 1, "\xF9"], [218, 1, "\xFA"], [219, 1, "\xFB"], [220, 1, "\xFC"], [221, 1, "\xFD"], [222, 1, "\xFE"], [223, 6, "ss"], [[224, 246], 2], [247, 2], [[248, 255], 2], [256, 1, "\u0101"], [257, 2], [258, 1, "\u0103"], [259, 2], [260, 1, "\u0105"], [261, 2], [262, 1, "\u0107"], [263, 2], [264, 1, "\u0109"], [265, 2], [266, 1, "\u010B"], [267, 2], [268, 1, "\u010D"], [269, 2], [270, 1, "\u010F"], [271, 2], [272, 1, "\u0111"], [273, 2], [274, 1, "\u0113"], [275, 2], [276, 1, "\u0115"], [277, 2], [278, 1, "\u0117"], [279, 2], [280, 1, "\u0119"], [281, 2], [282, 1, "\u011B"], [283, 2], [284, 1, "\u011D"], [285, 2], [286, 1, "\u011F"], [287, 2], [288, 1, "\u0121"], [289, 2], [290, 1, "\u0123"], [291, 2], [292, 1, "\u0125"], [293, 2], [294, 1, "\u0127"], [295, 2], [296, 1, "\u0129"], [297, 2], [298, 1, "\u012B"], [299, 2], [300, 1, "\u012D"], [301, 2], [302, 1, "\u012F"], [303, 2], [304, 1, "i\u0307"], [305, 2], [[306, 307], 1, "ij"], [308, 1, "\u0135"], [309, 2], [310, 1, "\u0137"], [[311, 312], 2], [313, 1, "\u013A"], [314, 2], [315, 1, "\u013C"], [316, 2], [317, 1, "\u013E"], [318, 2], [[319, 320], 1, "l\xB7"], [321, 1, "\u0142"], [322, 2], [323, 1, "\u0144"], [324, 2], [325, 1, "\u0146"], [326, 2], [327, 1, "\u0148"], [328, 2], [329, 1, "\u02BCn"], [330, 1, "\u014B"], [331, 2], [332, 1, "\u014D"], [333, 2], [334, 1, "\u014F"], [335, 2], [336, 1, "\u0151"], [337, 2], [338, 1, "\u0153"], [339, 2], [340, 1, "\u0155"], [341, 2], [342, 1, "\u0157"], [343, 2], [344, 1, "\u0159"], [345, 2], [346, 1, "\u015B"], [347, 2], [348, 1, "\u015D"], [349, 2], [350, 1, "\u015F"], [351, 2], [352, 1, "\u0161"], [353, 2], [354, 1, "\u0163"], [355, 2], [356, 1, "\u0165"], [357, 2], [358, 1, "\u0167"], [359, 2], [360, 1, "\u0169"], [361, 2], [362, 1, "\u016B"], [363, 2], [364, 1, "\u016D"], [365, 2], [366, 1, "\u016F"], [367, 2], [368, 1, "\u0171"], [369, 2], [370, 1, "\u0173"], [371, 2], [372, 1, "\u0175"], [373, 2], [374, 1, "\u0177"], [375, 2], [376, 1, "\xFF"], [377, 1, "\u017A"], [378, 2], [379, 1, "\u017C"], [380, 2], [381, 1, "\u017E"], [382, 2], [383, 1, "s"], [384, 2], [385, 1, "\u0253"], [386, 1, "\u0183"], [387, 2], [388, 1, "\u0185"], [389, 2], [390, 1, "\u0254"], [391, 1, "\u0188"], [392, 2], [393, 1, "\u0256"], [394, 1, "\u0257"], [395, 1, "\u018C"], [[396, 397], 2], [398, 1, "\u01DD"], [399, 1, "\u0259"], [400, 1, "\u025B"], [401, 1, "\u0192"], [402, 2], [403, 1, "\u0260"], [404, 1, "\u0263"], [405, 2], [406, 1, "\u0269"], [407, 1, "\u0268"], [408, 1, "\u0199"], [[409, 411], 2], [412, 1, "\u026F"], [413, 1, "\u0272"], [414, 2], [415, 1, "\u0275"], [416, 1, "\u01A1"], [417, 2], [418, 1, "\u01A3"], [419, 2], [420, 1, "\u01A5"], [421, 2], [422, 1, "\u0280"], [423, 1, "\u01A8"], [424, 2], [425, 1, "\u0283"], [[426, 427], 2], [428, 1, "\u01AD"], [429, 2], [430, 1, "\u0288"], [431, 1, "\u01B0"], [432, 2], [433, 1, "\u028A"], [434, 1, "\u028B"], [435, 1, "\u01B4"], [436, 2], [437, 1, "\u01B6"], [438, 2], [439, 1, "\u0292"], [440, 1, "\u01B9"], [[441, 443], 2], [444, 1, "\u01BD"], [[445, 451], 2], [[452, 454], 1, "d\u017E"], [[455, 457], 1, "lj"], [[458, 460], 1, "nj"], [461, 1, "\u01CE"], [462, 2], [463, 1, "\u01D0"], [464, 2], [465, 1, "\u01D2"], [466, 2], [467, 1, "\u01D4"], [468, 2], [469, 1, "\u01D6"], [470, 2], [471, 1, "\u01D8"], [472, 2], [473, 1, "\u01DA"], [474, 2], [475, 1, "\u01DC"], [[476, 477], 2], [478, 1, "\u01DF"], [479, 2], [480, 1, "\u01E1"], [481, 2], [482, 1, "\u01E3"], [483, 2], [484, 1, "\u01E5"], [485, 2], [486, 1, "\u01E7"], [487, 2], [488, 1, "\u01E9"], [489, 2], [490, 1, "\u01EB"], [491, 2], [492, 1, "\u01ED"], [493, 2], [494, 1, "\u01EF"], [[495, 496], 2], [[497, 499], 1, "dz"], [500, 1, "\u01F5"], [501, 2], [502, 1, "\u0195"], [503, 1, "\u01BF"], [504, 1, "\u01F9"], [505, 2], [506, 1, "\u01FB"], [507, 2], [508, 1, "\u01FD"], [509, 2], [510, 1, "\u01FF"], [511, 2], [512, 1, "\u0201"], [513, 2], [514, 1, "\u0203"], [515, 2], [516, 1, "\u0205"], [517, 2], [518, 1, "\u0207"], [519, 2], [520, 1, "\u0209"], [521, 2], [522, 1, "\u020B"], [523, 2], [524, 1, "\u020D"], [525, 2], [526, 1, "\u020F"], [527, 2], [528, 1, "\u0211"], [529, 2], [530, 1, "\u0213"], [531, 2], [532, 1, "\u0215"], [533, 2], [534, 1, "\u0217"], [535, 2], [536, 1, "\u0219"], [537, 2], [538, 1, "\u021B"], [539, 2], [540, 1, "\u021D"], [541, 2], [542, 1, "\u021F"], [543, 2], [544, 1, "\u019E"], [545, 2], [546, 1, "\u0223"], [547, 2], [548, 1, "\u0225"], [549, 2], [550, 1, "\u0227"], [551, 2], [552, 1, "\u0229"], [553, 2], [554, 1, "\u022B"], [555, 2], [556, 1, "\u022D"], [557, 2], [558, 1, "\u022F"], [559, 2], [560, 1, "\u0231"], [561, 2], [562, 1, "\u0233"], [563, 2], [[564, 566], 2], [[567, 569], 2], [570, 1, "\u2C65"], [571, 1, "\u023C"], [572, 2], [573, 1, "\u019A"], [574, 1, "\u2C66"], [[575, 576], 2], [577, 1, "\u0242"], [578, 2], [579, 1, "\u0180"], [580, 1, "\u0289"], [581, 1, "\u028C"], [582, 1, "\u0247"], [583, 2], [584, 1, "\u0249"], [585, 2], [586, 1, "\u024B"], [587, 2], [588, 1, "\u024D"], [589, 2], [590, 1, "\u024F"], [591, 2], [[592, 680], 2], [[681, 685], 2], [[686, 687], 2], [688, 1, "h"], [689, 1, "\u0266"], [690, 1, "j"], [691, 1, "r"], [692, 1, "\u0279"], [693, 1, "\u027B"], [694, 1, "\u0281"], [695, 1, "w"], [696, 1, "y"], [[697, 705], 2], [[706, 709], 2], [[710, 721], 2], [[722, 727], 2], [728, 5, " \u0306"], [729, 5, " \u0307"], [730, 5, " \u030A"], [731, 5, " \u0328"], [732, 5, " \u0303"], [733, 5, " \u030B"], [734, 2], [735, 2], [736, 1, "\u0263"], [737, 1, "l"], [738, 1, "s"], [739, 1, "x"], [740, 1, "\u0295"], [[741, 745], 2], [[746, 747], 2], [748, 2], [749, 2], [750, 2], [[751, 767], 2], [[768, 831], 2], [832, 1, "\u0300"], [833, 1, "\u0301"], [834, 2], [835, 1, "\u0313"], [836, 1, "\u0308\u0301"], [837, 1, "\u03B9"], [[838, 846], 2], [847, 7], [[848, 855], 2], [[856, 860], 2], [[861, 863], 2], [[864, 865], 2], [866, 2], [[867, 879], 2], [880, 1, "\u0371"], [881, 2], [882, 1, "\u0373"], [883, 2], [884, 1, "\u02B9"], [885, 2], [886, 1, "\u0377"], [887, 2], [[888, 889], 3], [890, 5, " \u03B9"], [[891, 893], 2], [894, 5, ";"], [895, 1, "\u03F3"], [[896, 899], 3], [900, 5, " \u0301"], [901, 5, " \u0308\u0301"], [902, 1, "\u03AC"], [903, 1, "\xB7"], [904, 1, "\u03AD"], [905, 1, "\u03AE"], [906, 1, "\u03AF"], [907, 3], [908, 1, "\u03CC"], [909, 3], [910, 1, "\u03CD"], [911, 1, "\u03CE"], [912, 2], [913, 1, "\u03B1"], [914, 1, "\u03B2"], [915, 1, "\u03B3"], [916, 1, "\u03B4"], [917, 1, "\u03B5"], [918, 1, "\u03B6"], [919, 1, "\u03B7"], [920, 1, "\u03B8"], [921, 1, "\u03B9"], [922, 1, "\u03BA"], [923, 1, "\u03BB"], [924, 1, "\u03BC"], [925, 1, "\u03BD"], [926, 1, "\u03BE"], [927, 1, "\u03BF"], [928, 1, "\u03C0"], [929, 1, "\u03C1"], [930, 3], [931, 1, "\u03C3"], [932, 1, "\u03C4"], [933, 1, "\u03C5"], [934, 1, "\u03C6"], [935, 1, "\u03C7"], [936, 1, "\u03C8"], [937, 1, "\u03C9"], [938, 1, "\u03CA"], [939, 1, "\u03CB"], [[940, 961], 2], [962, 6, "\u03C3"], [[963, 974], 2], [975, 1, "\u03D7"], [976, 1, "\u03B2"], [977, 1, "\u03B8"], [978, 1, "\u03C5"], [979, 1, "\u03CD"], [980, 1, "\u03CB"], [981, 1, "\u03C6"], [982, 1, "\u03C0"], [983, 2], [984, 1, "\u03D9"], [985, 2], [986, 1, "\u03DB"], [987, 2], [988, 1, "\u03DD"], [989, 2], [990, 1, "\u03DF"], [991, 2], [992, 1, "\u03E1"], [993, 2], [994, 1, "\u03E3"], [995, 2], [996, 1, "\u03E5"], [997, 2], [998, 1, "\u03E7"], [999, 2], [1e3, 1, "\u03E9"], [1001, 2], [1002, 1, "\u03EB"], [1003, 2], [1004, 1, "\u03ED"], [1005, 2], [1006, 1, "\u03EF"], [1007, 2], [1008, 1, "\u03BA"], [1009, 1, "\u03C1"], [1010, 1, "\u03C3"], [1011, 2], [1012, 1, "\u03B8"], [1013, 1, "\u03B5"], [1014, 2], [1015, 1, "\u03F8"], [1016, 2], [1017, 1, "\u03C3"], [1018, 1, "\u03FB"], [1019, 2], [1020, 2], [1021, 1, "\u037B"], [1022, 1, "\u037C"], [1023, 1, "\u037D"], [1024, 1, "\u0450"], [1025, 1, "\u0451"], [1026, 1, "\u0452"], [1027, 1, "\u0453"], [1028, 1, "\u0454"], [1029, 1, "\u0455"], [1030, 1, "\u0456"], [1031, 1, "\u0457"], [1032, 1, "\u0458"], [1033, 1, "\u0459"], [1034, 1, "\u045A"], [1035, 1, "\u045B"], [1036, 1, "\u045C"], [1037, 1, "\u045D"], [1038, 1, "\u045E"], [1039, 1, "\u045F"], [1040, 1, "\u0430"], [1041, 1, "\u0431"], [1042, 1, "\u0432"], [1043, 1, "\u0433"], [1044, 1, "\u0434"], [1045, 1, "\u0435"], [1046, 1, "\u0436"], [1047, 1, "\u0437"], [1048, 1, "\u0438"], [1049, 1, "\u0439"], [1050, 1, "\u043A"], [1051, 1, "\u043B"], [1052, 1, "\u043C"], [1053, 1, "\u043D"], [1054, 1, "\u043E"], [1055, 1, "\u043F"], [1056, 1, "\u0440"], [1057, 1, "\u0441"], [1058, 1, "\u0442"], [1059, 1, "\u0443"], [1060, 1, "\u0444"], [1061, 1, "\u0445"], [1062, 1, "\u0446"], [1063, 1, "\u0447"], [1064, 1, "\u0448"], [1065, 1, "\u0449"], [1066, 1, "\u044A"], [1067, 1, "\u044B"], [1068, 1, "\u044C"], [1069, 1, "\u044D"], [1070, 1, "\u044E"], [1071, 1, "\u044F"], [[1072, 1103], 2], [1104, 2], [[1105, 1116], 2], [1117, 2], [[1118, 1119], 2], [1120, 1, "\u0461"], [1121, 2], [1122, 1, "\u0463"], [1123, 2], [1124, 1, "\u0465"], [1125, 2], [1126, 1, "\u0467"], [1127, 2], [1128, 1, "\u0469"], [1129, 2], [1130, 1, "\u046B"], [1131, 2], [1132, 1, "\u046D"], [1133, 2], [1134, 1, "\u046F"], [1135, 2], [1136, 1, "\u0471"], [1137, 2], [1138, 1, "\u0473"], [1139, 2], [1140, 1, "\u0475"], [1141, 2], [1142, 1, "\u0477"], [1143, 2], [1144, 1, "\u0479"], [1145, 2], [1146, 1, "\u047B"], [1147, 2], [1148, 1, "\u047D"], [1149, 2], [1150, 1, "\u047F"], [1151, 2], [1152, 1, "\u0481"], [1153, 2], [1154, 2], [[1155, 1158], 2], [1159, 2], [[1160, 1161], 2], [1162, 1, "\u048B"], [1163, 2], [1164, 1, "\u048D"], [1165, 2], [1166, 1, "\u048F"], [1167, 2], [1168, 1, "\u0491"], [1169, 2], [1170, 1, "\u0493"], [1171, 2], [1172, 1, "\u0495"], [1173, 2], [1174, 1, "\u0497"], [1175, 2], [1176, 1, "\u0499"], [1177, 2], [1178, 1, "\u049B"], [1179, 2], [1180, 1, "\u049D"], [1181, 2], [1182, 1, "\u049F"], [1183, 2], [1184, 1, "\u04A1"], [1185, 2], [1186, 1, "\u04A3"], [1187, 2], [1188, 1, "\u04A5"], [1189, 2], [1190, 1, "\u04A7"], [1191, 2], [1192, 1, "\u04A9"], [1193, 2], [1194, 1, "\u04AB"], [1195, 2], [1196, 1, "\u04AD"], [1197, 2], [1198, 1, "\u04AF"], [1199, 2], [1200, 1, "\u04B1"], [1201, 2], [1202, 1, "\u04B3"], [1203, 2], [1204, 1, "\u04B5"], [1205, 2], [1206, 1, "\u04B7"], [1207, 2], [1208, 1, "\u04B9"], [1209, 2], [1210, 1, "\u04BB"], [1211, 2], [1212, 1, "\u04BD"], [1213, 2], [1214, 1, "\u04BF"], [1215, 2], [1216, 3], [1217, 1, "\u04C2"], [1218, 2], [1219, 1, "\u04C4"], [1220, 2], [1221, 1, "\u04C6"], [1222, 2], [1223, 1, "\u04C8"], [1224, 2], [1225, 1, "\u04CA"], [1226, 2], [1227, 1, "\u04CC"], [1228, 2], [1229, 1, "\u04CE"], [1230, 2], [1231, 2], [1232, 1, "\u04D1"], [1233, 2], [1234, 1, "\u04D3"], [1235, 2], [1236, 1, "\u04D5"], [1237, 2], [1238, 1, "\u04D7"], [1239, 2], [1240, 1, "\u04D9"], [1241, 2], [1242, 1, "\u04DB"], [1243, 2], [1244, 1, "\u04DD"], [1245, 2], [1246, 1, "\u04DF"], [1247, 2], [1248, 1, "\u04E1"], [1249, 2], [1250, 1, "\u04E3"], [1251, 2], [1252, 1, "\u04E5"], [1253, 2], [1254, 1, "\u04E7"], [1255, 2], [1256, 1, "\u04E9"], [1257, 2], [1258, 1, "\u04EB"], [1259, 2], [1260, 1, "\u04ED"], [1261, 2], [1262, 1, "\u04EF"], [1263, 2], [1264, 1, "\u04F1"], [1265, 2], [1266, 1, "\u04F3"], [1267, 2], [1268, 1, "\u04F5"], [1269, 2], [1270, 1, "\u04F7"], [1271, 2], [1272, 1, "\u04F9"], [1273, 2], [1274, 1, "\u04FB"], [1275, 2], [1276, 1, "\u04FD"], [1277, 2], [1278, 1, "\u04FF"], [1279, 2], [1280, 1, "\u0501"], [1281, 2], [1282, 1, "\u0503"], [1283, 2], [1284, 1, "\u0505"], [1285, 2], [1286, 1, "\u0507"], [1287, 2], [1288, 1, "\u0509"], [1289, 2], [1290, 1, "\u050B"], [1291, 2], [1292, 1, "\u050D"], [1293, 2], [1294, 1, "\u050F"], [1295, 2], [1296, 1, "\u0511"], [1297, 2], [1298, 1, "\u0513"], [1299, 2], [1300, 1, "\u0515"], [1301, 2], [1302, 1, "\u0517"], [1303, 2], [1304, 1, "\u0519"], [1305, 2], [1306, 1, "\u051B"], [1307, 2], [1308, 1, "\u051D"], [1309, 2], [1310, 1, "\u051F"], [1311, 2], [1312, 1, "\u0521"], [1313, 2], [1314, 1, "\u0523"], [1315, 2], [1316, 1, "\u0525"], [1317, 2], [1318, 1, "\u0527"], [1319, 2], [1320, 1, "\u0529"], [1321, 2], [1322, 1, "\u052B"], [1323, 2], [1324, 1, "\u052D"], [1325, 2], [1326, 1, "\u052F"], [1327, 2], [1328, 3], [1329, 1, "\u0561"], [1330, 1, "\u0562"], [1331, 1, "\u0563"], [1332, 1, "\u0564"], [1333, 1, "\u0565"], [1334, 1, "\u0566"], [1335, 1, "\u0567"], [1336, 1, "\u0568"], [1337, 1, "\u0569"], [1338, 1, "\u056A"], [1339, 1, "\u056B"], [1340, 1, "\u056C"], [1341, 1, "\u056D"], [1342, 1, "\u056E"], [1343, 1, "\u056F"], [1344, 1, "\u0570"], [1345, 1, "\u0571"], [1346, 1, "\u0572"], [1347, 1, "\u0573"], [1348, 1, "\u0574"], [1349, 1, "\u0575"], [1350, 1, "\u0576"], [1351, 1, "\u0577"], [1352, 1, "\u0578"], [1353, 1, "\u0579"], [1354, 1, "\u057A"], [1355, 1, "\u057B"], [1356, 1, "\u057C"], [1357, 1, "\u057D"], [1358, 1, "\u057E"], [1359, 1, "\u057F"], [1360, 1, "\u0580"], [1361, 1, "\u0581"], [1362, 1, "\u0582"], [1363, 1, "\u0583"], [1364, 1, "\u0584"], [1365, 1, "\u0585"], [1366, 1, "\u0586"], [[1367, 1368], 3], [1369, 2], [[1370, 1375], 2], [1376, 2], [[1377, 1414], 2], [1415, 1, "\u0565\u0582"], [1416, 2], [1417, 2], [1418, 2], [[1419, 1420], 3], [[1421, 1422], 2], [1423, 2], [1424, 3], [[1425, 1441], 2], [1442, 2], [[1443, 1455], 2], [[1456, 1465], 2], [1466, 2], [[1467, 1469], 2], [1470, 2], [1471, 2], [1472, 2], [[1473, 1474], 2], [1475, 2], [1476, 2], [1477, 2], [1478, 2], [1479, 2], [[1480, 1487], 3], [[1488, 1514], 2], [[1515, 1518], 3], [1519, 2], [[1520, 1524], 2], [[1525, 1535], 3], [[1536, 1539], 3], [1540, 3], [1541, 3], [[1542, 1546], 2], [1547, 2], [1548, 2], [[1549, 1551], 2], [[1552, 1557], 2], [[1558, 1562], 2], [1563, 2], [1564, 3], [1565, 2], [1566, 2], [1567, 2], [1568, 2], [[1569, 1594], 2], [[1595, 1599], 2], [1600, 2], [[1601, 1618], 2], [[1619, 1621], 2], [[1622, 1624], 2], [[1625, 1630], 2], [1631, 2], [[1632, 1641], 2], [[1642, 1645], 2], [[1646, 1647], 2], [[1648, 1652], 2], [1653, 1, "\u0627\u0674"], [1654, 1, "\u0648\u0674"], [1655, 1, "\u06C7\u0674"], [1656, 1, "\u064A\u0674"], [[1657, 1719], 2], [[1720, 1721], 2], [[1722, 1726], 2], [1727, 2], [[1728, 1742], 2], [1743, 2], [[1744, 1747], 2], [1748, 2], [[1749, 1756], 2], [1757, 3], [1758, 2], [[1759, 1768], 2], [1769, 2], [[1770, 1773], 2], [[1774, 1775], 2], [[1776, 1785], 2], [[1786, 1790], 2], [1791, 2], [[1792, 1805], 2], [1806, 3], [1807, 3], [[1808, 1836], 2], [[1837, 1839], 2], [[1840, 1866], 2], [[1867, 1868], 3], [[1869, 1871], 2], [[1872, 1901], 2], [[1902, 1919], 2], [[1920, 1968], 2], [1969, 2], [[1970, 1983], 3], [[1984, 2037], 2], [[2038, 2042], 2], [[2043, 2044], 3], [2045, 2], [[2046, 2047], 2], [[2048, 2093], 2], [[2094, 2095], 3], [[2096, 2110], 2], [2111, 3], [[2112, 2139], 2], [[2140, 2141], 3], [2142, 2], [2143, 3], [[2144, 2154], 2], [[2155, 2159], 3], [[2160, 2183], 2], [2184, 2], [[2185, 2190], 2], [2191, 3], [[2192, 2193], 3], [[2194, 2199], 3], [[2200, 2207], 2], [2208, 2], [2209, 2], [[2210, 2220], 2], [[2221, 2226], 2], [[2227, 2228], 2], [2229, 2], [[2230, 2237], 2], [[2238, 2247], 2], [[2248, 2258], 2], [2259, 2], [[2260, 2273], 2], [2274, 3], [2275, 2], [[2276, 2302], 2], [2303, 2], [2304, 2], [[2305, 2307], 2], [2308, 2], [[2309, 2361], 2], [[2362, 2363], 2], [[2364, 2381], 2], [2382, 2], [2383, 2], [[2384, 2388], 2], [2389, 2], [[2390, 2391], 2], [2392, 1, "\u0915\u093C"], [2393, 1, "\u0916\u093C"], [2394, 1, "\u0917\u093C"], [2395, 1, "\u091C\u093C"], [2396, 1, "\u0921\u093C"], [2397, 1, "\u0922\u093C"], [2398, 1, "\u092B\u093C"], [2399, 1, "\u092F\u093C"], [[2400, 2403], 2], [[2404, 2405], 2], [[2406, 2415], 2], [2416, 2], [[2417, 2418], 2], [[2419, 2423], 2], [2424, 2], [[2425, 2426], 2], [[2427, 2428], 2], [2429, 2], [[2430, 2431], 2], [2432, 2], [[2433, 2435], 2], [2436, 3], [[2437, 2444], 2], [[2445, 2446], 3], [[2447, 2448], 2], [[2449, 2450], 3], [[2451, 2472], 2], [2473, 3], [[2474, 2480], 2], [2481, 3], [2482, 2], [[2483, 2485], 3], [[2486, 2489], 2], [[2490, 2491], 3], [2492, 2], [2493, 2], [[2494, 2500], 2], [[2501, 2502], 3], [[2503, 2504], 2], [[2505, 2506], 3], [[2507, 2509], 2], [2510, 2], [[2511, 2518], 3], [2519, 2], [[2520, 2523], 3], [2524, 1, "\u09A1\u09BC"], [2525, 1, "\u09A2\u09BC"], [2526, 3], [2527, 1, "\u09AF\u09BC"], [[2528, 2531], 2], [[2532, 2533], 3], [[2534, 2545], 2], [[2546, 2554], 2], [2555, 2], [2556, 2], [2557, 2], [2558, 2], [[2559, 2560], 3], [2561, 2], [2562, 2], [2563, 2], [2564, 3], [[2565, 2570], 2], [[2571, 2574], 3], [[2575, 2576], 2], [[2577, 2578], 3], [[2579, 2600], 2], [2601, 3], [[2602, 2608], 2], [2609, 3], [2610, 2], [2611, 1, "\u0A32\u0A3C"], [2612, 3], [2613, 2], [2614, 1, "\u0A38\u0A3C"], [2615, 3], [[2616, 2617], 2], [[2618, 2619], 3], [2620, 2], [2621, 3], [[2622, 2626], 2], [[2627, 2630], 3], [[2631, 2632], 2], [[2633, 2634], 3], [[2635, 2637], 2], [[2638, 2640], 3], [2641, 2], [[2642, 2648], 3], [2649, 1, "\u0A16\u0A3C"], [2650, 1, "\u0A17\u0A3C"], [2651, 1, "\u0A1C\u0A3C"], [2652, 2], [2653, 3], [2654, 1, "\u0A2B\u0A3C"], [[2655, 2661], 3], [[2662, 2676], 2], [2677, 2], [2678, 2], [[2679, 2688], 3], [[2689, 2691], 2], [2692, 3], [[2693, 2699], 2], [2700, 2], [2701, 2], [2702, 3], [[2703, 2705], 2], [2706, 3], [[2707, 2728], 2], [2729, 3], [[2730, 2736], 2], [2737, 3], [[2738, 2739], 2], [2740, 3], [[2741, 2745], 2], [[2746, 2747], 3], [[2748, 2757], 2], [2758, 3], [[2759, 2761], 2], [2762, 3], [[2763, 2765], 2], [[2766, 2767], 3], [2768, 2], [[2769, 2783], 3], [2784, 2], [[2785, 2787], 2], [[2788, 2789], 3], [[2790, 2799], 2], [2800, 2], [2801, 2], [[2802, 2808], 3], [2809, 2], [[2810, 2815], 2], [2816, 3], [[2817, 2819], 2], [2820, 3], [[2821, 2828], 2], [[2829, 2830], 3], [[2831, 2832], 2], [[2833, 2834], 3], [[2835, 2856], 2], [2857, 3], [[2858, 2864], 2], [2865, 3], [[2866, 2867], 2], [2868, 3], [2869, 2], [[2870, 2873], 2], [[2874, 2875], 3], [[2876, 2883], 2], [2884, 2], [[2885, 2886], 3], [[2887, 2888], 2], [[2889, 2890], 3], [[2891, 2893], 2], [[2894, 2900], 3], [2901, 2], [[2902, 2903], 2], [[2904, 2907], 3], [2908, 1, "\u0B21\u0B3C"], [2909, 1, "\u0B22\u0B3C"], [2910, 3], [[2911, 2913], 2], [[2914, 2915], 2], [[2916, 2917], 3], [[2918, 2927], 2], [2928, 2], [2929, 2], [[2930, 2935], 2], [[2936, 2945], 3], [[2946, 2947], 2], [2948, 3], [[2949, 2954], 2], [[2955, 2957], 3], [[2958, 2960], 2], [2961, 3], [[2962, 2965], 2], [[2966, 2968], 3], [[2969, 2970], 2], [2971, 3], [2972, 2], [2973, 3], [[2974, 2975], 2], [[2976, 2978], 3], [[2979, 2980], 2], [[2981, 2983], 3], [[2984, 2986], 2], [[2987, 2989], 3], [[2990, 2997], 2], [2998, 2], [[2999, 3001], 2], [[3002, 3005], 3], [[3006, 3010], 2], [[3011, 3013], 3], [[3014, 3016], 2], [3017, 3], [[3018, 3021], 2], [[3022, 3023], 3], [3024, 2], [[3025, 3030], 3], [3031, 2], [[3032, 3045], 3], [3046, 2], [[3047, 3055], 2], [[3056, 3058], 2], [[3059, 3066], 2], [[3067, 3071], 3], [3072, 2], [[3073, 3075], 2], [3076, 2], [[3077, 3084], 2], [3085, 3], [[3086, 3088], 2], [3089, 3], [[3090, 3112], 2], [3113, 3], [[3114, 3123], 2], [3124, 2], [[3125, 3129], 2], [[3130, 3131], 3], [3132, 2], [3133, 2], [[3134, 3140], 2], [3141, 3], [[3142, 3144], 2], [3145, 3], [[3146, 3149], 2], [[3150, 3156], 3], [[3157, 3158], 2], [3159, 3], [[3160, 3161], 2], [3162, 2], [[3163, 3164], 3], [3165, 2], [[3166, 3167], 3], [[3168, 3169], 2], [[3170, 3171], 2], [[3172, 3173], 3], [[3174, 3183], 2], [[3184, 3190], 3], [3191, 2], [[3192, 3199], 2], [3200, 2], [3201, 2], [[3202, 3203], 2], [3204, 2], [[3205, 3212], 2], [3213, 3], [[3214, 3216], 2], [3217, 3], [[3218, 3240], 2], [3241, 3], [[3242, 3251], 2], [3252, 3], [[3253, 3257], 2], [[3258, 3259], 3], [[3260, 3261], 2], [[3262, 3268], 2], [3269, 3], [[3270, 3272], 2], [3273, 3], [[3274, 3277], 2], [[3278, 3284], 3], [[3285, 3286], 2], [[3287, 3292], 3], [3293, 2], [3294, 2], [3295, 3], [[3296, 3297], 2], [[3298, 3299], 2], [[3300, 3301], 3], [[3302, 3311], 2], [3312, 3], [[3313, 3314], 2], [3315, 2], [[3316, 3327], 3], [3328, 2], [3329, 2], [[3330, 3331], 2], [3332, 2], [[3333, 3340], 2], [3341, 3], [[3342, 3344], 2], [3345, 3], [[3346, 3368], 2], [3369, 2], [[3370, 3385], 2], [3386, 2], [[3387, 3388], 2], [3389, 2], [[3390, 3395], 2], [3396, 2], [3397, 3], [[3398, 3400], 2], [3401, 3], [[3402, 3405], 2], [3406, 2], [3407, 2], [[3408, 3411], 3], [[3412, 3414], 2], [3415, 2], [[3416, 3422], 2], [3423, 2], [[3424, 3425], 2], [[3426, 3427], 2], [[3428, 3429], 3], [[3430, 3439], 2], [[3440, 3445], 2], [[3446, 3448], 2], [3449, 2], [[3450, 3455], 2], [3456, 3], [3457, 2], [[3458, 3459], 2], [3460, 3], [[3461, 3478], 2], [[3479, 3481], 3], [[3482, 3505], 2], [3506, 3], [[3507, 3515], 2], [3516, 3], [3517, 2], [[3518, 3519], 3], [[3520, 3526], 2], [[3527, 3529], 3], [3530, 2], [[3531, 3534], 3], [[3535, 3540], 2], [3541, 3], [3542, 2], [3543, 3], [[3544, 3551], 2], [[3552, 3557], 3], [[3558, 3567], 2], [[3568, 3569], 3], [[3570, 3571], 2], [3572, 2], [[3573, 3584], 3], [[3585, 3634], 2], [3635, 1, "\u0E4D\u0E32"], [[3636, 3642], 2], [[3643, 3646], 3], [3647, 2], [[3648, 3662], 2], [3663, 2], [[3664, 3673], 2], [[3674, 3675], 2], [[3676, 3712], 3], [[3713, 3714], 2], [3715, 3], [3716, 2], [3717, 3], [3718, 2], [[3719, 3720], 2], [3721, 2], [3722, 2], [3723, 3], [3724, 2], [3725, 2], [[3726, 3731], 2], [[3732, 3735], 2], [3736, 2], [[3737, 3743], 2], [3744, 2], [[3745, 3747], 2], [3748, 3], [3749, 2], [3750, 3], [3751, 2], [[3752, 3753], 2], [[3754, 3755], 2], [3756, 2], [[3757, 3762], 2], [3763, 1, "\u0ECD\u0EB2"], [[3764, 3769], 2], [3770, 2], [[3771, 3773], 2], [[3774, 3775], 3], [[3776, 3780], 2], [3781, 3], [3782, 2], [3783, 3], [[3784, 3789], 2], [3790, 2], [3791, 3], [[3792, 3801], 2], [[3802, 3803], 3], [3804, 1, "\u0EAB\u0E99"], [3805, 1, "\u0EAB\u0EA1"], [[3806, 3807], 2], [[3808, 3839], 3], [3840, 2], [[3841, 3850], 2], [3851, 2], [3852, 1, "\u0F0B"], [[3853, 3863], 2], [[3864, 3865], 2], [[3866, 3871], 2], [[3872, 3881], 2], [[3882, 3892], 2], [3893, 2], [3894, 2], [3895, 2], [3896, 2], [3897, 2], [[3898, 3901], 2], [[3902, 3906], 2], [3907, 1, "\u0F42\u0FB7"], [[3908, 3911], 2], [3912, 3], [[3913, 3916], 2], [3917, 1, "\u0F4C\u0FB7"], [[3918, 3921], 2], [3922, 1, "\u0F51\u0FB7"], [[3923, 3926], 2], [3927, 1, "\u0F56\u0FB7"], [[3928, 3931], 2], [3932, 1, "\u0F5B\u0FB7"], [[3933, 3944], 2], [3945, 1, "\u0F40\u0FB5"], [3946, 2], [[3947, 3948], 2], [[3949, 3952], 3], [[3953, 3954], 2], [3955, 1, "\u0F71\u0F72"], [3956, 2], [3957, 1, "\u0F71\u0F74"], [3958, 1, "\u0FB2\u0F80"], [3959, 1, "\u0FB2\u0F71\u0F80"], [3960, 1, "\u0FB3\u0F80"], [3961, 1, "\u0FB3\u0F71\u0F80"], [[3962, 3968], 2], [3969, 1, "\u0F71\u0F80"], [[3970, 3972], 2], [3973, 2], [[3974, 3979], 2], [[3980, 3983], 2], [[3984, 3986], 2], [3987, 1, "\u0F92\u0FB7"], [[3988, 3989], 2], [3990, 2], [3991, 2], [3992, 3], [[3993, 3996], 2], [3997, 1, "\u0F9C\u0FB7"], [[3998, 4001], 2], [4002, 1, "\u0FA1\u0FB7"], [[4003, 4006], 2], [4007, 1, "\u0FA6\u0FB7"], [[4008, 4011], 2], [4012, 1, "\u0FAB\u0FB7"], [4013, 2], [[4014, 4016], 2], [[4017, 4023], 2], [4024, 2], [4025, 1, "\u0F90\u0FB5"], [[4026, 4028], 2], [4029, 3], [[4030, 4037], 2], [4038, 2], [[4039, 4044], 2], [4045, 3], [4046, 2], [4047, 2], [[4048, 4049], 2], [[4050, 4052], 2], [[4053, 4056], 2], [[4057, 4058], 2], [[4059, 4095], 3], [[4096, 4129], 2], [4130, 2], [[4131, 4135], 2], [4136, 2], [[4137, 4138], 2], [4139, 2], [[4140, 4146], 2], [[4147, 4149], 2], [[4150, 4153], 2], [[4154, 4159], 2], [[4160, 4169], 2], [[4170, 4175], 2], [[4176, 4185], 2], [[4186, 4249], 2], [[4250, 4253], 2], [[4254, 4255], 2], [[4256, 4293], 3], [4294, 3], [4295, 1, "\u2D27"], [[4296, 4300], 3], [4301, 1, "\u2D2D"], [[4302, 4303], 3], [[4304, 4342], 2], [[4343, 4344], 2], [[4345, 4346], 2], [4347, 2], [4348, 1, "\u10DC"], [[4349, 4351], 2], [[4352, 4441], 2], [[4442, 4446], 2], [[4447, 4448], 3], [[4449, 4514], 2], [[4515, 4519], 2], [[4520, 4601], 2], [[4602, 4607], 2], [[4608, 4614], 2], [4615, 2], [[4616, 4678], 2], [4679, 2], [4680, 2], [4681, 3], [[4682, 4685], 2], [[4686, 4687], 3], [[4688, 4694], 2], [4695, 3], [4696, 2], [4697, 3], [[4698, 4701], 2], [[4702, 4703], 3], [[4704, 4742], 2], [4743, 2], [4744, 2], [4745, 3], [[4746, 4749], 2], [[4750, 4751], 3], [[4752, 4782], 2], [4783, 2], [4784, 2], [4785, 3], [[4786, 4789], 2], [[4790, 4791], 3], [[4792, 4798], 2], [4799, 3], [4800, 2], [4801, 3], [[4802, 4805], 2], [[4806, 4807], 3], [[4808, 4814], 2], [4815, 2], [[4816, 4822], 2], [4823, 3], [[4824, 4846], 2], [4847, 2], [[4848, 4878], 2], [4879, 2], [4880, 2], [4881, 3], [[4882, 4885], 2], [[4886, 4887], 3], [[4888, 4894], 2], [4895, 2], [[4896, 4934], 2], [4935, 2], [[4936, 4954], 2], [[4955, 4956], 3], [[4957, 4958], 2], [4959, 2], [4960, 2], [[4961, 4988], 2], [[4989, 4991], 3], [[4992, 5007], 2], [[5008, 5017], 2], [[5018, 5023], 3], [[5024, 5108], 2], [5109, 2], [[5110, 5111], 3], [5112, 1, "\u13F0"], [5113, 1, "\u13F1"], [5114, 1, "\u13F2"], [5115, 1, "\u13F3"], [5116, 1, "\u13F4"], [5117, 1, "\u13F5"], [[5118, 5119], 3], [5120, 2], [[5121, 5740], 2], [[5741, 5742], 2], [[5743, 5750], 2], [[5751, 5759], 2], [5760, 3], [[5761, 5786], 2], [[5787, 5788], 2], [[5789, 5791], 3], [[5792, 5866], 2], [[5867, 5872], 2], [[5873, 5880], 2], [[5881, 5887], 3], [[5888, 5900], 2], [5901, 2], [[5902, 5908], 2], [5909, 2], [[5910, 5918], 3], [5919, 2], [[5920, 5940], 2], [[5941, 5942], 2], [[5943, 5951], 3], [[5952, 5971], 2], [[5972, 5983], 3], [[5984, 5996], 2], [5997, 3], [[5998, 6e3], 2], [6001, 3], [[6002, 6003], 2], [[6004, 6015], 3], [[6016, 6067], 2], [[6068, 6069], 3], [[6070, 6099], 2], [[6100, 6102], 2], [6103, 2], [[6104, 6107], 2], [6108, 2], [6109, 2], [[6110, 6111], 3], [[6112, 6121], 2], [[6122, 6127], 3], [[6128, 6137], 2], [[6138, 6143], 3], [[6144, 6149], 2], [6150, 3], [[6151, 6154], 2], [[6155, 6157], 7], [6158, 3], [6159, 7], [[6160, 6169], 2], [[6170, 6175], 3], [[6176, 6263], 2], [6264, 2], [[6265, 6271], 3], [[6272, 6313], 2], [6314, 2], [[6315, 6319], 3], [[6320, 6389], 2], [[6390, 6399], 3], [[6400, 6428], 2], [[6429, 6430], 2], [6431, 3], [[6432, 6443], 2], [[6444, 6447], 3], [[6448, 6459], 2], [[6460, 6463], 3], [6464, 2], [[6465, 6467], 3], [[6468, 6469], 2], [[6470, 6509], 2], [[6510, 6511], 3], [[6512, 6516], 2], [[6517, 6527], 3], [[6528, 6569], 2], [[6570, 6571], 2], [[6572, 6575], 3], [[6576, 6601], 2], [[6602, 6607], 3], [[6608, 6617], 2], [6618, 2], [[6619, 6621], 3], [[6622, 6623], 2], [[6624, 6655], 2], [[6656, 6683], 2], [[6684, 6685], 3], [[6686, 6687], 2], [[6688, 6750], 2], [6751, 3], [[6752, 6780], 2], [[6781, 6782], 3], [[6783, 6793], 2], [[6794, 6799], 3], [[6800, 6809], 2], [[6810, 6815], 3], [[6816, 6822], 2], [6823, 2], [[6824, 6829], 2], [[6830, 6831], 3], [[6832, 6845], 2], [6846, 2], [[6847, 6848], 2], [[6849, 6862], 2], [[6863, 6911], 3], [[6912, 6987], 2], [6988, 2], [[6989, 6991], 3], [[6992, 7001], 2], [[7002, 7018], 2], [[7019, 7027], 2], [[7028, 7036], 2], [[7037, 7038], 2], [7039, 3], [[7040, 7082], 2], [[7083, 7085], 2], [[7086, 7097], 2], [[7098, 7103], 2], [[7104, 7155], 2], [[7156, 7163], 3], [[7164, 7167], 2], [[7168, 7223], 2], [[7224, 7226], 3], [[7227, 7231], 2], [[7232, 7241], 2], [[7242, 7244], 3], [[7245, 7293], 2], [[7294, 7295], 2], [7296, 1, "\u0432"], [7297, 1, "\u0434"], [7298, 1, "\u043E"], [7299, 1, "\u0441"], [[7300, 7301], 1, "\u0442"], [7302, 1, "\u044A"], [7303, 1, "\u0463"], [7304, 1, "\uA64B"], [[7305, 7311], 3], [7312, 1, "\u10D0"], [7313, 1, "\u10D1"], [7314, 1, "\u10D2"], [7315, 1, "\u10D3"], [7316, 1, "\u10D4"], [7317, 1, "\u10D5"], [7318, 1, "\u10D6"], [7319, 1, "\u10D7"], [7320, 1, "\u10D8"], [7321, 1, "\u10D9"], [7322, 1, "\u10DA"], [7323, 1, "\u10DB"], [7324, 1, "\u10DC"], [7325, 1, "\u10DD"], [7326, 1, "\u10DE"], [7327, 1, "\u10DF"], [7328, 1, "\u10E0"], [7329, 1, "\u10E1"], [7330, 1, "\u10E2"], [7331, 1, "\u10E3"], [7332, 1, "\u10E4"], [7333, 1, "\u10E5"], [7334, 1, "\u10E6"], [7335, 1, "\u10E7"], [7336, 1, "\u10E8"], [7337, 1, "\u10E9"], [7338, 1, "\u10EA"], [7339, 1, "\u10EB"], [7340, 1, "\u10EC"], [7341, 1, "\u10ED"], [7342, 1, "\u10EE"], [7343, 1, "\u10EF"], [7344, 1, "\u10F0"], [7345, 1, "\u10F1"], [7346, 1, "\u10F2"], [7347, 1, "\u10F3"], [7348, 1, "\u10F4"], [7349, 1, "\u10F5"], [7350, 1, "\u10F6"], [7351, 1, "\u10F7"], [7352, 1, "\u10F8"], [7353, 1, "\u10F9"], [7354, 1, "\u10FA"], [[7355, 7356], 3], [7357, 1, "\u10FD"], [7358, 1, "\u10FE"], [7359, 1, "\u10FF"], [[7360, 7367], 2], [[7368, 7375], 3], [[7376, 7378], 2], [7379, 2], [[7380, 7410], 2], [[7411, 7414], 2], [7415, 2], [[7416, 7417], 2], [7418, 2], [[7419, 7423], 3], [[7424, 7467], 2], [7468, 1, "a"], [7469, 1, "\xE6"], [7470, 1, "b"], [7471, 2], [7472, 1, "d"], [7473, 1, "e"], [7474, 1, "\u01DD"], [7475, 1, "g"], [7476, 1, "h"], [7477, 1, "i"], [7478, 1, "j"], [7479, 1, "k"], [7480, 1, "l"], [7481, 1, "m"], [7482, 1, "n"], [7483, 2], [7484, 1, "o"], [7485, 1, "\u0223"], [7486, 1, "p"], [7487, 1, "r"], [7488, 1, "t"], [7489, 1, "u"], [7490, 1, "w"], [7491, 1, "a"], [7492, 1, "\u0250"], [7493, 1, "\u0251"], [7494, 1, "\u1D02"], [7495, 1, "b"], [7496, 1, "d"], [7497, 1, "e"], [7498, 1, "\u0259"], [7499, 1, "\u025B"], [7500, 1, "\u025C"], [7501, 1, "g"], [7502, 2], [7503, 1, "k"], [7504, 1, "m"], [7505, 1, "\u014B"], [7506, 1, "o"], [7507, 1, "\u0254"], [7508, 1, "\u1D16"], [7509, 1, "\u1D17"], [7510, 1, "p"], [7511, 1, "t"], [7512, 1, "u"], [7513, 1, "\u1D1D"], [7514, 1, "\u026F"], [7515, 1, "v"], [7516, 1, "\u1D25"], [7517, 1, "\u03B2"], [7518, 1, "\u03B3"], [7519, 1, "\u03B4"], [7520, 1, "\u03C6"], [7521, 1, "\u03C7"], [7522, 1, "i"], [7523, 1, "r"], [7524, 1, "u"], [7525, 1, "v"], [7526, 1, "\u03B2"], [7527, 1, "\u03B3"], [7528, 1, "\u03C1"], [7529, 1, "\u03C6"], [7530, 1, "\u03C7"], [7531, 2], [[7532, 7543], 2], [7544, 1, "\u043D"], [[7545, 7578], 2], [7579, 1, "\u0252"], [7580, 1, "c"], [7581, 1, "\u0255"], [7582, 1, "\xF0"], [7583, 1, "\u025C"], [7584, 1, "f"], [7585, 1, "\u025F"], [7586, 1, "\u0261"], [7587, 1, "\u0265"], [7588, 1, "\u0268"], [7589, 1, "\u0269"], [7590, 1, "\u026A"], [7591, 1, "\u1D7B"], [7592, 1, "\u029D"], [7593, 1, "\u026D"], [7594, 1, "\u1D85"], [7595, 1, "\u029F"], [7596, 1, "\u0271"], [7597, 1, "\u0270"], [7598, 1, "\u0272"], [7599, 1, "\u0273"], [7600, 1, "\u0274"], [7601, 1, "\u0275"], [7602, 1, "\u0278"], [7603, 1, "\u0282"], [7604, 1, "\u0283"], [7605, 1, "\u01AB"], [7606, 1, "\u0289"], [7607, 1, "\u028A"], [7608, 1, "\u1D1C"], [7609, 1, "\u028B"], [7610, 1, "\u028C"], [7611, 1, "z"], [7612, 1, "\u0290"], [7613, 1, "\u0291"], [7614, 1, "\u0292"], [7615, 1, "\u03B8"], [[7616, 7619], 2], [[7620, 7626], 2], [[7627, 7654], 2], [[7655, 7669], 2], [[7670, 7673], 2], [7674, 2], [7675, 2], [7676, 2], [7677, 2], [[7678, 7679], 2], [7680, 1, "\u1E01"], [7681, 2], [7682, 1, "\u1E03"], [7683, 2], [7684, 1, "\u1E05"], [7685, 2], [7686, 1, "\u1E07"], [7687, 2], [7688, 1, "\u1E09"], [7689, 2], [7690, 1, "\u1E0B"], [7691, 2], [7692, 1, "\u1E0D"], [7693, 2], [7694, 1, "\u1E0F"], [7695, 2], [7696, 1, "\u1E11"], [7697, 2], [7698, 1, "\u1E13"], [7699, 2], [7700, 1, "\u1E15"], [7701, 2], [7702, 1, "\u1E17"], [7703, 2], [7704, 1, "\u1E19"], [7705, 2], [7706, 1, "\u1E1B"], [7707, 2], [7708, 1, "\u1E1D"], [7709, 2], [7710, 1, "\u1E1F"], [7711, 2], [7712, 1, "\u1E21"], [7713, 2], [7714, 1, "\u1E23"], [7715, 2], [7716, 1, "\u1E25"], [7717, 2], [7718, 1, "\u1E27"], [7719, 2], [7720, 1, "\u1E29"], [7721, 2], [7722, 1, "\u1E2B"], [7723, 2], [7724, 1, "\u1E2D"], [7725, 2], [7726, 1, "\u1E2F"], [7727, 2], [7728, 1, "\u1E31"], [7729, 2], [7730, 1, "\u1E33"], [7731, 2], [7732, 1, "\u1E35"], [7733, 2], [7734, 1, "\u1E37"], [7735, 2], [7736, 1, "\u1E39"], [7737, 2], [7738, 1, "\u1E3B"], [7739, 2], [7740, 1, "\u1E3D"], [7741, 2], [7742, 1, "\u1E3F"], [7743, 2], [7744, 1, "\u1E41"], [7745, 2], [7746, 1, "\u1E43"], [7747, 2], [7748, 1, "\u1E45"], [7749, 2], [7750, 1, "\u1E47"], [7751, 2], [7752, 1, "\u1E49"], [7753, 2], [7754, 1, "\u1E4B"], [7755, 2], [7756, 1, "\u1E4D"], [7757, 2], [7758, 1, "\u1E4F"], [7759, 2], [7760, 1, "\u1E51"], [7761, 2], [7762, 1, "\u1E53"], [7763, 2], [7764, 1, "\u1E55"], [7765, 2], [7766, 1, "\u1E57"], [7767, 2], [7768, 1, "\u1E59"], [7769, 2], [7770, 1, "\u1E5B"], [7771, 2], [7772, 1, "\u1E5D"], [7773, 2], [7774, 1, "\u1E5F"], [7775, 2], [7776, 1, "\u1E61"], [7777, 2], [7778, 1, "\u1E63"], [7779, 2], [7780, 1, "\u1E65"], [7781, 2], [7782, 1, "\u1E67"], [7783, 2], [7784, 1, "\u1E69"], [7785, 2], [7786, 1, "\u1E6B"], [7787, 2], [7788, 1, "\u1E6D"], [7789, 2], [7790, 1, "\u1E6F"], [7791, 2], [7792, 1, "\u1E71"], [7793, 2], [7794, 1, "\u1E73"], [7795, 2], [7796, 1, "\u1E75"], [7797, 2], [7798, 1, "\u1E77"], [7799, 2], [7800, 1, "\u1E79"], [7801, 2], [7802, 1, "\u1E7B"], [7803, 2], [7804, 1, "\u1E7D"], [7805, 2], [7806, 1, "\u1E7F"], [7807, 2], [7808, 1, "\u1E81"], [7809, 2], [7810, 1, "\u1E83"], [7811, 2], [7812, 1, "\u1E85"], [7813, 2], [7814, 1, "\u1E87"], [7815, 2], [7816, 1, "\u1E89"], [7817, 2], [7818, 1, "\u1E8B"], [7819, 2], [7820, 1, "\u1E8D"], [7821, 2], [7822, 1, "\u1E8F"], [7823, 2], [7824, 1, "\u1E91"], [7825, 2], [7826, 1, "\u1E93"], [7827, 2], [7828, 1, "\u1E95"], [[7829, 7833], 2], [7834, 1, "a\u02BE"], [7835, 1, "\u1E61"], [[7836, 7837], 2], [7838, 1, "ss"], [7839, 2], [7840, 1, "\u1EA1"], [7841, 2], [7842, 1, "\u1EA3"], [7843, 2], [7844, 1, "\u1EA5"], [7845, 2], [7846, 1, "\u1EA7"], [7847, 2], [7848, 1, "\u1EA9"], [7849, 2], [7850, 1, "\u1EAB"], [7851, 2], [7852, 1, "\u1EAD"], [7853, 2], [7854, 1, "\u1EAF"], [7855, 2], [7856, 1, "\u1EB1"], [7857, 2], [7858, 1, "\u1EB3"], [7859, 2], [7860, 1, "\u1EB5"], [7861, 2], [7862, 1, "\u1EB7"], [7863, 2], [7864, 1, "\u1EB9"], [7865, 2], [7866, 1, "\u1EBB"], [7867, 2], [7868, 1, "\u1EBD"], [7869, 2], [7870, 1, "\u1EBF"], [7871, 2], [7872, 1, "\u1EC1"], [7873, 2], [7874, 1, "\u1EC3"], [7875, 2], [7876, 1, "\u1EC5"], [7877, 2], [7878, 1, "\u1EC7"], [7879, 2], [7880, 1, "\u1EC9"], [7881, 2], [7882, 1, "\u1ECB"], [7883, 2], [7884, 1, "\u1ECD"], [7885, 2], [7886, 1, "\u1ECF"], [7887, 2], [7888, 1, "\u1ED1"], [7889, 2], [7890, 1, "\u1ED3"], [7891, 2], [7892, 1, "\u1ED5"], [7893, 2], [7894, 1, "\u1ED7"], [7895, 2], [7896, 1, "\u1ED9"], [7897, 2], [7898, 1, "\u1EDB"], [7899, 2], [7900, 1, "\u1EDD"], [7901, 2], [7902, 1, "\u1EDF"], [7903, 2], [7904, 1, "\u1EE1"], [7905, 2], [7906, 1, "\u1EE3"], [7907, 2], [7908, 1, "\u1EE5"], [7909, 2], [7910, 1, "\u1EE7"], [7911, 2], [7912, 1, "\u1EE9"], [7913, 2], [7914, 1, "\u1EEB"], [7915, 2], [7916, 1, "\u1EED"], [7917, 2], [7918, 1, "\u1EEF"], [7919, 2], [7920, 1, "\u1EF1"], [7921, 2], [7922, 1, "\u1EF3"], [7923, 2], [7924, 1, "\u1EF5"], [7925, 2], [7926, 1, "\u1EF7"], [7927, 2], [7928, 1, "\u1EF9"], [7929, 2], [7930, 1, "\u1EFB"], [7931, 2], [7932, 1, "\u1EFD"], [7933, 2], [7934, 1, "\u1EFF"], [7935, 2], [[7936, 7943], 2], [7944, 1, "\u1F00"], [7945, 1, "\u1F01"], [7946, 1, "\u1F02"], [7947, 1, "\u1F03"], [7948, 1, "\u1F04"], [7949, 1, "\u1F05"], [7950, 1, "\u1F06"], [7951, 1, "\u1F07"], [[7952, 7957], 2], [[7958, 7959], 3], [7960, 1, "\u1F10"], [7961, 1, "\u1F11"], [7962, 1, "\u1F12"], [7963, 1, "\u1F13"], [7964, 1, "\u1F14"], [7965, 1, "\u1F15"], [[7966, 7967], 3], [[7968, 7975], 2], [7976, 1, "\u1F20"], [7977, 1, "\u1F21"], [7978, 1, "\u1F22"], [7979, 1, "\u1F23"], [7980, 1, "\u1F24"], [7981, 1, "\u1F25"], [7982, 1, "\u1F26"], [7983, 1, "\u1F27"], [[7984, 7991], 2], [7992, 1, "\u1F30"], [7993, 1, "\u1F31"], [7994, 1, "\u1F32"], [7995, 1, "\u1F33"], [7996, 1, "\u1F34"], [7997, 1, "\u1F35"], [7998, 1, "\u1F36"], [7999, 1, "\u1F37"], [[8e3, 8005], 2], [[8006, 8007], 3], [8008, 1, "\u1F40"], [8009, 1, "\u1F41"], [8010, 1, "\u1F42"], [8011, 1, "\u1F43"], [8012, 1, "\u1F44"], [8013, 1, "\u1F45"], [[8014, 8015], 3], [[8016, 8023], 2], [8024, 3], [8025, 1, "\u1F51"], [8026, 3], [8027, 1, "\u1F53"], [8028, 3], [8029, 1, "\u1F55"], [8030, 3], [8031, 1, "\u1F57"], [[8032, 8039], 2], [8040, 1, "\u1F60"], [8041, 1, "\u1F61"], [8042, 1, "\u1F62"], [8043, 1, "\u1F63"], [8044, 1, "\u1F64"], [8045, 1, "\u1F65"], [8046, 1, "\u1F66"], [8047, 1, "\u1F67"], [8048, 2], [8049, 1, "\u03AC"], [8050, 2], [8051, 1, "\u03AD"], [8052, 2], [8053, 1, "\u03AE"], [8054, 2], [8055, 1, "\u03AF"], [8056, 2], [8057, 1, "\u03CC"], [8058, 2], [8059, 1, "\u03CD"], [8060, 2], [8061, 1, "\u03CE"], [[8062, 8063], 3], [8064, 1, "\u1F00\u03B9"], [8065, 1, "\u1F01\u03B9"], [8066, 1, "\u1F02\u03B9"], [8067, 1, "\u1F03\u03B9"], [8068, 1, "\u1F04\u03B9"], [8069, 1, "\u1F05\u03B9"], [8070, 1, "\u1F06\u03B9"], [8071, 1, "\u1F07\u03B9"], [8072, 1, "\u1F00\u03B9"], [8073, 1, "\u1F01\u03B9"], [8074, 1, "\u1F02\u03B9"], [8075, 1, "\u1F03\u03B9"], [8076, 1, "\u1F04\u03B9"], [8077, 1, "\u1F05\u03B9"], [8078, 1, "\u1F06\u03B9"], [8079, 1, "\u1F07\u03B9"], [8080, 1, "\u1F20\u03B9"], [8081, 1, "\u1F21\u03B9"], [8082, 1, "\u1F22\u03B9"], [8083, 1, "\u1F23\u03B9"], [8084, 1, "\u1F24\u03B9"], [8085, 1, "\u1F25\u03B9"], [8086, 1, "\u1F26\u03B9"], [8087, 1, "\u1F27\u03B9"], [8088, 1, "\u1F20\u03B9"], [8089, 1, "\u1F21\u03B9"], [8090, 1, "\u1F22\u03B9"], [8091, 1, "\u1F23\u03B9"], [8092, 1, "\u1F24\u03B9"], [8093, 1, "\u1F25\u03B9"], [8094, 1, "\u1F26\u03B9"], [8095, 1, "\u1F27\u03B9"], [8096, 1, "\u1F60\u03B9"], [8097, 1, "\u1F61\u03B9"], [8098, 1, "\u1F62\u03B9"], [8099, 1, "\u1F63\u03B9"], [8100, 1, "\u1F64\u03B9"], [8101, 1, "\u1F65\u03B9"], [8102, 1, "\u1F66\u03B9"], [8103, 1, "\u1F67\u03B9"], [8104, 1, "\u1F60\u03B9"], [8105, 1, "\u1F61\u03B9"], [8106, 1, "\u1F62\u03B9"], [8107, 1, "\u1F63\u03B9"], [8108, 1, "\u1F64\u03B9"], [8109, 1, "\u1F65\u03B9"], [8110, 1, "\u1F66\u03B9"], [8111, 1, "\u1F67\u03B9"], [[8112, 8113], 2], [8114, 1, "\u1F70\u03B9"], [8115, 1, "\u03B1\u03B9"], [8116, 1, "\u03AC\u03B9"], [8117, 3], [8118, 2], [8119, 1, "\u1FB6\u03B9"], [8120, 1, "\u1FB0"], [8121, 1, "\u1FB1"], [8122, 1, "\u1F70"], [8123, 1, "\u03AC"], [8124, 1, "\u03B1\u03B9"], [8125, 5, " \u0313"], [8126, 1, "\u03B9"], [8127, 5, " \u0313"], [8128, 5, " \u0342"], [8129, 5, " \u0308\u0342"], [8130, 1, "\u1F74\u03B9"], [8131, 1, "\u03B7\u03B9"], [8132, 1, "\u03AE\u03B9"], [8133, 3], [8134, 2], [8135, 1, "\u1FC6\u03B9"], [8136, 1, "\u1F72"], [8137, 1, "\u03AD"], [8138, 1, "\u1F74"], [8139, 1, "\u03AE"], [8140, 1, "\u03B7\u03B9"], [8141, 5, " \u0313\u0300"], [8142, 5, " \u0313\u0301"], [8143, 5, " \u0313\u0342"], [[8144, 8146], 2], [8147, 1, "\u0390"], [[8148, 8149], 3], [[8150, 8151], 2], [8152, 1, "\u1FD0"], [8153, 1, "\u1FD1"], [8154, 1, "\u1F76"], [8155, 1, "\u03AF"], [8156, 3], [8157, 5, " \u0314\u0300"], [8158, 5, " \u0314\u0301"], [8159, 5, " \u0314\u0342"], [[8160, 8162], 2], [8163, 1, "\u03B0"], [[8164, 8167], 2], [8168, 1, "\u1FE0"], [8169, 1, "\u1FE1"], [8170, 1, "\u1F7A"], [8171, 1, "\u03CD"], [8172, 1, "\u1FE5"], [8173, 5, " \u0308\u0300"], [8174, 5, " \u0308\u0301"], [8175, 5, "`"], [[8176, 8177], 3], [8178, 1, "\u1F7C\u03B9"], [8179, 1, "\u03C9\u03B9"], [8180, 1, "\u03CE\u03B9"], [8181, 3], [8182, 2], [8183, 1, "\u1FF6\u03B9"], [8184, 1, "\u1F78"], [8185, 1, "\u03CC"], [8186, 1, "\u1F7C"], [8187, 1, "\u03CE"], [8188, 1, "\u03C9\u03B9"], [8189, 5, " \u0301"], [8190, 5, " \u0314"], [8191, 3], [[8192, 8202], 5, " "], [8203, 7], [[8204, 8205], 6, ""], [[8206, 8207], 3], [8208, 2], [8209, 1, "\u2010"], [[8210, 8214], 2], [8215, 5, " \u0333"], [[8216, 8227], 2], [[8228, 8230], 3], [8231, 2], [[8232, 8238], 3], [8239, 5, " "], [[8240, 8242], 2], [8243, 1, "\u2032\u2032"], [8244, 1, "\u2032\u2032\u2032"], [8245, 2], [8246, 1, "\u2035\u2035"], [8247, 1, "\u2035\u2035\u2035"], [[8248, 8251], 2], [8252, 5, "!!"], [8253, 2], [8254, 5, " \u0305"], [[8255, 8262], 2], [8263, 5, "??"], [8264, 5, "?!"], [8265, 5, "!?"], [[8266, 8269], 2], [[8270, 8274], 2], [[8275, 8276], 2], [[8277, 8278], 2], [8279, 1, "\u2032\u2032\u2032\u2032"], [[8280, 8286], 2], [8287, 5, " "], [8288, 7], [[8289, 8291], 3], [8292, 7], [8293, 3], [[8294, 8297], 3], [[8298, 8303], 3], [8304, 1, "0"], [8305, 1, "i"], [[8306, 8307], 3], [8308, 1, "4"], [8309, 1, "5"], [8310, 1, "6"], [8311, 1, "7"], [8312, 1, "8"], [8313, 1, "9"], [8314, 5, "+"], [8315, 1, "\u2212"], [8316, 5, "="], [8317, 5, "("], [8318, 5, ")"], [8319, 1, "n"], [8320, 1, "0"], [8321, 1, "1"], [8322, 1, "2"], [8323, 1, "3"], [8324, 1, "4"], [8325, 1, "5"], [8326, 1, "6"], [8327, 1, "7"], [8328, 1, "8"], [8329, 1, "9"], [8330, 5, "+"], [8331, 1, "\u2212"], [8332, 5, "="], [8333, 5, "("], [8334, 5, ")"], [8335, 3], [8336, 1, "a"], [8337, 1, "e"], [8338, 1, "o"], [8339, 1, "x"], [8340, 1, "\u0259"], [8341, 1, "h"], [8342, 1, "k"], [8343, 1, "l"], [8344, 1, "m"], [8345, 1, "n"], [8346, 1, "p"], [8347, 1, "s"], [8348, 1, "t"], [[8349, 8351], 3], [[8352, 8359], 2], [8360, 1, "rs"], [[8361, 8362], 2], [8363, 2], [8364, 2], [[8365, 8367], 2], [[8368, 8369], 2], [[8370, 8373], 2], [[8374, 8376], 2], [8377, 2], [8378, 2], [[8379, 8381], 2], [8382, 2], [8383, 2], [8384, 2], [[8385, 8399], 3], [[8400, 8417], 2], [[8418, 8419], 2], [[8420, 8426], 2], [8427, 2], [[8428, 8431], 2], [8432, 2], [[8433, 8447], 3], [8448, 5, "a/c"], [8449, 5, "a/s"], [8450, 1, "c"], [8451, 1, "\xB0c"], [8452, 2], [8453, 5, "c/o"], [8454, 5, "c/u"], [8455, 1, "\u025B"], [8456, 2], [8457, 1, "\xB0f"], [8458, 1, "g"], [[8459, 8462], 1, "h"], [8463, 1, "\u0127"], [[8464, 8465], 1, "i"], [[8466, 8467], 1, "l"], [8468, 2], [8469, 1, "n"], [8470, 1, "no"], [[8471, 8472], 2], [8473, 1, "p"], [8474, 1, "q"], [[8475, 8477], 1, "r"], [[8478, 8479], 2], [8480, 1, "sm"], [8481, 1, "tel"], [8482, 1, "tm"], [8483, 2], [8484, 1, "z"], [8485, 2], [8486, 1, "\u03C9"], [8487, 2], [8488, 1, "z"], [8489, 2], [8490, 1, "k"], [8491, 1, "\xE5"], [8492, 1, "b"], [8493, 1, "c"], [8494, 2], [[8495, 8496], 1, "e"], [8497, 1, "f"], [8498, 3], [8499, 1, "m"], [8500, 1, "o"], [8501, 1, "\u05D0"], [8502, 1, "\u05D1"], [8503, 1, "\u05D2"], [8504, 1, "\u05D3"], [8505, 1, "i"], [8506, 2], [8507, 1, "fax"], [8508, 1, "\u03C0"], [[8509, 8510], 1, "\u03B3"], [8511, 1, "\u03C0"], [8512, 1, "\u2211"], [[8513, 8516], 2], [[8517, 8518], 1, "d"], [8519, 1, "e"], [8520, 1, "i"], [8521, 1, "j"], [[8522, 8523], 2], [8524, 2], [8525, 2], [8526, 2], [8527, 2], [8528, 1, "1\u20447"], [8529, 1, "1\u20449"], [8530, 1, "1\u204410"], [8531, 1, "1\u20443"], [8532, 1, "2\u20443"], [8533, 1, "1\u20445"], [8534, 1, "2\u20445"], [8535, 1, "3\u20445"], [8536, 1, "4\u20445"], [8537, 1, "1\u20446"], [8538, 1, "5\u20446"], [8539, 1, "1\u20448"], [8540, 1, "3\u20448"], [8541, 1, "5\u20448"], [8542, 1, "7\u20448"], [8543, 1, "1\u2044"], [8544, 1, "i"], [8545, 1, "ii"], [8546, 1, "iii"], [8547, 1, "iv"], [8548, 1, "v"], [8549, 1, "vi"], [8550, 1, "vii"], [8551, 1, "viii"], [8552, 1, "ix"], [8553, 1, "x"], [8554, 1, "xi"], [8555, 1, "xii"], [8556, 1, "l"], [8557, 1, "c"], [8558, 1, "d"], [8559, 1, "m"], [8560, 1, "i"], [8561, 1, "ii"], [8562, 1, "iii"], [8563, 1, "iv"], [8564, 1, "v"], [8565, 1, "vi"], [8566, 1, "vii"], [8567, 1, "viii"], [8568, 1, "ix"], [8569, 1, "x"], [8570, 1, "xi"], [8571, 1, "xii"], [8572, 1, "l"], [8573, 1, "c"], [8574, 1, "d"], [8575, 1, "m"], [[8576, 8578], 2], [8579, 3], [8580, 2], [[8581, 8584], 2], [8585, 1, "0\u20443"], [[8586, 8587], 2], [[8588, 8591], 3], [[8592, 8682], 2], [[8683, 8691], 2], [[8692, 8703], 2], [[8704, 8747], 2], [8748, 1, "\u222B\u222B"], [8749, 1, "\u222B\u222B\u222B"], [8750, 2], [8751, 1, "\u222E\u222E"], [8752, 1, "\u222E\u222E\u222E"], [[8753, 8799], 2], [8800, 4], [[8801, 8813], 2], [[8814, 8815], 4], [[8816, 8945], 2], [[8946, 8959], 2], [8960, 2], [8961, 2], [[8962, 9e3], 2], [9001, 1, "\u3008"], [9002, 1, "\u3009"], [[9003, 9082], 2], [9083, 2], [9084, 2], [[9085, 9114], 2], [[9115, 9166], 2], [[9167, 9168], 2], [[9169, 9179], 2], [[9180, 9191], 2], [9192, 2], [[9193, 9203], 2], [[9204, 9210], 2], [[9211, 9214], 2], [9215, 2], [[9216, 9252], 2], [[9253, 9254], 2], [[9255, 9279], 3], [[9280, 9290], 2], [[9291, 9311], 3], [9312, 1, "1"], [9313, 1, "2"], [9314, 1, "3"], [9315, 1, "4"], [9316, 1, "5"], [9317, 1, "6"], [9318, 1, "7"], [9319, 1, "8"], [9320, 1, "9"], [9321, 1, "10"], [9322, 1, "11"], [9323, 1, "12"], [9324, 1, "13"], [9325, 1, "14"], [9326, 1, "15"], [9327, 1, "16"], [9328, 1, "17"], [9329, 1, "18"], [9330, 1, "19"], [9331, 1, "20"], [9332, 5, "(1)"], [9333, 5, "(2)"], [9334, 5, "(3)"], [9335, 5, "(4)"], [9336, 5, "(5)"], [9337, 5, "(6)"], [9338, 5, "(7)"], [9339, 5, "(8)"], [9340, 5, "(9)"], [9341, 5, "(10)"], [9342, 5, "(11)"], [9343, 5, "(12)"], [9344, 5, "(13)"], [9345, 5, "(14)"], [9346, 5, "(15)"], [9347, 5, "(16)"], [9348, 5, "(17)"], [9349, 5, "(18)"], [9350, 5, "(19)"], [9351, 5, "(20)"], [[9352, 9371], 3], [9372, 5, "(a)"], [9373, 5, "(b)"], [9374, 5, "(c)"], [9375, 5, "(d)"], [9376, 5, "(e)"], [9377, 5, "(f)"], [9378, 5, "(g)"], [9379, 5, "(h)"], [9380, 5, "(i)"], [9381, 5, "(j)"], [9382, 5, "(k)"], [9383, 5, "(l)"], [9384, 5, "(m)"], [9385, 5, "(n)"], [9386, 5, "(o)"], [9387, 5, "(p)"], [9388, 5, "(q)"], [9389, 5, "(r)"], [9390, 5, "(s)"], [9391, 5, "(t)"], [9392, 5, "(u)"], [9393, 5, "(v)"], [9394, 5, "(w)"], [9395, 5, "(x)"], [9396, 5, "(y)"], [9397, 5, "(z)"], [9398, 1, "a"], [9399, 1, "b"], [9400, 1, "c"], [9401, 1, "d"], [9402, 1, "e"], [9403, 1, "f"], [9404, 1, "g"], [9405, 1, "h"], [9406, 1, "i"], [9407, 1, "j"], [9408, 1, "k"], [9409, 1, "l"], [9410, 1, "m"], [9411, 1, "n"], [9412, 1, "o"], [9413, 1, "p"], [9414, 1, "q"], [9415, 1, "r"], [9416, 1, "s"], [9417, 1, "t"], [9418, 1, "u"], [9419, 1, "v"], [9420, 1, "w"], [9421, 1, "x"], [9422, 1, "y"], [9423, 1, "z"], [9424, 1, "a"], [9425, 1, "b"], [9426, 1, "c"], [9427, 1, "d"], [9428, 1, "e"], [9429, 1, "f"], [9430, 1, "g"], [9431, 1, "h"], [9432, 1, "i"], [9433, 1, "j"], [9434, 1, "k"], [9435, 1, "l"], [9436, 1, "m"], [9437, 1, "n"], [9438, 1, "o"], [9439, 1, "p"], [9440, 1, "q"], [9441, 1, "r"], [9442, 1, "s"], [9443, 1, "t"], [9444, 1, "u"], [9445, 1, "v"], [9446, 1, "w"], [9447, 1, "x"], [9448, 1, "y"], [9449, 1, "z"], [9450, 1, "0"], [[9451, 9470], 2], [9471, 2], [[9472, 9621], 2], [[9622, 9631], 2], [[9632, 9711], 2], [[9712, 9719], 2], [[9720, 9727], 2], [[9728, 9747], 2], [[9748, 9749], 2], [[9750, 9751], 2], [9752, 2], [9753, 2], [[9754, 9839], 2], [[9840, 9841], 2], [[9842, 9853], 2], [[9854, 9855], 2], [[9856, 9865], 2], [[9866, 9873], 2], [[9874, 9884], 2], [9885, 2], [[9886, 9887], 2], [[9888, 9889], 2], [[9890, 9905], 2], [9906, 2], [[9907, 9916], 2], [[9917, 9919], 2], [[9920, 9923], 2], [[9924, 9933], 2], [9934, 2], [[9935, 9953], 2], [9954, 2], [9955, 2], [[9956, 9959], 2], [[9960, 9983], 2], [9984, 2], [[9985, 9988], 2], [9989, 2], [[9990, 9993], 2], [[9994, 9995], 2], [[9996, 10023], 2], [10024, 2], [[10025, 10059], 2], [10060, 2], [10061, 2], [10062, 2], [[10063, 10066], 2], [[10067, 10069], 2], [10070, 2], [10071, 2], [[10072, 10078], 2], [[10079, 10080], 2], [[10081, 10087], 2], [[10088, 10101], 2], [[10102, 10132], 2], [[10133, 10135], 2], [[10136, 10159], 2], [10160, 2], [[10161, 10174], 2], [10175, 2], [[10176, 10182], 2], [[10183, 10186], 2], [10187, 2], [10188, 2], [10189, 2], [[10190, 10191], 2], [[10192, 10219], 2], [[10220, 10223], 2], [[10224, 10239], 2], [[10240, 10495], 2], [[10496, 10763], 2], [10764, 1, "\u222B\u222B\u222B\u222B"], [[10765, 10867], 2], [10868, 5, "::="], [10869, 5, "=="], [10870, 5, "==="], [[10871, 10971], 2], [10972, 1, "\u2ADD\u0338"], [[10973, 11007], 2], [[11008, 11021], 2], [[11022, 11027], 2], [[11028, 11034], 2], [[11035, 11039], 2], [[11040, 11043], 2], [[11044, 11084], 2], [[11085, 11087], 2], [[11088, 11092], 2], [[11093, 11097], 2], [[11098, 11123], 2], [[11124, 11125], 3], [[11126, 11157], 2], [11158, 3], [11159, 2], [[11160, 11193], 2], [[11194, 11196], 2], [[11197, 11208], 2], [11209, 2], [[11210, 11217], 2], [11218, 2], [[11219, 11243], 2], [[11244, 11247], 2], [[11248, 11262], 2], [11263, 2], [11264, 1, "\u2C30"], [11265, 1, "\u2C31"], [11266, 1, "\u2C32"], [11267, 1, "\u2C33"], [11268, 1, "\u2C34"], [11269, 1, "\u2C35"], [11270, 1, "\u2C36"], [11271, 1, "\u2C37"], [11272, 1, "\u2C38"], [11273, 1, "\u2C39"], [11274, 1, "\u2C3A"], [11275, 1, "\u2C3B"], [11276, 1, "\u2C3C"], [11277, 1, "\u2C3D"], [11278, 1, "\u2C3E"], [11279, 1, "\u2C3F"], [11280, 1, "\u2C40"], [11281, 1, "\u2C41"], [11282, 1, "\u2C42"], [11283, 1, "\u2C43"], [11284, 1, "\u2C44"], [11285, 1, "\u2C45"], [11286, 1, "\u2C46"], [11287, 1, "\u2C47"], [11288, 1, "\u2C48"], [11289, 1, "\u2C49"], [11290, 1, "\u2C4A"], [11291, 1, "\u2C4B"], [11292, 1, "\u2C4C"], [11293, 1, "\u2C4D"], [11294, 1, "\u2C4E"], [11295, 1, "\u2C4F"], [11296, 1, "\u2C50"], [11297, 1, "\u2C51"], [11298, 1, "\u2C52"], [11299, 1, "\u2C53"], [11300, 1, "\u2C54"], [11301, 1, "\u2C55"], [11302, 1, "\u2C56"], [11303, 1, "\u2C57"], [11304, 1, "\u2C58"], [11305, 1, "\u2C59"], [11306, 1, "\u2C5A"], [11307, 1, "\u2C5B"], [11308, 1, "\u2C5C"], [11309, 1, "\u2C5D"], [11310, 1, "\u2C5E"], [11311, 1, "\u2C5F"], [[11312, 11358], 2], [11359, 2], [11360, 1, "\u2C61"], [11361, 2], [11362, 1, "\u026B"], [11363, 1, "\u1D7D"], [11364, 1, "\u027D"], [[11365, 11366], 2], [11367, 1, "\u2C68"], [11368, 2], [11369, 1, "\u2C6A"], [11370, 2], [11371, 1, "\u2C6C"], [11372, 2], [11373, 1, "\u0251"], [11374, 1, "\u0271"], [11375, 1, "\u0250"], [11376, 1, "\u0252"], [11377, 2], [11378, 1, "\u2C73"], [11379, 2], [11380, 2], [11381, 1, "\u2C76"], [[11382, 11383], 2], [[11384, 11387], 2], [11388, 1, "j"], [11389, 1, "v"], [11390, 1, "\u023F"], [11391, 1, "\u0240"], [11392, 1, "\u2C81"], [11393, 2], [11394, 1, "\u2C83"], [11395, 2], [11396, 1, "\u2C85"], [11397, 2], [11398, 1, "\u2C87"], [11399, 2], [11400, 1, "\u2C89"], [11401, 2], [11402, 1, "\u2C8B"], [11403, 2], [11404, 1, "\u2C8D"], [11405, 2], [11406, 1, "\u2C8F"], [11407, 2], [11408, 1, "\u2C91"], [11409, 2], [11410, 1, "\u2C93"], [11411, 2], [11412, 1, "\u2C95"], [11413, 2], [11414, 1, "\u2C97"], [11415, 2], [11416, 1, "\u2C99"], [11417, 2], [11418, 1, "\u2C9B"], [11419, 2], [11420, 1, "\u2C9D"], [11421, 2], [11422, 1, "\u2C9F"], [11423, 2], [11424, 1, "\u2CA1"], [11425, 2], [11426, 1, "\u2CA3"], [11427, 2], [11428, 1, "\u2CA5"], [11429, 2], [11430, 1, "\u2CA7"], [11431, 2], [11432, 1, "\u2CA9"], [11433, 2], [11434, 1, "\u2CAB"], [11435, 2], [11436, 1, "\u2CAD"], [11437, 2], [11438, 1, "\u2CAF"], [11439, 2], [11440, 1, "\u2CB1"], [11441, 2], [11442, 1, "\u2CB3"], [11443, 2], [11444, 1, "\u2CB5"], [11445, 2], [11446, 1, "\u2CB7"], [11447, 2], [11448, 1, "\u2CB9"], [11449, 2], [11450, 1, "\u2CBB"], [11451, 2], [11452, 1, "\u2CBD"], [11453, 2], [11454, 1, "\u2CBF"], [11455, 2], [11456, 1, "\u2CC1"], [11457, 2], [11458, 1, "\u2CC3"], [11459, 2], [11460, 1, "\u2CC5"], [11461, 2], [11462, 1, "\u2CC7"], [11463, 2], [11464, 1, "\u2CC9"], [11465, 2], [11466, 1, "\u2CCB"], [11467, 2], [11468, 1, "\u2CCD"], [11469, 2], [11470, 1, "\u2CCF"], [11471, 2], [11472, 1, "\u2CD1"], [11473, 2], [11474, 1, "\u2CD3"], [11475, 2], [11476, 1, "\u2CD5"], [11477, 2], [11478, 1, "\u2CD7"], [11479, 2], [11480, 1, "\u2CD9"], [11481, 2], [11482, 1, "\u2CDB"], [11483, 2], [11484, 1, "\u2CDD"], [11485, 2], [11486, 1, "\u2CDF"], [11487, 2], [11488, 1, "\u2CE1"], [11489, 2], [11490, 1, "\u2CE3"], [[11491, 11492], 2], [[11493, 11498], 2], [11499, 1, "\u2CEC"], [11500, 2], [11501, 1, "\u2CEE"], [[11502, 11505], 2], [11506, 1, "\u2CF3"], [11507, 2], [[11508, 11512], 3], [[11513, 11519], 2], [[11520, 11557], 2], [11558, 3], [11559, 2], [[11560, 11564], 3], [11565, 2], [[11566, 11567], 3], [[11568, 11621], 2], [[11622, 11623], 2], [[11624, 11630], 3], [11631, 1, "\u2D61"], [11632, 2], [[11633, 11646], 3], [11647, 2], [[11648, 11670], 2], [[11671, 11679], 3], [[11680, 11686], 2], [11687, 3], [[11688, 11694], 2], [11695, 3], [[11696, 11702], 2], [11703, 3], [[11704, 11710], 2], [11711, 3], [[11712, 11718], 2], [11719, 3], [[11720, 11726], 2], [11727, 3], [[11728, 11734], 2], [11735, 3], [[11736, 11742], 2], [11743, 3], [[11744, 11775], 2], [[11776, 11799], 2], [[11800, 11803], 2], [[11804, 11805], 2], [[11806, 11822], 2], [11823, 2], [11824, 2], [11825, 2], [[11826, 11835], 2], [[11836, 11842], 2], [[11843, 11844], 2], [[11845, 11849], 2], [[11850, 11854], 2], [11855, 2], [[11856, 11858], 2], [[11859, 11869], 2], [[11870, 11903], 3], [[11904, 11929], 2], [11930, 3], [[11931, 11934], 2], [11935, 1, "\u6BCD"], [[11936, 12018], 2], [12019, 1, "\u9F9F"], [[12020, 12031], 3], [12032, 1, "\u4E00"], [12033, 1, "\u4E28"], [12034, 1, "\u4E36"], [12035, 1, "\u4E3F"], [12036, 1, "\u4E59"], [12037, 1, "\u4E85"], [12038, 1, "\u4E8C"], [12039, 1, "\u4EA0"], [12040, 1, "\u4EBA"], [12041, 1, "\u513F"], [12042, 1, "\u5165"], [12043, 1, "\u516B"], [12044, 1, "\u5182"], [12045, 1, "\u5196"], [12046, 1, "\u51AB"], [12047, 1, "\u51E0"], [12048, 1, "\u51F5"], [12049, 1, "\u5200"], [12050, 1, "\u529B"], [12051, 1, "\u52F9"], [12052, 1, "\u5315"], [12053, 1, "\u531A"], [12054, 1, "\u5338"], [12055, 1, "\u5341"], [12056, 1, "\u535C"], [12057, 1, "\u5369"], [12058, 1, "\u5382"], [12059, 1, "\u53B6"], [12060, 1, "\u53C8"], [12061, 1, "\u53E3"], [12062, 1, "\u56D7"], [12063, 1, "\u571F"], [12064, 1, "\u58EB"], [12065, 1, "\u5902"], [12066, 1, "\u590A"], [12067, 1, "\u5915"], [12068, 1, "\u5927"], [12069, 1, "\u5973"], [12070, 1, "\u5B50"], [12071, 1, "\u5B80"], [12072, 1, "\u5BF8"], [12073, 1, "\u5C0F"], [12074, 1, "\u5C22"], [12075, 1, "\u5C38"], [12076, 1, "\u5C6E"], [12077, 1, "\u5C71"], [12078, 1, "\u5DDB"], [12079, 1, "\u5DE5"], [12080, 1, "\u5DF1"], [12081, 1, "\u5DFE"], [12082, 1, "\u5E72"], [12083, 1, "\u5E7A"], [12084, 1, "\u5E7F"], [12085, 1, "\u5EF4"], [12086, 1, "\u5EFE"], [12087, 1, "\u5F0B"], [12088, 1, "\u5F13"], [12089, 1, "\u5F50"], [12090, 1, "\u5F61"], [12091, 1, "\u5F73"], [12092, 1, "\u5FC3"], [12093, 1, "\u6208"], [12094, 1, "\u6236"], [12095, 1, "\u624B"], [12096, 1, "\u652F"], [12097, 1, "\u6534"], [12098, 1, "\u6587"], [12099, 1, "\u6597"], [12100, 1, "\u65A4"], [12101, 1, "\u65B9"], [12102, 1, "\u65E0"], [12103, 1, "\u65E5"], [12104, 1, "\u66F0"], [12105, 1, "\u6708"], [12106, 1, "\u6728"], [12107, 1, "\u6B20"], [12108, 1, "\u6B62"], [12109, 1, "\u6B79"], [12110, 1, "\u6BB3"], [12111, 1, "\u6BCB"], [12112, 1, "\u6BD4"], [12113, 1, "\u6BDB"], [12114, 1, "\u6C0F"], [12115, 1, "\u6C14"], [12116, 1, "\u6C34"], [12117, 1, "\u706B"], [12118, 1, "\u722A"], [12119, 1, "\u7236"], [12120, 1, "\u723B"], [12121, 1, "\u723F"], [12122, 1, "\u7247"], [12123, 1, "\u7259"], [12124, 1, "\u725B"], [12125, 1, "\u72AC"], [12126, 1, "\u7384"], [12127, 1, "\u7389"], [12128, 1, "\u74DC"], [12129, 1, "\u74E6"], [12130, 1, "\u7518"], [12131, 1, "\u751F"], [12132, 1, "\u7528"], [12133, 1, "\u7530"], [12134, 1, "\u758B"], [12135, 1, "\u7592"], [12136, 1, "\u7676"], [12137, 1, "\u767D"], [12138, 1, "\u76AE"], [12139, 1, "\u76BF"], [12140, 1, "\u76EE"], [12141, 1, "\u77DB"], [12142, 1, "\u77E2"], [12143, 1, "\u77F3"], [12144, 1, "\u793A"], [12145, 1, "\u79B8"], [12146, 1, "\u79BE"], [12147, 1, "\u7A74"], [12148, 1, "\u7ACB"], [12149, 1, "\u7AF9"], [12150, 1, "\u7C73"], [12151, 1, "\u7CF8"], [12152, 1, "\u7F36"], [12153, 1, "\u7F51"], [12154, 1, "\u7F8A"], [12155, 1, "\u7FBD"], [12156, 1, "\u8001"], [12157, 1, "\u800C"], [12158, 1, "\u8012"], [12159, 1, "\u8033"], [12160, 1, "\u807F"], [12161, 1, "\u8089"], [12162, 1, "\u81E3"], [12163, 1, "\u81EA"], [12164, 1, "\u81F3"], [12165, 1, "\u81FC"], [12166, 1, "\u820C"], [12167, 1, "\u821B"], [12168, 1, "\u821F"], [12169, 1, "\u826E"], [12170, 1, "\u8272"], [12171, 1, "\u8278"], [12172, 1, "\u864D"], [12173, 1, "\u866B"], [12174, 1, "\u8840"], [12175, 1, "\u884C"], [12176, 1, "\u8863"], [12177, 1, "\u897E"], [12178, 1, "\u898B"], [12179, 1, "\u89D2"], [12180, 1, "\u8A00"], [12181, 1, "\u8C37"], [12182, 1, "\u8C46"], [12183, 1, "\u8C55"], [12184, 1, "\u8C78"], [12185, 1, "\u8C9D"], [12186, 1, "\u8D64"], [12187, 1, "\u8D70"], [12188, 1, "\u8DB3"], [12189, 1, "\u8EAB"], [12190, 1, "\u8ECA"], [12191, 1, "\u8F9B"], [12192, 1, "\u8FB0"], [12193, 1, "\u8FB5"], [12194, 1, "\u9091"], [12195, 1, "\u9149"], [12196, 1, "\u91C6"], [12197, 1, "\u91CC"], [12198, 1, "\u91D1"], [12199, 1, "\u9577"], [12200, 1, "\u9580"], [12201, 1, "\u961C"], [12202, 1, "\u96B6"], [12203, 1, "\u96B9"], [12204, 1, "\u96E8"], [12205, 1, "\u9751"], [12206, 1, "\u975E"], [12207, 1, "\u9762"], [12208, 1, "\u9769"], [12209, 1, "\u97CB"], [12210, 1, "\u97ED"], [12211, 1, "\u97F3"], [12212, 1, "\u9801"], [12213, 1, "\u98A8"], [12214, 1, "\u98DB"], [12215, 1, "\u98DF"], [12216, 1, "\u9996"], [12217, 1, "\u9999"], [12218, 1, "\u99AC"], [12219, 1, "\u9AA8"], [12220, 1, "\u9AD8"], [12221, 1, "\u9ADF"], [12222, 1, "\u9B25"], [12223, 1, "\u9B2F"], [12224, 1, "\u9B32"], [12225, 1, "\u9B3C"], [12226, 1, "\u9B5A"], [12227, 1, "\u9CE5"], [12228, 1, "\u9E75"], [12229, 1, "\u9E7F"], [12230, 1, "\u9EA5"], [12231, 1, "\u9EBB"], [12232, 1, "\u9EC3"], [12233, 1, "\u9ECD"], [12234, 1, "\u9ED1"], [12235, 1, "\u9EF9"], [12236, 1, "\u9EFD"], [12237, 1, "\u9F0E"], [12238, 1, "\u9F13"], [12239, 1, "\u9F20"], [12240, 1, "\u9F3B"], [12241, 1, "\u9F4A"], [12242, 1, "\u9F52"], [12243, 1, "\u9F8D"], [12244, 1, "\u9F9C"], [12245, 1, "\u9FA0"], [[12246, 12271], 3], [[12272, 12283], 3], [[12284, 12287], 3], [12288, 5, " "], [12289, 2], [12290, 1, "."], [[12291, 12292], 2], [[12293, 12295], 2], [[12296, 12329], 2], [[12330, 12333], 2], [[12334, 12341], 2], [12342, 1, "\u3012"], [12343, 2], [12344, 1, "\u5341"], [12345, 1, "\u5344"], [12346, 1, "\u5345"], [12347, 2], [12348, 2], [12349, 2], [12350, 2], [12351, 2], [12352, 3], [[12353, 12436], 2], [[12437, 12438], 2], [[12439, 12440], 3], [[12441, 12442], 2], [12443, 5, " \u3099"], [12444, 5, " \u309A"], [[12445, 12446], 2], [12447, 1, "\u3088\u308A"], [12448, 2], [[12449, 12542], 2], [12543, 1, "\u30B3\u30C8"], [[12544, 12548], 3], [[12549, 12588], 2], [12589, 2], [12590, 2], [12591, 2], [12592, 3], [12593, 1, "\u1100"], [12594, 1, "\u1101"], [12595, 1, "\u11AA"], [12596, 1, "\u1102"], [12597, 1, "\u11AC"], [12598, 1, "\u11AD"], [12599, 1, "\u1103"], [12600, 1, "\u1104"], [12601, 1, "\u1105"], [12602, 1, "\u11B0"], [12603, 1, "\u11B1"], [12604, 1, "\u11B2"], [12605, 1, "\u11B3"], [12606, 1, "\u11B4"], [12607, 1, "\u11B5"], [12608, 1, "\u111A"], [12609, 1, "\u1106"], [12610, 1, "\u1107"], [12611, 1, "\u1108"], [12612, 1, "\u1121"], [12613, 1, "\u1109"], [12614, 1, "\u110A"], [12615, 1, "\u110B"], [12616, 1, "\u110C"], [12617, 1, "\u110D"], [12618, 1, "\u110E"], [12619, 1, "\u110F"], [12620, 1, "\u1110"], [12621, 1, "\u1111"], [12622, 1, "\u1112"], [12623, 1, "\u1161"], [12624, 1, "\u1162"], [12625, 1, "\u1163"], [12626, 1, "\u1164"], [12627, 1, "\u1165"], [12628, 1, "\u1166"], [12629, 1, "\u1167"], [12630, 1, "\u1168"], [12631, 1, "\u1169"], [12632, 1, "\u116A"], [12633, 1, "\u116B"], [12634, 1, "\u116C"], [12635, 1, "\u116D"], [12636, 1, "\u116E"], [12637, 1, "\u116F"], [12638, 1, "\u1170"], [12639, 1, "\u1171"], [12640, 1, "\u1172"], [12641, 1, "\u1173"], [12642, 1, "\u1174"], [12643, 1, "\u1175"], [12644, 3], [12645, 1, "\u1114"], [12646, 1, "\u1115"], [12647, 1, "\u11C7"], [12648, 1, "\u11C8"], [12649, 1, "\u11CC"], [12650, 1, "\u11CE"], [12651, 1, "\u11D3"], [12652, 1, "\u11D7"], [12653, 1, "\u11D9"], [12654, 1, "\u111C"], [12655, 1, "\u11DD"], [12656, 1, "\u11DF"], [12657, 1, "\u111D"], [12658, 1, "\u111E"], [12659, 1, "\u1120"], [12660, 1, "\u1122"], [12661, 1, "\u1123"], [12662, 1, "\u1127"], [12663, 1, "\u1129"], [12664, 1, "\u112B"], [12665, 1, "\u112C"], [12666, 1, "\u112D"], [12667, 1, "\u112E"], [12668, 1, "\u112F"], [12669, 1, "\u1132"], [12670, 1, "\u1136"], [12671, 1, "\u1140"], [12672, 1, "\u1147"], [12673, 1, "\u114C"], [12674, 1, "\u11F1"], [12675, 1, "\u11F2"], [12676, 1, "\u1157"], [12677, 1, "\u1158"], [12678, 1, "\u1159"], [12679, 1, "\u1184"], [12680, 1, "\u1185"], [12681, 1, "\u1188"], [12682, 1, "\u1191"], [12683, 1, "\u1192"], [12684, 1, "\u1194"], [12685, 1, "\u119E"], [12686, 1, "\u11A1"], [12687, 3], [[12688, 12689], 2], [12690, 1, "\u4E00"], [12691, 1, "\u4E8C"], [12692, 1, "\u4E09"], [12693, 1, "\u56DB"], [12694, 1, "\u4E0A"], [12695, 1, "\u4E2D"], [12696, 1, "\u4E0B"], [12697, 1, "\u7532"], [12698, 1, "\u4E59"], [12699, 1, "\u4E19"], [12700, 1, "\u4E01"], [12701, 1, "\u5929"], [12702, 1, "\u5730"], [12703, 1, "\u4EBA"], [[12704, 12727], 2], [[12728, 12730], 2], [[12731, 12735], 2], [[12736, 12751], 2], [[12752, 12771], 2], [[12772, 12783], 3], [[12784, 12799], 2], [12800, 5, "(\u1100)"], [12801, 5, "(\u1102)"], [12802, 5, "(\u1103)"], [12803, 5, "(\u1105)"], [12804, 5, "(\u1106)"], [12805, 5, "(\u1107)"], [12806, 5, "(\u1109)"], [12807, 5, "(\u110B)"], [12808, 5, "(\u110C)"], [12809, 5, "(\u110E)"], [12810, 5, "(\u110F)"], [12811, 5, "(\u1110)"], [12812, 5, "(\u1111)"], [12813, 5, "(\u1112)"], [12814, 5, "(\uAC00)"], [12815, 5, "(\uB098)"], [12816, 5, "(\uB2E4)"], [12817, 5, "(\uB77C)"], [12818, 5, "(\uB9C8)"], [12819, 5, "(\uBC14)"], [12820, 5, "(\uC0AC)"], [12821, 5, "(\uC544)"], [12822, 5, "(\uC790)"], [12823, 5, "(\uCC28)"], [12824, 5, "(\uCE74)"], [12825, 5, "(\uD0C0)"], [12826, 5, "(\uD30C)"], [12827, 5, "(\uD558)"], [12828, 5, "(\uC8FC)"], [12829, 5, "(\uC624\uC804)"], [12830, 5, "(\uC624\uD6C4)"], [12831, 3], [12832, 5, "(\u4E00)"], [12833, 5, "(\u4E8C)"], [12834, 5, "(\u4E09)"], [12835, 5, "(\u56DB)"], [12836, 5, "(\u4E94)"], [12837, 5, "(\u516D)"], [12838, 5, "(\u4E03)"], [12839, 5, "(\u516B)"], [12840, 5, "(\u4E5D)"], [12841, 5, "(\u5341)"], [12842, 5, "(\u6708)"], [12843, 5, "(\u706B)"], [12844, 5, "(\u6C34)"], [12845, 5, "(\u6728)"], [12846, 5, "(\u91D1)"], [12847, 5, "(\u571F)"], [12848, 5, "(\u65E5)"], [12849, 5, "(\u682A)"], [12850, 5, "(\u6709)"], [12851, 5, "(\u793E)"], [12852, 5, "(\u540D)"], [12853, 5, "(\u7279)"], [12854, 5, "(\u8CA1)"], [12855, 5, "(\u795D)"], [12856, 5, "(\u52B4)"], [12857, 5, "(\u4EE3)"], [12858, 5, "(\u547C)"], [12859, 5, "(\u5B66)"], [12860, 5, "(\u76E3)"], [12861, 5, "(\u4F01)"], [12862, 5, "(\u8CC7)"], [12863, 5, "(\u5354)"], [12864, 5, "(\u796D)"], [12865, 5, "(\u4F11)"], [12866, 5, "(\u81EA)"], [12867, 5, "(\u81F3)"], [12868, 1, "\u554F"], [12869, 1, "\u5E7C"], [12870, 1, "\u6587"], [12871, 1, "\u7B8F"], [[12872, 12879], 2], [12880, 1, "pte"], [12881, 1, "21"], [12882, 1, "22"], [12883, 1, "23"], [12884, 1, "24"], [12885, 1, "25"], [12886, 1, "26"], [12887, 1, "27"], [12888, 1, "28"], [12889, 1, "29"], [12890, 1, "30"], [12891, 1, "31"], [12892, 1, "32"], [12893, 1, "33"], [12894, 1, "34"], [12895, 1, "35"], [12896, 1, "\u1100"], [12897, 1, "\u1102"], [12898, 1, "\u1103"], [12899, 1, "\u1105"], [12900, 1, "\u1106"], [12901, 1, "\u1107"], [12902, 1, "\u1109"], [12903, 1, "\u110B"], [12904, 1, "\u110C"], [12905, 1, "\u110E"], [12906, 1, "\u110F"], [12907, 1, "\u1110"], [12908, 1, "\u1111"], [12909, 1, "\u1112"], [12910, 1, "\uAC00"], [12911, 1, "\uB098"], [12912, 1, "\uB2E4"], [12913, 1, "\uB77C"], [12914, 1, "\uB9C8"], [12915, 1, "\uBC14"], [12916, 1, "\uC0AC"], [12917, 1, "\uC544"], [12918, 1, "\uC790"], [12919, 1, "\uCC28"], [12920, 1, "\uCE74"], [12921, 1, "\uD0C0"], [12922, 1, "\uD30C"], [12923, 1, "\uD558"], [12924, 1, "\uCC38\uACE0"], [12925, 1, "\uC8FC\uC758"], [12926, 1, "\uC6B0"], [12927, 2], [12928, 1, "\u4E00"], [12929, 1, "\u4E8C"], [12930, 1, "\u4E09"], [12931, 1, "\u56DB"], [12932, 1, "\u4E94"], [12933, 1, "\u516D"], [12934, 1, "\u4E03"], [12935, 1, "\u516B"], [12936, 1, "\u4E5D"], [12937, 1, "\u5341"], [12938, 1, "\u6708"], [12939, 1, "\u706B"], [12940, 1, "\u6C34"], [12941, 1, "\u6728"], [12942, 1, "\u91D1"], [12943, 1, "\u571F"], [12944, 1, "\u65E5"], [12945, 1, "\u682A"], [12946, 1, "\u6709"], [12947, 1, "\u793E"], [12948, 1, "\u540D"], [12949, 1, "\u7279"], [12950, 1, "\u8CA1"], [12951, 1, "\u795D"], [12952, 1, "\u52B4"], [12953, 1, "\u79D8"], [12954, 1, "\u7537"], [12955, 1, "\u5973"], [12956, 1, "\u9069"], [12957, 1, "\u512A"], [12958, 1, "\u5370"], [12959, 1, "\u6CE8"], [12960, 1, "\u9805"], [12961, 1, "\u4F11"], [12962, 1, "\u5199"], [12963, 1, "\u6B63"], [12964, 1, "\u4E0A"], [12965, 1, "\u4E2D"], [12966, 1, "\u4E0B"], [12967, 1, "\u5DE6"], [12968, 1, "\u53F3"], [12969, 1, "\u533B"], [12970, 1, "\u5B97"], [12971, 1, "\u5B66"], [12972, 1, "\u76E3"], [12973, 1, "\u4F01"], [12974, 1, "\u8CC7"], [12975, 1, "\u5354"], [12976, 1, "\u591C"], [12977, 1, "36"], [12978, 1, "37"], [12979, 1, "38"], [12980, 1, "39"], [12981, 1, "40"], [12982, 1, "41"], [12983, 1, "42"], [12984, 1, "43"], [12985, 1, "44"], [12986, 1, "45"], [12987, 1, "46"], [12988, 1, "47"], [12989, 1, "48"], [12990, 1, "49"], [12991, 1, "50"], [12992, 1, "1\u6708"], [12993, 1, "2\u6708"], [12994, 1, "3\u6708"], [12995, 1, "4\u6708"], [12996, 1, "5\u6708"], [12997, 1, "6\u6708"], [12998, 1, "7\u6708"], [12999, 1, "8\u6708"], [13e3, 1, "9\u6708"], [13001, 1, "10\u6708"], [13002, 1, "11\u6708"], [13003, 1, "12\u6708"], [13004, 1, "hg"], [13005, 1, "erg"], [13006, 1, "ev"], [13007, 1, "ltd"], [13008, 1, "\u30A2"], [13009, 1, "\u30A4"], [13010, 1, "\u30A6"], [13011, 1, "\u30A8"], [13012, 1, "\u30AA"], [13013, 1, "\u30AB"], [13014, 1, "\u30AD"], [13015, 1, "\u30AF"], [13016, 1, "\u30B1"], [13017, 1, "\u30B3"], [13018, 1, "\u30B5"], [13019, 1, "\u30B7"], [13020, 1, "\u30B9"], [13021, 1, "\u30BB"], [13022, 1, "\u30BD"], [13023, 1, "\u30BF"], [13024, 1, "\u30C1"], [13025, 1, "\u30C4"], [13026, 1, "\u30C6"], [13027, 1, "\u30C8"], [13028, 1, "\u30CA"], [13029, 1, "\u30CB"], [13030, 1, "\u30CC"], [13031, 1, "\u30CD"], [13032, 1, "\u30CE"], [13033, 1, "\u30CF"], [13034, 1, "\u30D2"], [13035, 1, "\u30D5"], [13036, 1, "\u30D8"], [13037, 1, "\u30DB"], [13038, 1, "\u30DE"], [13039, 1, "\u30DF"], [13040, 1, "\u30E0"], [13041, 1, "\u30E1"], [13042, 1, "\u30E2"], [13043, 1, "\u30E4"], [13044, 1, "\u30E6"], [13045, 1, "\u30E8"], [13046, 1, "\u30E9"], [13047, 1, "\u30EA"], [13048, 1, "\u30EB"], [13049, 1, "\u30EC"], [13050, 1, "\u30ED"], [13051, 1, "\u30EF"], [13052, 1, "\u30F0"], [13053, 1, "\u30F1"], [13054, 1, "\u30F2"], [13055, 1, "\u4EE4\u548C"], [13056, 1, "\u30A2\u30D1\u30FC\u30C8"], [13057, 1, "\u30A2\u30EB\u30D5\u30A1"], [13058, 1, "\u30A2\u30F3\u30DA\u30A2"], [13059, 1, "\u30A2\u30FC\u30EB"], [13060, 1, "\u30A4\u30CB\u30F3\u30B0"], [13061, 1, "\u30A4\u30F3\u30C1"], [13062, 1, "\u30A6\u30A9\u30F3"], [13063, 1, "\u30A8\u30B9\u30AF\u30FC\u30C9"], [13064, 1, "\u30A8\u30FC\u30AB\u30FC"], [13065, 1, "\u30AA\u30F3\u30B9"], [13066, 1, "\u30AA\u30FC\u30E0"], [13067, 1, "\u30AB\u30A4\u30EA"], [13068, 1, "\u30AB\u30E9\u30C3\u30C8"], [13069, 1, "\u30AB\u30ED\u30EA\u30FC"], [13070, 1, "\u30AC\u30ED\u30F3"], [13071, 1, "\u30AC\u30F3\u30DE"], [13072, 1, "\u30AE\u30AC"], [13073, 1, "\u30AE\u30CB\u30FC"], [13074, 1, "\u30AD\u30E5\u30EA\u30FC"], [13075, 1, "\u30AE\u30EB\u30C0\u30FC"], [13076, 1, "\u30AD\u30ED"], [13077, 1, "\u30AD\u30ED\u30B0\u30E9\u30E0"], [13078, 1, "\u30AD\u30ED\u30E1\u30FC\u30C8\u30EB"], [13079, 1, "\u30AD\u30ED\u30EF\u30C3\u30C8"], [13080, 1, "\u30B0\u30E9\u30E0"], [13081, 1, "\u30B0\u30E9\u30E0\u30C8\u30F3"], [13082, 1, "\u30AF\u30EB\u30BC\u30A4\u30ED"], [13083, 1, "\u30AF\u30ED\u30FC\u30CD"], [13084, 1, "\u30B1\u30FC\u30B9"], [13085, 1, "\u30B3\u30EB\u30CA"], [13086, 1, "\u30B3\u30FC\u30DD"], [13087, 1, "\u30B5\u30A4\u30AF\u30EB"], [13088, 1, "\u30B5\u30F3\u30C1\u30FC\u30E0"], [13089, 1, "\u30B7\u30EA\u30F3\u30B0"], [13090, 1, "\u30BB\u30F3\u30C1"], [13091, 1, "\u30BB\u30F3\u30C8"], [13092, 1, "\u30C0\u30FC\u30B9"], [13093, 1, "\u30C7\u30B7"], [13094, 1, "\u30C9\u30EB"], [13095, 1, "\u30C8\u30F3"], [13096, 1, "\u30CA\u30CE"], [13097, 1, "\u30CE\u30C3\u30C8"], [13098, 1, "\u30CF\u30A4\u30C4"], [13099, 1, "\u30D1\u30FC\u30BB\u30F3\u30C8"], [13100, 1, "\u30D1\u30FC\u30C4"], [13101, 1, "\u30D0\u30FC\u30EC\u30EB"], [13102, 1, "\u30D4\u30A2\u30B9\u30C8\u30EB"], [13103, 1, "\u30D4\u30AF\u30EB"], [13104, 1, "\u30D4\u30B3"], [13105, 1, "\u30D3\u30EB"], [13106, 1, "\u30D5\u30A1\u30E9\u30C3\u30C9"], [13107, 1, "\u30D5\u30A3\u30FC\u30C8"], [13108, 1, "\u30D6\u30C3\u30B7\u30A7\u30EB"], [13109, 1, "\u30D5\u30E9\u30F3"], [13110, 1, "\u30D8\u30AF\u30BF\u30FC\u30EB"], [13111, 1, "\u30DA\u30BD"], [13112, 1, "\u30DA\u30CB\u30D2"], [13113, 1, "\u30D8\u30EB\u30C4"], [13114, 1, "\u30DA\u30F3\u30B9"], [13115, 1, "\u30DA\u30FC\u30B8"], [13116, 1, "\u30D9\u30FC\u30BF"], [13117, 1, "\u30DD\u30A4\u30F3\u30C8"], [13118, 1, "\u30DC\u30EB\u30C8"], [13119, 1, "\u30DB\u30F3"], [13120, 1, "\u30DD\u30F3\u30C9"], [13121, 1, "\u30DB\u30FC\u30EB"], [13122, 1, "\u30DB\u30FC\u30F3"], [13123, 1, "\u30DE\u30A4\u30AF\u30ED"], [13124, 1, "\u30DE\u30A4\u30EB"], [13125, 1, "\u30DE\u30C3\u30CF"], [13126, 1, "\u30DE\u30EB\u30AF"], [13127, 1, "\u30DE\u30F3\u30B7\u30E7\u30F3"], [13128, 1, "\u30DF\u30AF\u30ED\u30F3"], [13129, 1, "\u30DF\u30EA"], [13130, 1, "\u30DF\u30EA\u30D0\u30FC\u30EB"], [13131, 1, "\u30E1\u30AC"], [13132, 1, "\u30E1\u30AC\u30C8\u30F3"], [13133, 1, "\u30E1\u30FC\u30C8\u30EB"], [13134, 1, "\u30E4\u30FC\u30C9"], [13135, 1, "\u30E4\u30FC\u30EB"], [13136, 1, "\u30E6\u30A2\u30F3"], [13137, 1, "\u30EA\u30C3\u30C8\u30EB"], [13138, 1, "\u30EA\u30E9"], [13139, 1, "\u30EB\u30D4\u30FC"], [13140, 1, "\u30EB\u30FC\u30D6\u30EB"], [13141, 1, "\u30EC\u30E0"], [13142, 1, "\u30EC\u30F3\u30C8\u30B2\u30F3"], [13143, 1, "\u30EF\u30C3\u30C8"], [13144, 1, "0\u70B9"], [13145, 1, "1\u70B9"], [13146, 1, "2\u70B9"], [13147, 1, "3\u70B9"], [13148, 1, "4\u70B9"], [13149, 1, "5\u70B9"], [13150, 1, "6\u70B9"], [13151, 1, "7\u70B9"], [13152, 1, "8\u70B9"], [13153, 1, "9\u70B9"], [13154, 1, "10\u70B9"], [13155, 1, "11\u70B9"], [13156, 1, "12\u70B9"], [13157, 1, "13\u70B9"], [13158, 1, "14\u70B9"], [13159, 1, "15\u70B9"], [13160, 1, "16\u70B9"], [13161, 1, "17\u70B9"], [13162, 1, "18\u70B9"], [13163, 1, "19\u70B9"], [13164, 1, "20\u70B9"], [13165, 1, "21\u70B9"], [13166, 1, "22\u70B9"], [13167, 1, "23\u70B9"], [13168, 1, "24\u70B9"], [13169, 1, "hpa"], [13170, 1, "da"], [13171, 1, "au"], [13172, 1, "bar"], [13173, 1, "ov"], [13174, 1, "pc"], [13175, 1, "dm"], [13176, 1, "dm2"], [13177, 1, "dm3"], [13178, 1, "iu"], [13179, 1, "\u5E73\u6210"], [13180, 1, "\u662D\u548C"], [13181, 1, "\u5927\u6B63"], [13182, 1, "\u660E\u6CBB"], [13183, 1, "\u682A\u5F0F\u4F1A\u793E"], [13184, 1, "pa"], [13185, 1, "na"], [13186, 1, "\u03BCa"], [13187, 1, "ma"], [13188, 1, "ka"], [13189, 1, "kb"], [13190, 1, "mb"], [13191, 1, "gb"], [13192, 1, "cal"], [13193, 1, "kcal"], [13194, 1, "pf"], [13195, 1, "nf"], [13196, 1, "\u03BCf"], [13197, 1, "\u03BCg"], [13198, 1, "mg"], [13199, 1, "kg"], [13200, 1, "hz"], [13201, 1, "khz"], [13202, 1, "mhz"], [13203, 1, "ghz"], [13204, 1, "thz"], [13205, 1, "\u03BCl"], [13206, 1, "ml"], [13207, 1, "dl"], [13208, 1, "kl"], [13209, 1, "fm"], [13210, 1, "nm"], [13211, 1, "\u03BCm"], [13212, 1, "mm"], [13213, 1, "cm"], [13214, 1, "km"], [13215, 1, "mm2"], [13216, 1, "cm2"], [13217, 1, "m2"], [13218, 1, "km2"], [13219, 1, "mm3"], [13220, 1, "cm3"], [13221, 1, "m3"], [13222, 1, "km3"], [13223, 1, "m\u2215s"], [13224, 1, "m\u2215s2"], [13225, 1, "pa"], [13226, 1, "kpa"], [13227, 1, "mpa"], [13228, 1, "gpa"], [13229, 1, "rad"], [13230, 1, "rad\u2215s"], [13231, 1, "rad\u2215s2"], [13232, 1, "ps"], [13233, 1, "ns"], [13234, 1, "\u03BCs"], [13235, 1, "ms"], [13236, 1, "pv"], [13237, 1, "nv"], [13238, 1, "\u03BCv"], [13239, 1, "mv"], [13240, 1, "kv"], [13241, 1, "mv"], [13242, 1, "pw"], [13243, 1, "nw"], [13244, 1, "\u03BCw"], [13245, 1, "mw"], [13246, 1, "kw"], [13247, 1, "mw"], [13248, 1, "k\u03C9"], [13249, 1, "m\u03C9"], [13250, 3], [13251, 1, "bq"], [13252, 1, "cc"], [13253, 1, "cd"], [13254, 1, "c\u2215kg"], [13255, 3], [13256, 1, "db"], [13257, 1, "gy"], [13258, 1, "ha"], [13259, 1, "hp"], [13260, 1, "in"], [13261, 1, "kk"], [13262, 1, "km"], [13263, 1, "kt"], [13264, 1, "lm"], [13265, 1, "ln"], [13266, 1, "log"], [13267, 1, "lx"], [13268, 1, "mb"], [13269, 1, "mil"], [13270, 1, "mol"], [13271, 1, "ph"], [13272, 3], [13273, 1, "ppm"], [13274, 1, "pr"], [13275, 1, "sr"], [13276, 1, "sv"], [13277, 1, "wb"], [13278, 1, "v\u2215m"], [13279, 1, "a\u2215m"], [13280, 1, "1\u65E5"], [13281, 1, "2\u65E5"], [13282, 1, "3\u65E5"], [13283, 1, "4\u65E5"], [13284, 1, "5\u65E5"], [13285, 1, "6\u65E5"], [13286, 1, "7\u65E5"], [13287, 1, "8\u65E5"], [13288, 1, "9\u65E5"], [13289, 1, "10\u65E5"], [13290, 1, "11\u65E5"], [13291, 1, "12\u65E5"], [13292, 1, "13\u65E5"], [13293, 1, "14\u65E5"], [13294, 1, "15\u65E5"], [13295, 1, "16\u65E5"], [13296, 1, "17\u65E5"], [13297, 1, "18\u65E5"], [13298, 1, "19\u65E5"], [13299, 1, "20\u65E5"], [13300, 1, "21\u65E5"], [13301, 1, "22\u65E5"], [13302, 1, "23\u65E5"], [13303, 1, "24\u65E5"], [13304, 1, "25\u65E5"], [13305, 1, "26\u65E5"], [13306, 1, "27\u65E5"], [13307, 1, "28\u65E5"], [13308, 1, "29\u65E5"], [13309, 1, "30\u65E5"], [13310, 1, "31\u65E5"], [13311, 1, "gal"], [[13312, 19893], 2], [[19894, 19903], 2], [[19904, 19967], 2], [[19968, 40869], 2], [[40870, 40891], 2], [[40892, 40899], 2], [[40900, 40907], 2], [40908, 2], [[40909, 40917], 2], [[40918, 40938], 2], [[40939, 40943], 2], [[40944, 40956], 2], [[40957, 40959], 2], [[40960, 42124], 2], [[42125, 42127], 3], [[42128, 42145], 2], [[42146, 42147], 2], [[42148, 42163], 2], [42164, 2], [[42165, 42176], 2], [42177, 2], [[42178, 42180], 2], [42181, 2], [42182, 2], [[42183, 42191], 3], [[42192, 42237], 2], [[42238, 42239], 2], [[42240, 42508], 2], [[42509, 42511], 2], [[42512, 42539], 2], [[42540, 42559], 3], [42560, 1, "\uA641"], [42561, 2], [42562, 1, "\uA643"], [42563, 2], [42564, 1, "\uA645"], [42565, 2], [42566, 1, "\uA647"], [42567, 2], [42568, 1, "\uA649"], [42569, 2], [42570, 1, "\uA64B"], [42571, 2], [42572, 1, "\uA64D"], [42573, 2], [42574, 1, "\uA64F"], [42575, 2], [42576, 1, "\uA651"], [42577, 2], [42578, 1, "\uA653"], [42579, 2], [42580, 1, "\uA655"], [42581, 2], [42582, 1, "\uA657"], [42583, 2], [42584, 1, "\uA659"], [42585, 2], [42586, 1, "\uA65B"], [42587, 2], [42588, 1, "\uA65D"], [42589, 2], [42590, 1, "\uA65F"], [42591, 2], [42592, 1, "\uA661"], [42593, 2], [42594, 1, "\uA663"], [42595, 2], [42596, 1, "\uA665"], [42597, 2], [42598, 1, "\uA667"], [42599, 2], [42600, 1, "\uA669"], [42601, 2], [42602, 1, "\uA66B"], [42603, 2], [42604, 1, "\uA66D"], [[42605, 42607], 2], [[42608, 42611], 2], [[42612, 42619], 2], [[42620, 42621], 2], [42622, 2], [42623, 2], [42624, 1, "\uA681"], [42625, 2], [42626, 1, "\uA683"], [42627, 2], [42628, 1, "\uA685"], [42629, 2], [42630, 1, "\uA687"], [42631, 2], [42632, 1, "\uA689"], [42633, 2], [42634, 1, "\uA68B"], [42635, 2], [42636, 1, "\uA68D"], [42637, 2], [42638, 1, "\uA68F"], [42639, 2], [42640, 1, "\uA691"], [42641, 2], [42642, 1, "\uA693"], [42643, 2], [42644, 1, "\uA695"], [42645, 2], [42646, 1, "\uA697"], [42647, 2], [42648, 1, "\uA699"], [42649, 2], [42650, 1, "\uA69B"], [42651, 2], [42652, 1, "\u044A"], [42653, 1, "\u044C"], [42654, 2], [42655, 2], [[42656, 42725], 2], [[42726, 42735], 2], [[42736, 42737], 2], [[42738, 42743], 2], [[42744, 42751], 3], [[42752, 42774], 2], [[42775, 42778], 2], [[42779, 42783], 2], [[42784, 42785], 2], [42786, 1, "\uA723"], [42787, 2], [42788, 1, "\uA725"], [42789, 2], [42790, 1, "\uA727"], [42791, 2], [42792, 1, "\uA729"], [42793, 2], [42794, 1, "\uA72B"], [42795, 2], [42796, 1, "\uA72D"], [42797, 2], [42798, 1, "\uA72F"], [[42799, 42801], 2], [42802, 1, "\uA733"], [42803, 2], [42804, 1, "\uA735"], [42805, 2], [42806, 1, "\uA737"], [42807, 2], [42808, 1, "\uA739"], [42809, 2], [42810, 1, "\uA73B"], [42811, 2], [42812, 1, "\uA73D"], [42813, 2], [42814, 1, "\uA73F"], [42815, 2], [42816, 1, "\uA741"], [42817, 2], [42818, 1, "\uA743"], [42819, 2], [42820, 1, "\uA745"], [42821, 2], [42822, 1, "\uA747"], [42823, 2], [42824, 1, "\uA749"], [42825, 2], [42826, 1, "\uA74B"], [42827, 2], [42828, 1, "\uA74D"], [42829, 2], [42830, 1, "\uA74F"], [42831, 2], [42832, 1, "\uA751"], [42833, 2], [42834, 1, "\uA753"], [42835, 2], [42836, 1, "\uA755"], [42837, 2], [42838, 1, "\uA757"], [42839, 2], [42840, 1, "\uA759"], [42841, 2], [42842, 1, "\uA75B"], [42843, 2], [42844, 1, "\uA75D"], [42845, 2], [42846, 1, "\uA75F"], [42847, 2], [42848, 1, "\uA761"], [42849, 2], [42850, 1, "\uA763"], [42851, 2], [42852, 1, "\uA765"], [42853, 2], [42854, 1, "\uA767"], [42855, 2], [42856, 1, "\uA769"], [42857, 2], [42858, 1, "\uA76B"], [42859, 2], [42860, 1, "\uA76D"], [42861, 2], [42862, 1, "\uA76F"], [42863, 2], [42864, 1, "\uA76F"], [[42865, 42872], 2], [42873, 1, "\uA77A"], [42874, 2], [42875, 1, "\uA77C"], [42876, 2], [42877, 1, "\u1D79"], [42878, 1, "\uA77F"], [42879, 2], [42880, 1, "\uA781"], [42881, 2], [42882, 1, "\uA783"], [42883, 2], [42884, 1, "\uA785"], [42885, 2], [42886, 1, "\uA787"], [[42887, 42888], 2], [[42889, 42890], 2], [42891, 1, "\uA78C"], [42892, 2], [42893, 1, "\u0265"], [42894, 2], [42895, 2], [42896, 1, "\uA791"], [42897, 2], [42898, 1, "\uA793"], [42899, 2], [[42900, 42901], 2], [42902, 1, "\uA797"], [42903, 2], [42904, 1, "\uA799"], [42905, 2], [42906, 1, "\uA79B"], [42907, 2], [42908, 1, "\uA79D"], [42909, 2], [42910, 1, "\uA79F"], [42911, 2], [42912, 1, "\uA7A1"], [42913, 2], [42914, 1, "\uA7A3"], [42915, 2], [42916, 1, "\uA7A5"], [42917, 2], [42918, 1, "\uA7A7"], [42919, 2], [42920, 1, "\uA7A9"], [42921, 2], [42922, 1, "\u0266"], [42923, 1, "\u025C"], [42924, 1, "\u0261"], [42925, 1, "\u026C"], [42926, 1, "\u026A"], [42927, 2], [42928, 1, "\u029E"], [42929, 1, "\u0287"], [42930, 1, "\u029D"], [42931, 1, "\uAB53"], [42932, 1, "\uA7B5"], [42933, 2], [42934, 1, "\uA7B7"], [42935, 2], [42936, 1, "\uA7B9"], [42937, 2], [42938, 1, "\uA7BB"], [42939, 2], [42940, 1, "\uA7BD"], [42941, 2], [42942, 1, "\uA7BF"], [42943, 2], [42944, 1, "\uA7C1"], [42945, 2], [42946, 1, "\uA7C3"], [42947, 2], [42948, 1, "\uA794"], [42949, 1, "\u0282"], [42950, 1, "\u1D8E"], [42951, 1, "\uA7C8"], [42952, 2], [42953, 1, "\uA7CA"], [42954, 2], [[42955, 42959], 3], [42960, 1, "\uA7D1"], [42961, 2], [42962, 3], [42963, 2], [42964, 3], [42965, 2], [42966, 1, "\uA7D7"], [42967, 2], [42968, 1, "\uA7D9"], [42969, 2], [[42970, 42993], 3], [42994, 1, "c"], [42995, 1, "f"], [42996, 1, "q"], [42997, 1, "\uA7F6"], [42998, 2], [42999, 2], [43e3, 1, "\u0127"], [43001, 1, "\u0153"], [43002, 2], [[43003, 43007], 2], [[43008, 43047], 2], [[43048, 43051], 2], [43052, 2], [[43053, 43055], 3], [[43056, 43065], 2], [[43066, 43071], 3], [[43072, 43123], 2], [[43124, 43127], 2], [[43128, 43135], 3], [[43136, 43204], 2], [43205, 2], [[43206, 43213], 3], [[43214, 43215], 2], [[43216, 43225], 2], [[43226, 43231], 3], [[43232, 43255], 2], [[43256, 43258], 2], [43259, 2], [43260, 2], [43261, 2], [[43262, 43263], 2], [[43264, 43309], 2], [[43310, 43311], 2], [[43312, 43347], 2], [[43348, 43358], 3], [43359, 2], [[43360, 43388], 2], [[43389, 43391], 3], [[43392, 43456], 2], [[43457, 43469], 2], [43470, 3], [[43471, 43481], 2], [[43482, 43485], 3], [[43486, 43487], 2], [[43488, 43518], 2], [43519, 3], [[43520, 43574], 2], [[43575, 43583], 3], [[43584, 43597], 2], [[43598, 43599], 3], [[43600, 43609], 2], [[43610, 43611], 3], [[43612, 43615], 2], [[43616, 43638], 2], [[43639, 43641], 2], [[43642, 43643], 2], [[43644, 43647], 2], [[43648, 43714], 2], [[43715, 43738], 3], [[43739, 43741], 2], [[43742, 43743], 2], [[43744, 43759], 2], [[43760, 43761], 2], [[43762, 43766], 2], [[43767, 43776], 3], [[43777, 43782], 2], [[43783, 43784], 3], [[43785, 43790], 2], [[43791, 43792], 3], [[43793, 43798], 2], [[43799, 43807], 3], [[43808, 43814], 2], [43815, 3], [[43816, 43822], 2], [43823, 3], [[43824, 43866], 2], [43867, 2], [43868, 1, "\uA727"], [43869, 1, "\uAB37"], [43870, 1, "\u026B"], [43871, 1, "\uAB52"], [[43872, 43875], 2], [[43876, 43877], 2], [[43878, 43879], 2], [43880, 2], [43881, 1, "\u028D"], [[43882, 43883], 2], [[43884, 43887], 3], [43888, 1, "\u13A0"], [43889, 1, "\u13A1"], [43890, 1, "\u13A2"], [43891, 1, "\u13A3"], [43892, 1, "\u13A4"], [43893, 1, "\u13A5"], [43894, 1, "\u13A6"], [43895, 1, "\u13A7"], [43896, 1, "\u13A8"], [43897, 1, "\u13A9"], [43898, 1, "\u13AA"], [43899, 1, "\u13AB"], [43900, 1, "\u13AC"], [43901, 1, "\u13AD"], [43902, 1, "\u13AE"], [43903, 1, "\u13AF"], [43904, 1, "\u13B0"], [43905, 1, "\u13B1"], [43906, 1, "\u13B2"], [43907, 1, "\u13B3"], [43908, 1, "\u13B4"], [43909, 1, "\u13B5"], [43910, 1, "\u13B6"], [43911, 1, "\u13B7"], [43912, 1, "\u13B8"], [43913, 1, "\u13B9"], [43914, 1, "\u13BA"], [43915, 1, "\u13BB"], [43916, 1, "\u13BC"], [43917, 1, "\u13BD"], [43918, 1, "\u13BE"], [43919, 1, "\u13BF"], [43920, 1, "\u13C0"], [43921, 1, "\u13C1"], [43922, 1, "\u13C2"], [43923, 1, "\u13C3"], [43924, 1, "\u13C4"], [43925, 1, "\u13C5"], [43926, 1, "\u13C6"], [43927, 1, "\u13C7"], [43928, 1, "\u13C8"], [43929, 1, "\u13C9"], [43930, 1, "\u13CA"], [43931, 1, "\u13CB"], [43932, 1, "\u13CC"], [43933, 1, "\u13CD"], [43934, 1, "\u13CE"], [43935, 1, "\u13CF"], [43936, 1, "\u13D0"], [43937, 1, "\u13D1"], [43938, 1, "\u13D2"], [43939, 1, "\u13D3"], [43940, 1, "\u13D4"], [43941, 1, "\u13D5"], [43942, 1, "\u13D6"], [43943, 1, "\u13D7"], [43944, 1, "\u13D8"], [43945, 1, "\u13D9"], [43946, 1, "\u13DA"], [43947, 1, "\u13DB"], [43948, 1, "\u13DC"], [43949, 1, "\u13DD"], [43950, 1, "\u13DE"], [43951, 1, "\u13DF"], [43952, 1, "\u13E0"], [43953, 1, "\u13E1"], [43954, 1, "\u13E2"], [43955, 1, "\u13E3"], [43956, 1, "\u13E4"], [43957, 1, "\u13E5"], [43958, 1, "\u13E6"], [43959, 1, "\u13E7"], [43960, 1, "\u13E8"], [43961, 1, "\u13E9"], [43962, 1, "\u13EA"], [43963, 1, "\u13EB"], [43964, 1, "\u13EC"], [43965, 1, "\u13ED"], [43966, 1, "\u13EE"], [43967, 1, "\u13EF"], [[43968, 44010], 2], [44011, 2], [[44012, 44013], 2], [[44014, 44015], 3], [[44016, 44025], 2], [[44026, 44031], 3], [[44032, 55203], 2], [[55204, 55215], 3], [[55216, 55238], 2], [[55239, 55242], 3], [[55243, 55291], 2], [[55292, 55295], 3], [[55296, 57343], 3], [[57344, 63743], 3], [63744, 1, "\u8C48"], [63745, 1, "\u66F4"], [63746, 1, "\u8ECA"], [63747, 1, "\u8CC8"], [63748, 1, "\u6ED1"], [63749, 1, "\u4E32"], [63750, 1, "\u53E5"], [[63751, 63752], 1, "\u9F9C"], [63753, 1, "\u5951"], [63754, 1, "\u91D1"], [63755, 1, "\u5587"], [63756, 1, "\u5948"], [63757, 1, "\u61F6"], [63758, 1, "\u7669"], [63759, 1, "\u7F85"], [63760, 1, "\u863F"], [63761, 1, "\u87BA"], [63762, 1, "\u88F8"], [63763, 1, "\u908F"], [63764, 1, "\u6A02"], [63765, 1, "\u6D1B"], [63766, 1, "\u70D9"], [63767, 1, "\u73DE"], [63768, 1, "\u843D"], [63769, 1, "\u916A"], [63770, 1, "\u99F1"], [63771, 1, "\u4E82"], [63772, 1, "\u5375"], [63773, 1, "\u6B04"], [63774, 1, "\u721B"], [63775, 1, "\u862D"], [63776, 1, "\u9E1E"], [63777, 1, "\u5D50"], [63778, 1, "\u6FEB"], [63779, 1, "\u85CD"], [63780, 1, "\u8964"], [63781, 1, "\u62C9"], [63782, 1, "\u81D8"], [63783, 1, "\u881F"], [63784, 1, "\u5ECA"], [63785, 1, "\u6717"], [63786, 1, "\u6D6A"], [63787, 1, "\u72FC"], [63788, 1, "\u90CE"], [63789, 1, "\u4F86"], [63790, 1, "\u51B7"], [63791, 1, "\u52DE"], [63792, 1, "\u64C4"], [63793, 1, "\u6AD3"], [63794, 1, "\u7210"], [63795, 1, "\u76E7"], [63796, 1, "\u8001"], [63797, 1, "\u8606"], [63798, 1, "\u865C"], [63799, 1, "\u8DEF"], [63800, 1, "\u9732"], [63801, 1, "\u9B6F"], [63802, 1, "\u9DFA"], [63803, 1, "\u788C"], [63804, 1, "\u797F"], [63805, 1, "\u7DA0"], [63806, 1, "\u83C9"], [63807, 1, "\u9304"], [63808, 1, "\u9E7F"], [63809, 1, "\u8AD6"], [63810, 1, "\u58DF"], [63811, 1, "\u5F04"], [63812, 1, "\u7C60"], [63813, 1, "\u807E"], [63814, 1, "\u7262"], [63815, 1, "\u78CA"], [63816, 1, "\u8CC2"], [63817, 1, "\u96F7"], [63818, 1, "\u58D8"], [63819, 1, "\u5C62"], [63820, 1, "\u6A13"], [63821, 1, "\u6DDA"], [63822, 1, "\u6F0F"], [63823, 1, "\u7D2F"], [63824, 1, "\u7E37"], [63825, 1, "\u964B"], [63826, 1, "\u52D2"], [63827, 1, "\u808B"], [63828, 1, "\u51DC"], [63829, 1, "\u51CC"], [63830, 1, "\u7A1C"], [63831, 1, "\u7DBE"], [63832, 1, "\u83F1"], [63833, 1, "\u9675"], [63834, 1, "\u8B80"], [63835, 1, "\u62CF"], [63836, 1, "\u6A02"], [63837, 1, "\u8AFE"], [63838, 1, "\u4E39"], [63839, 1, "\u5BE7"], [63840, 1, "\u6012"], [63841, 1, "\u7387"], [63842, 1, "\u7570"], [63843, 1, "\u5317"], [63844, 1, "\u78FB"], [63845, 1, "\u4FBF"], [63846, 1, "\u5FA9"], [63847, 1, "\u4E0D"], [63848, 1, "\u6CCC"], [63849, 1, "\u6578"], [63850, 1, "\u7D22"], [63851, 1, "\u53C3"], [63852, 1, "\u585E"], [63853, 1, "\u7701"], [63854, 1, "\u8449"], [63855, 1, "\u8AAA"], [63856, 1, "\u6BBA"], [63857, 1, "\u8FB0"], [63858, 1, "\u6C88"], [63859, 1, "\u62FE"], [63860, 1, "\u82E5"], [63861, 1, "\u63A0"], [63862, 1, "\u7565"], [63863, 1, "\u4EAE"], [63864, 1, "\u5169"], [63865, 1, "\u51C9"], [63866, 1, "\u6881"], [63867, 1, "\u7CE7"], [63868, 1, "\u826F"], [63869, 1, "\u8AD2"], [63870, 1, "\u91CF"], [63871, 1, "\u52F5"], [63872, 1, "\u5442"], [63873, 1, "\u5973"], [63874, 1, "\u5EEC"], [63875, 1, "\u65C5"], [63876, 1, "\u6FFE"], [63877, 1, "\u792A"], [63878, 1, "\u95AD"], [63879, 1, "\u9A6A"], [63880, 1, "\u9E97"], [63881, 1, "\u9ECE"], [63882, 1, "\u529B"], [63883, 1, "\u66C6"], [63884, 1, "\u6B77"], [63885, 1, "\u8F62"], [63886, 1, "\u5E74"], [63887, 1, "\u6190"], [63888, 1, "\u6200"], [63889, 1, "\u649A"], [63890, 1, "\u6F23"], [63891, 1, "\u7149"], [63892, 1, "\u7489"], [63893, 1, "\u79CA"], [63894, 1, "\u7DF4"], [63895, 1, "\u806F"], [63896, 1, "\u8F26"], [63897, 1, "\u84EE"], [63898, 1, "\u9023"], [63899, 1, "\u934A"], [63900, 1, "\u5217"], [63901, 1, "\u52A3"], [63902, 1, "\u54BD"], [63903, 1, "\u70C8"], [63904, 1, "\u88C2"], [63905, 1, "\u8AAA"], [63906, 1, "\u5EC9"], [63907, 1, "\u5FF5"], [63908, 1, "\u637B"], [63909, 1, "\u6BAE"], [63910, 1, "\u7C3E"], [63911, 1, "\u7375"], [63912, 1, "\u4EE4"], [63913, 1, "\u56F9"], [63914, 1, "\u5BE7"], [63915, 1, "\u5DBA"], [63916, 1, "\u601C"], [63917, 1, "\u73B2"], [63918, 1, "\u7469"], [63919, 1, "\u7F9A"], [63920, 1, "\u8046"], [63921, 1, "\u9234"], [63922, 1, "\u96F6"], [63923, 1, "\u9748"], [63924, 1, "\u9818"], [63925, 1, "\u4F8B"], [63926, 1, "\u79AE"], [63927, 1, "\u91B4"], [63928, 1, "\u96B8"], [63929, 1, "\u60E1"], [63930, 1, "\u4E86"], [63931, 1, "\u50DA"], [63932, 1, "\u5BEE"], [63933, 1, "\u5C3F"], [63934, 1, "\u6599"], [63935, 1, "\u6A02"], [63936, 1, "\u71CE"], [63937, 1, "\u7642"], [63938, 1, "\u84FC"], [63939, 1, "\u907C"], [63940, 1, "\u9F8D"], [63941, 1, "\u6688"], [63942, 1, "\u962E"], [63943, 1, "\u5289"], [63944, 1, "\u677B"], [63945, 1, "\u67F3"], [63946, 1, "\u6D41"], [63947, 1, "\u6E9C"], [63948, 1, "\u7409"], [63949, 1, "\u7559"], [63950, 1, "\u786B"], [63951, 1, "\u7D10"], [63952, 1, "\u985E"], [63953, 1, "\u516D"], [63954, 1, "\u622E"], [63955, 1, "\u9678"], [63956, 1, "\u502B"], [63957, 1, "\u5D19"], [63958, 1, "\u6DEA"], [63959, 1, "\u8F2A"], [63960, 1, "\u5F8B"], [63961, 1, "\u6144"], [63962, 1, "\u6817"], [63963, 1, "\u7387"], [63964, 1, "\u9686"], [63965, 1, "\u5229"], [63966, 1, "\u540F"], [63967, 1, "\u5C65"], [63968, 1, "\u6613"], [63969, 1, "\u674E"], [63970, 1, "\u68A8"], [63971, 1, "\u6CE5"], [63972, 1, "\u7406"], [63973, 1, "\u75E2"], [63974, 1, "\u7F79"], [63975, 1, "\u88CF"], [63976, 1, "\u88E1"], [63977, 1, "\u91CC"], [63978, 1, "\u96E2"], [63979, 1, "\u533F"], [63980, 1, "\u6EBA"], [63981, 1, "\u541D"], [63982, 1, "\u71D0"], [63983, 1, "\u7498"], [63984, 1, "\u85FA"], [63985, 1, "\u96A3"], [63986, 1, "\u9C57"], [63987, 1, "\u9E9F"], [63988, 1, "\u6797"], [63989, 1, "\u6DCB"], [63990, 1, "\u81E8"], [63991, 1, "\u7ACB"], [63992, 1, "\u7B20"], [63993, 1, "\u7C92"], [63994, 1, "\u72C0"], [63995, 1, "\u7099"], [63996, 1, "\u8B58"], [63997, 1, "\u4EC0"], [63998, 1, "\u8336"], [63999, 1, "\u523A"], [64e3, 1, "\u5207"], [64001, 1, "\u5EA6"], [64002, 1, "\u62D3"], [64003, 1, "\u7CD6"], [64004, 1, "\u5B85"], [64005, 1, "\u6D1E"], [64006, 1, "\u66B4"], [64007, 1, "\u8F3B"], [64008, 1, "\u884C"], [64009, 1, "\u964D"], [64010, 1, "\u898B"], [64011, 1, "\u5ED3"], [64012, 1, "\u5140"], [64013, 1, "\u55C0"], [[64014, 64015], 2], [64016, 1, "\u585A"], [64017, 2], [64018, 1, "\u6674"], [[64019, 64020], 2], [64021, 1, "\u51DE"], [64022, 1, "\u732A"], [64023, 1, "\u76CA"], [64024, 1, "\u793C"], [64025, 1, "\u795E"], [64026, 1, "\u7965"], [64027, 1, "\u798F"], [64028, 1, "\u9756"], [64029, 1, "\u7CBE"], [64030, 1, "\u7FBD"], [64031, 2], [64032, 1, "\u8612"], [64033, 2], [64034, 1, "\u8AF8"], [[64035, 64036], 2], [64037, 1, "\u9038"], [64038, 1, "\u90FD"], [[64039, 64041], 2], [64042, 1, "\u98EF"], [64043, 1, "\u98FC"], [64044, 1, "\u9928"], [64045, 1, "\u9DB4"], [64046, 1, "\u90DE"], [64047, 1, "\u96B7"], [64048, 1, "\u4FAE"], [64049, 1, "\u50E7"], [64050, 1, "\u514D"], [64051, 1, "\u52C9"], [64052, 1, "\u52E4"], [64053, 1, "\u5351"], [64054, 1, "\u559D"], [64055, 1, "\u5606"], [64056, 1, "\u5668"], [64057, 1, "\u5840"], [64058, 1, "\u58A8"], [64059, 1, "\u5C64"], [64060, 1, "\u5C6E"], [64061, 1, "\u6094"], [64062, 1, "\u6168"], [64063, 1, "\u618E"], [64064, 1, "\u61F2"], [64065, 1, "\u654F"], [64066, 1, "\u65E2"], [64067, 1, "\u6691"], [64068, 1, "\u6885"], [64069, 1, "\u6D77"], [64070, 1, "\u6E1A"], [64071, 1, "\u6F22"], [64072, 1, "\u716E"], [64073, 1, "\u722B"], [64074, 1, "\u7422"], [64075, 1, "\u7891"], [64076, 1, "\u793E"], [64077, 1, "\u7949"], [64078, 1, "\u7948"], [64079, 1, "\u7950"], [64080, 1, "\u7956"], [64081, 1, "\u795D"], [64082, 1, "\u798D"], [64083, 1, "\u798E"], [64084, 1, "\u7A40"], [64085, 1, "\u7A81"], [64086, 1, "\u7BC0"], [64087, 1, "\u7DF4"], [64088, 1, "\u7E09"], [64089, 1, "\u7E41"], [64090, 1, "\u7F72"], [64091, 1, "\u8005"], [64092, 1, "\u81ED"], [[64093, 64094], 1, "\u8279"], [64095, 1, "\u8457"], [64096, 1, "\u8910"], [64097, 1, "\u8996"], [64098, 1, "\u8B01"], [64099, 1, "\u8B39"], [64100, 1, "\u8CD3"], [64101, 1, "\u8D08"], [64102, 1, "\u8FB6"], [64103, 1, "\u9038"], [64104, 1, "\u96E3"], [64105, 1, "\u97FF"], [64106, 1, "\u983B"], [64107, 1, "\u6075"], [64108, 1, "\u{242EE}"], [64109, 1, "\u8218"], [[64110, 64111], 3], [64112, 1, "\u4E26"], [64113, 1, "\u51B5"], [64114, 1, "\u5168"], [64115, 1, "\u4F80"], [64116, 1, "\u5145"], [64117, 1, "\u5180"], [64118, 1, "\u52C7"], [64119, 1, "\u52FA"], [64120, 1, "\u559D"], [64121, 1, "\u5555"], [64122, 1, "\u5599"], [64123, 1, "\u55E2"], [64124, 1, "\u585A"], [64125, 1, "\u58B3"], [64126, 1, "\u5944"], [64127, 1, "\u5954"], [64128, 1, "\u5A62"], [64129, 1, "\u5B28"], [64130, 1, "\u5ED2"], [64131, 1, "\u5ED9"], [64132, 1, "\u5F69"], [64133, 1, "\u5FAD"], [64134, 1, "\u60D8"], [64135, 1, "\u614E"], [64136, 1, "\u6108"], [64137, 1, "\u618E"], [64138, 1, "\u6160"], [64139, 1, "\u61F2"], [64140, 1, "\u6234"], [64141, 1, "\u63C4"], [64142, 1, "\u641C"], [64143, 1, "\u6452"], [64144, 1, "\u6556"], [64145, 1, "\u6674"], [64146, 1, "\u6717"], [64147, 1, "\u671B"], [64148, 1, "\u6756"], [64149, 1, "\u6B79"], [64150, 1, "\u6BBA"], [64151, 1, "\u6D41"], [64152, 1, "\u6EDB"], [64153, 1, "\u6ECB"], [64154, 1, "\u6F22"], [64155, 1, "\u701E"], [64156, 1, "\u716E"], [64157, 1, "\u77A7"], [64158, 1, "\u7235"], [64159, 1, "\u72AF"], [64160, 1, "\u732A"], [64161, 1, "\u7471"], [64162, 1, "\u7506"], [64163, 1, "\u753B"], [64164, 1, "\u761D"], [64165, 1, "\u761F"], [64166, 1, "\u76CA"], [64167, 1, "\u76DB"], [64168, 1, "\u76F4"], [64169, 1, "\u774A"], [64170, 1, "\u7740"], [64171, 1, "\u78CC"], [64172, 1, "\u7AB1"], [64173, 1, "\u7BC0"], [64174, 1, "\u7C7B"], [64175, 1, "\u7D5B"], [64176, 1, "\u7DF4"], [64177, 1, "\u7F3E"], [64178, 1, "\u8005"], [64179, 1, "\u8352"], [64180, 1, "\u83EF"], [64181, 1, "\u8779"], [64182, 1, "\u8941"], [64183, 1, "\u8986"], [64184, 1, "\u8996"], [64185, 1, "\u8ABF"], [64186, 1, "\u8AF8"], [64187, 1, "\u8ACB"], [64188, 1, "\u8B01"], [64189, 1, "\u8AFE"], [64190, 1, "\u8AED"], [64191, 1, "\u8B39"], [64192, 1, "\u8B8A"], [64193, 1, "\u8D08"], [64194, 1, "\u8F38"], [64195, 1, "\u9072"], [64196, 1, "\u9199"], [64197, 1, "\u9276"], [64198, 1, "\u967C"], [64199, 1, "\u96E3"], [64200, 1, "\u9756"], [64201, 1, "\u97DB"], [64202, 1, "\u97FF"], [64203, 1, "\u980B"], [64204, 1, "\u983B"], [64205, 1, "\u9B12"], [64206, 1, "\u9F9C"], [64207, 1, "\u{2284A}"], [64208, 1, "\u{22844}"], [64209, 1, "\u{233D5}"], [64210, 1, "\u3B9D"], [64211, 1, "\u4018"], [64212, 1, "\u4039"], [64213, 1, "\u{25249}"], [64214, 1, "\u{25CD0}"], [64215, 1, "\u{27ED3}"], [64216, 1, "\u9F43"], [64217, 1, "\u9F8E"], [[64218, 64255], 3], [64256, 1, "ff"], [64257, 1, "fi"], [64258, 1, "fl"], [64259, 1, "ffi"], [64260, 1, "ffl"], [[64261, 64262], 1, "st"], [[64263, 64274], 3], [64275, 1, "\u0574\u0576"], [64276, 1, "\u0574\u0565"], [64277, 1, "\u0574\u056B"], [64278, 1, "\u057E\u0576"], [64279, 1, "\u0574\u056D"], [[64280, 64284], 3], [64285, 1, "\u05D9\u05B4"], [64286, 2], [64287, 1, "\u05F2\u05B7"], [64288, 1, "\u05E2"], [64289, 1, "\u05D0"], [64290, 1, "\u05D3"], [64291, 1, "\u05D4"], [64292, 1, "\u05DB"], [64293, 1, "\u05DC"], [64294, 1, "\u05DD"], [64295, 1, "\u05E8"], [64296, 1, "\u05EA"], [64297, 5, "+"], [64298, 1, "\u05E9\u05C1"], [64299, 1, "\u05E9\u05C2"], [64300, 1, "\u05E9\u05BC\u05C1"], [64301, 1, "\u05E9\u05BC\u05C2"], [64302, 1, "\u05D0\u05B7"], [64303, 1, "\u05D0\u05B8"], [64304, 1, "\u05D0\u05BC"], [64305, 1, "\u05D1\u05BC"], [64306, 1, "\u05D2\u05BC"], [64307, 1, "\u05D3\u05BC"], [64308, 1, "\u05D4\u05BC"], [64309, 1, "\u05D5\u05BC"], [64310, 1, "\u05D6\u05BC"], [64311, 3], [64312, 1, "\u05D8\u05BC"], [64313, 1, "\u05D9\u05BC"], [64314, 1, "\u05DA\u05BC"], [64315, 1, "\u05DB\u05BC"], [64316, 1, "\u05DC\u05BC"], [64317, 3], [64318, 1, "\u05DE\u05BC"], [64319, 3], [64320, 1, "\u05E0\u05BC"], [64321, 1, "\u05E1\u05BC"], [64322, 3], [64323, 1, "\u05E3\u05BC"], [64324, 1, "\u05E4\u05BC"], [64325, 3], [64326, 1, "\u05E6\u05BC"], [64327, 1, "\u05E7\u05BC"], [64328, 1, "\u05E8\u05BC"], [64329, 1, "\u05E9\u05BC"], [64330, 1, "\u05EA\u05BC"], [64331, 1, "\u05D5\u05B9"], [64332, 1, "\u05D1\u05BF"], [64333, 1, "\u05DB\u05BF"], [64334, 1, "\u05E4\u05BF"], [64335, 1, "\u05D0\u05DC"], [[64336, 64337], 1, "\u0671"], [[64338, 64341], 1, "\u067B"], [[64342, 64345], 1, "\u067E"], [[64346, 64349], 1, "\u0680"], [[64350, 64353], 1, "\u067A"], [[64354, 64357], 1, "\u067F"], [[64358, 64361], 1, "\u0679"], [[64362, 64365], 1, "\u06A4"], [[64366, 64369], 1, "\u06A6"], [[64370, 64373], 1, "\u0684"], [[64374, 64377], 1, "\u0683"], [[64378, 64381], 1, "\u0686"], [[64382, 64385], 1, "\u0687"], [[64386, 64387], 1, "\u068D"], [[64388, 64389], 1, "\u068C"], [[64390, 64391], 1, "\u068E"], [[64392, 64393], 1, "\u0688"], [[64394, 64395], 1, "\u0698"], [[64396, 64397], 1, "\u0691"], [[64398, 64401], 1, "\u06A9"], [[64402, 64405], 1, "\u06AF"], [[64406, 64409], 1, "\u06B3"], [[64410, 64413], 1, "\u06B1"], [[64414, 64415], 1, "\u06BA"], [[64416, 64419], 1, "\u06BB"], [[64420, 64421], 1, "\u06C0"], [[64422, 64425], 1, "\u06C1"], [[64426, 64429], 1, "\u06BE"], [[64430, 64431], 1, "\u06D2"], [[64432, 64433], 1, "\u06D3"], [[64434, 64449], 2], [64450, 2], [[64451, 64466], 3], [[64467, 64470], 1, "\u06AD"], [[64471, 64472], 1, "\u06C7"], [[64473, 64474], 1, "\u06C6"], [[64475, 64476], 1, "\u06C8"], [64477, 1, "\u06C7\u0674"], [[64478, 64479], 1, "\u06CB"], [[64480, 64481], 1, "\u06C5"], [[64482, 64483], 1, "\u06C9"], [[64484, 64487], 1, "\u06D0"], [[64488, 64489], 1, "\u0649"], [[64490, 64491], 1, "\u0626\u0627"], [[64492, 64493], 1, "\u0626\u06D5"], [[64494, 64495], 1, "\u0626\u0648"], [[64496, 64497], 1, "\u0626\u06C7"], [[64498, 64499], 1, "\u0626\u06C6"], [[64500, 64501], 1, "\u0626\u06C8"], [[64502, 64504], 1, "\u0626\u06D0"], [[64505, 64507], 1, "\u0626\u0649"], [[64508, 64511], 1, "\u06CC"], [64512, 1, "\u0626\u062C"], [64513, 1, "\u0626\u062D"], [64514, 1, "\u0626\u0645"], [64515, 1, "\u0626\u0649"], [64516, 1, "\u0626\u064A"], [64517, 1, "\u0628\u062C"], [64518, 1, "\u0628\u062D"], [64519, 1, "\u0628\u062E"], [64520, 1, "\u0628\u0645"], [64521, 1, "\u0628\u0649"], [64522, 1, "\u0628\u064A"], [64523, 1, "\u062A\u062C"], [64524, 1, "\u062A\u062D"], [64525, 1, "\u062A\u062E"], [64526, 1, "\u062A\u0645"], [64527, 1, "\u062A\u0649"], [64528, 1, "\u062A\u064A"], [64529, 1, "\u062B\u062C"], [64530, 1, "\u062B\u0645"], [64531, 1, "\u062B\u0649"], [64532, 1, "\u062B\u064A"], [64533, 1, "\u062C\u062D"], [64534, 1, "\u062C\u0645"], [64535, 1, "\u062D\u062C"], [64536, 1, "\u062D\u0645"], [64537, 1, "\u062E\u062C"], [64538, 1, "\u062E\u062D"], [64539, 1, "\u062E\u0645"], [64540, 1, "\u0633\u062C"], [64541, 1, "\u0633\u062D"], [64542, 1, "\u0633\u062E"], [64543, 1, "\u0633\u0645"], [64544, 1, "\u0635\u062D"], [64545, 1, "\u0635\u0645"], [64546, 1, "\u0636\u062C"], [64547, 1, "\u0636\u062D"], [64548, 1, "\u0636\u062E"], [64549, 1, "\u0636\u0645"], [64550, 1, "\u0637\u062D"], [64551, 1, "\u0637\u0645"], [64552, 1, "\u0638\u0645"], [64553, 1, "\u0639\u062C"], [64554, 1, "\u0639\u0645"], [64555, 1, "\u063A\u062C"], [64556, 1, "\u063A\u0645"], [64557, 1, "\u0641\u062C"], [64558, 1, "\u0641\u062D"], [64559, 1, "\u0641\u062E"], [64560, 1, "\u0641\u0645"], [64561, 1, "\u0641\u0649"], [64562, 1, "\u0641\u064A"], [64563, 1, "\u0642\u062D"], [64564, 1, "\u0642\u0645"], [64565, 1, "\u0642\u0649"], [64566, 1, "\u0642\u064A"], [64567, 1, "\u0643\u0627"], [64568, 1, "\u0643\u062C"], [64569, 1, "\u0643\u062D"], [64570, 1, "\u0643\u062E"], [64571, 1, "\u0643\u0644"], [64572, 1, "\u0643\u0645"], [64573, 1, "\u0643\u0649"], [64574, 1, "\u0643\u064A"], [64575, 1, "\u0644\u062C"], [64576, 1, "\u0644\u062D"], [64577, 1, "\u0644\u062E"], [64578, 1, "\u0644\u0645"], [64579, 1, "\u0644\u0649"], [64580, 1, "\u0644\u064A"], [64581, 1, "\u0645\u062C"], [64582, 1, "\u0645\u062D"], [64583, 1, "\u0645\u062E"], [64584, 1, "\u0645\u0645"], [64585, 1, "\u0645\u0649"], [64586, 1, "\u0645\u064A"], [64587, 1, "\u0646\u062C"], [64588, 1, "\u0646\u062D"], [64589, 1, "\u0646\u062E"], [64590, 1, "\u0646\u0645"], [64591, 1, "\u0646\u0649"], [64592, 1, "\u0646\u064A"], [64593, 1, "\u0647\u062C"], [64594, 1, "\u0647\u0645"], [64595, 1, "\u0647\u0649"], [64596, 1, "\u0647\u064A"], [64597, 1, "\u064A\u062C"], [64598, 1, "\u064A\u062D"], [64599, 1, "\u064A\u062E"], [64600, 1, "\u064A\u0645"], [64601, 1, "\u064A\u0649"], [64602, 1, "\u064A\u064A"], [64603, 1, "\u0630\u0670"], [64604, 1, "\u0631\u0670"], [64605, 1, "\u0649\u0670"], [64606, 5, " \u064C\u0651"], [64607, 5, " \u064D\u0651"], [64608, 5, " \u064E\u0651"], [64609, 5, " \u064F\u0651"], [64610, 5, " \u0650\u0651"], [64611, 5, " \u0651\u0670"], [64612, 1, "\u0626\u0631"], [64613, 1, "\u0626\u0632"], [64614, 1, "\u0626\u0645"], [64615, 1, "\u0626\u0646"], [64616, 1, "\u0626\u0649"], [64617, 1, "\u0626\u064A"], [64618, 1, "\u0628\u0631"], [64619, 1, "\u0628\u0632"], [64620, 1, "\u0628\u0645"], [64621, 1, "\u0628\u0646"], [64622, 1, "\u0628\u0649"], [64623, 1, "\u0628\u064A"], [64624, 1, "\u062A\u0631"], [64625, 1, "\u062A\u0632"], [64626, 1, "\u062A\u0645"], [64627, 1, "\u062A\u0646"], [64628, 1, "\u062A\u0649"], [64629, 1, "\u062A\u064A"], [64630, 1, "\u062B\u0631"], [64631, 1, "\u062B\u0632"], [64632, 1, "\u062B\u0645"], [64633, 1, "\u062B\u0646"], [64634, 1, "\u062B\u0649"], [64635, 1, "\u062B\u064A"], [64636, 1, "\u0641\u0649"], [64637, 1, "\u0641\u064A"], [64638, 1, "\u0642\u0649"], [64639, 1, "\u0642\u064A"], [64640, 1, "\u0643\u0627"], [64641, 1, "\u0643\u0644"], [64642, 1, "\u0643\u0645"], [64643, 1, "\u0643\u0649"], [64644, 1, "\u0643\u064A"], [64645, 1, "\u0644\u0645"], [64646, 1, "\u0644\u0649"], [64647, 1, "\u0644\u064A"], [64648, 1, "\u0645\u0627"], [64649, 1, "\u0645\u0645"], [64650, 1, "\u0646\u0631"], [64651, 1, "\u0646\u0632"], [64652, 1, "\u0646\u0645"], [64653, 1, "\u0646\u0646"], [64654, 1, "\u0646\u0649"], [64655, 1, "\u0646\u064A"], [64656, 1, "\u0649\u0670"], [64657, 1, "\u064A\u0631"], [64658, 1, "\u064A\u0632"], [64659, 1, "\u064A\u0645"], [64660, 1, "\u064A\u0646"], [64661, 1, "\u064A\u0649"], [64662, 1, "\u064A\u064A"], [64663, 1, "\u0626\u062C"], [64664, 1, "\u0626\u062D"], [64665, 1, "\u0626\u062E"], [64666, 1, "\u0626\u0645"], [64667, 1, "\u0626\u0647"], [64668, 1, "\u0628\u062C"], [64669, 1, "\u0628\u062D"], [64670, 1, "\u0628\u062E"], [64671, 1, "\u0628\u0645"], [64672, 1, "\u0628\u0647"], [64673, 1, "\u062A\u062C"], [64674, 1, "\u062A\u062D"], [64675, 1, "\u062A\u062E"], [64676, 1, "\u062A\u0645"], [64677, 1, "\u062A\u0647"], [64678, 1, "\u062B\u0645"], [64679, 1, "\u062C\u062D"], [64680, 1, "\u062C\u0645"], [64681, 1, "\u062D\u062C"], [64682, 1, "\u062D\u0645"], [64683, 1, "\u062E\u062C"], [64684, 1, "\u062E\u0645"], [64685, 1, "\u0633\u062C"], [64686, 1, "\u0633\u062D"], [64687, 1, "\u0633\u062E"], [64688, 1, "\u0633\u0645"], [64689, 1, "\u0635\u062D"], [64690, 1, "\u0635\u062E"], [64691, 1, "\u0635\u0645"], [64692, 1, "\u0636\u062C"], [64693, 1, "\u0636\u062D"], [64694, 1, "\u0636\u062E"], [64695, 1, "\u0636\u0645"], [64696, 1, "\u0637\u062D"], [64697, 1, "\u0638\u0645"], [64698, 1, "\u0639\u062C"], [64699, 1, "\u0639\u0645"], [64700, 1, "\u063A\u062C"], [64701, 1, "\u063A\u0645"], [64702, 1, "\u0641\u062C"], [64703, 1, "\u0641\u062D"], [64704, 1, "\u0641\u062E"], [64705, 1, "\u0641\u0645"], [64706, 1, "\u0642\u062D"], [64707, 1, "\u0642\u0645"], [64708, 1, "\u0643\u062C"], [64709, 1, "\u0643\u062D"], [64710, 1, "\u0643\u062E"], [64711, 1, "\u0643\u0644"], [64712, 1, "\u0643\u0645"], [64713, 1, "\u0644\u062C"], [64714, 1, "\u0644\u062D"], [64715, 1, "\u0644\u062E"], [64716, 1, "\u0644\u0645"], [64717, 1, "\u0644\u0647"], [64718, 1, "\u0645\u062C"], [64719, 1, "\u0645\u062D"], [64720, 1, "\u0645\u062E"], [64721, 1, "\u0645\u0645"], [64722, 1, "\u0646\u062C"], [64723, 1, "\u0646\u062D"], [64724, 1, "\u0646\u062E"], [64725, 1, "\u0646\u0645"], [64726, 1, "\u0646\u0647"], [64727, 1, "\u0647\u062C"], [64728, 1, "\u0647\u0645"], [64729, 1, "\u0647\u0670"], [64730, 1, "\u064A\u062C"], [64731, 1, "\u064A\u062D"], [64732, 1, "\u064A\u062E"], [64733, 1, "\u064A\u0645"], [64734, 1, "\u064A\u0647"], [64735, 1, "\u0626\u0645"], [64736, 1, "\u0626\u0647"], [64737, 1, "\u0628\u0645"], [64738, 1, "\u0628\u0647"], [64739, 1, "\u062A\u0645"], [64740, 1, "\u062A\u0647"], [64741, 1, "\u062B\u0645"], [64742, 1, "\u062B\u0647"], [64743, 1, "\u0633\u0645"], [64744, 1, "\u0633\u0647"], [64745, 1, "\u0634\u0645"], [64746, 1, "\u0634\u0647"], [64747, 1, "\u0643\u0644"], [64748, 1, "\u0643\u0645"], [64749, 1, "\u0644\u0645"], [64750, 1, "\u0646\u0645"], [64751, 1, "\u0646\u0647"], [64752, 1, "\u064A\u0645"], [64753, 1, "\u064A\u0647"], [64754, 1, "\u0640\u064E\u0651"], [64755, 1, "\u0640\u064F\u0651"], [64756, 1, "\u0640\u0650\u0651"], [64757, 1, "\u0637\u0649"], [64758, 1, "\u0637\u064A"], [64759, 1, "\u0639\u0649"], [64760, 1, "\u0639\u064A"], [64761, 1, "\u063A\u0649"], [64762, 1, "\u063A\u064A"], [64763, 1, "\u0633\u0649"], [64764, 1, "\u0633\u064A"], [64765, 1, "\u0634\u0649"], [64766, 1, "\u0634\u064A"], [64767, 1, "\u062D\u0649"], [64768, 1, "\u062D\u064A"], [64769, 1, "\u062C\u0649"], [64770, 1, "\u062C\u064A"], [64771, 1, "\u062E\u0649"], [64772, 1, "\u062E\u064A"], [64773, 1, "\u0635\u0649"], [64774, 1, "\u0635\u064A"], [64775, 1, "\u0636\u0649"], [64776, 1, "\u0636\u064A"], [64777, 1, "\u0634\u062C"], [64778, 1, "\u0634\u062D"], [64779, 1, "\u0634\u062E"], [64780, 1, "\u0634\u0645"], [64781, 1, "\u0634\u0631"], [64782, 1, "\u0633\u0631"], [64783, 1, "\u0635\u0631"], [64784, 1, "\u0636\u0631"], [64785, 1, "\u0637\u0649"], [64786, 1, "\u0637\u064A"], [64787, 1, "\u0639\u0649"], [64788, 1, "\u0639\u064A"], [64789, 1, "\u063A\u0649"], [64790, 1, "\u063A\u064A"], [64791, 1, "\u0633\u0649"], [64792, 1, "\u0633\u064A"], [64793, 1, "\u0634\u0649"], [64794, 1, "\u0634\u064A"], [64795, 1, "\u062D\u0649"], [64796, 1, "\u062D\u064A"], [64797, 1, "\u062C\u0649"], [64798, 1, "\u062C\u064A"], [64799, 1, "\u062E\u0649"], [64800, 1, "\u062E\u064A"], [64801, 1, "\u0635\u0649"], [64802, 1, "\u0635\u064A"], [64803, 1, "\u0636\u0649"], [64804, 1, "\u0636\u064A"], [64805, 1, "\u0634\u062C"], [64806, 1, "\u0634\u062D"], [64807, 1, "\u0634\u062E"], [64808, 1, "\u0634\u0645"], [64809, 1, "\u0634\u0631"], [64810, 1, "\u0633\u0631"], [64811, 1, "\u0635\u0631"], [64812, 1, "\u0636\u0631"], [64813, 1, "\u0634\u062C"], [64814, 1, "\u0634\u062D"], [64815, 1, "\u0634\u062E"], [64816, 1, "\u0634\u0645"], [64817, 1, "\u0633\u0647"], [64818, 1, "\u0634\u0647"], [64819, 1, "\u0637\u0645"], [64820, 1, "\u0633\u062C"], [64821, 1, "\u0633\u062D"], [64822, 1, "\u0633\u062E"], [64823, 1, "\u0634\u062C"], [64824, 1, "\u0634\u062D"], [64825, 1, "\u0634\u062E"], [64826, 1, "\u0637\u0645"], [64827, 1, "\u0638\u0645"], [[64828, 64829], 1, "\u0627\u064B"], [[64830, 64831], 2], [[64832, 64847], 2], [64848, 1, "\u062A\u062C\u0645"], [[64849, 64850], 1, "\u062A\u062D\u062C"], [64851, 1, "\u062A\u062D\u0645"], [64852, 1, "\u062A\u062E\u0645"], [64853, 1, "\u062A\u0645\u062C"], [64854, 1, "\u062A\u0645\u062D"], [64855, 1, "\u062A\u0645\u062E"], [[64856, 64857], 1, "\u062C\u0645\u062D"], [64858, 1, "\u062D\u0645\u064A"], [64859, 1, "\u062D\u0645\u0649"], [64860, 1, "\u0633\u062D\u062C"], [64861, 1, "\u0633\u062C\u062D"], [64862, 1, "\u0633\u062C\u0649"], [[64863, 64864], 1, "\u0633\u0645\u062D"], [64865, 1, "\u0633\u0645\u062C"], [[64866, 64867], 1, "\u0633\u0645\u0645"], [[64868, 64869], 1, "\u0635\u062D\u062D"], [64870, 1, "\u0635\u0645\u0645"], [[64871, 64872], 1, "\u0634\u062D\u0645"], [64873, 1, "\u0634\u062C\u064A"], [[64874, 64875], 1, "\u0634\u0645\u062E"], [[64876, 64877], 1, "\u0634\u0645\u0645"], [64878, 1, "\u0636\u062D\u0649"], [[64879, 64880], 1, "\u0636\u062E\u0645"], [[64881, 64882], 1, "\u0637\u0645\u062D"], [64883, 1, "\u0637\u0645\u0645"], [64884, 1, "\u0637\u0645\u064A"], [64885, 1, "\u0639\u062C\u0645"], [[64886, 64887], 1, "\u0639\u0645\u0645"], [64888, 1, "\u0639\u0645\u0649"], [64889, 1, "\u063A\u0645\u0645"], [64890, 1, "\u063A\u0645\u064A"], [64891, 1, "\u063A\u0645\u0649"], [[64892, 64893], 1, "\u0641\u062E\u0645"], [64894, 1, "\u0642\u0645\u062D"], [64895, 1, "\u0642\u0645\u0645"], [64896, 1, "\u0644\u062D\u0645"], [64897, 1, "\u0644\u062D\u064A"], [64898, 1, "\u0644\u062D\u0649"], [[64899, 64900], 1, "\u0644\u062C\u062C"], [[64901, 64902], 1, "\u0644\u062E\u0645"], [[64903, 64904], 1, "\u0644\u0645\u062D"], [64905, 1, "\u0645\u062D\u062C"], [64906, 1, "\u0645\u062D\u0645"], [64907, 1, "\u0645\u062D\u064A"], [64908, 1, "\u0645\u062C\u062D"], [64909, 1, "\u0645\u062C\u0645"], [64910, 1, "\u0645\u062E\u062C"], [64911, 1, "\u0645\u062E\u0645"], [[64912, 64913], 3], [64914, 1, "\u0645\u062C\u062E"], [64915, 1, "\u0647\u0645\u062C"], [64916, 1, "\u0647\u0645\u0645"], [64917, 1, "\u0646\u062D\u0645"], [64918, 1, "\u0646\u062D\u0649"], [[64919, 64920], 1, "\u0646\u062C\u0645"], [64921, 1, "\u0646\u062C\u0649"], [64922, 1, "\u0646\u0645\u064A"], [64923, 1, "\u0646\u0645\u0649"], [[64924, 64925], 1, "\u064A\u0645\u0645"], [64926, 1, "\u0628\u062E\u064A"], [64927, 1, "\u062A\u062C\u064A"], [64928, 1, "\u062A\u062C\u0649"], [64929, 1, "\u062A\u062E\u064A"], [64930, 1, "\u062A\u062E\u0649"], [64931, 1, "\u062A\u0645\u064A"], [64932, 1, "\u062A\u0645\u0649"], [64933, 1, "\u062C\u0645\u064A"], [64934, 1, "\u062C\u062D\u0649"], [64935, 1, "\u062C\u0645\u0649"], [64936, 1, "\u0633\u062E\u0649"], [64937, 1, "\u0635\u062D\u064A"], [64938, 1, "\u0634\u062D\u064A"], [64939, 1, "\u0636\u062D\u064A"], [64940, 1, "\u0644\u062C\u064A"], [64941, 1, "\u0644\u0645\u064A"], [64942, 1, "\u064A\u062D\u064A"], [64943, 1, "\u064A\u062C\u064A"], [64944, 1, "\u064A\u0645\u064A"], [64945, 1, "\u0645\u0645\u064A"], [64946, 1, "\u0642\u0645\u064A"], [64947, 1, "\u0646\u062D\u064A"], [64948, 1, "\u0642\u0645\u062D"], [64949, 1, "\u0644\u062D\u0645"], [64950, 1, "\u0639\u0645\u064A"], [64951, 1, "\u0643\u0645\u064A"], [64952, 1, "\u0646\u062C\u062D"], [64953, 1, "\u0645\u062E\u064A"], [64954, 1, "\u0644\u062C\u0645"], [64955, 1, "\u0643\u0645\u0645"], [64956, 1, "\u0644\u062C\u0645"], [64957, 1, "\u0646\u062C\u062D"], [64958, 1, "\u062C\u062D\u064A"], [64959, 1, "\u062D\u062C\u064A"], [64960, 1, "\u0645\u062C\u064A"], [64961, 1, "\u0641\u0645\u064A"], [64962, 1, "\u0628\u062D\u064A"], [64963, 1, "\u0643\u0645\u0645"], [64964, 1, "\u0639\u062C\u0645"], [64965, 1, "\u0635\u0645\u0645"], [64966, 1, "\u0633\u062E\u064A"], [64967, 1, "\u0646\u062C\u064A"], [[64968, 64974], 3], [64975, 2], [[64976, 65007], 3], [65008, 1, "\u0635\u0644\u06D2"], [65009, 1, "\u0642\u0644\u06D2"], [65010, 1, "\u0627\u0644\u0644\u0647"], [65011, 1, "\u0627\u0643\u0628\u0631"], [65012, 1, "\u0645\u062D\u0645\u062F"], [65013, 1, "\u0635\u0644\u0639\u0645"], [65014, 1, "\u0631\u0633\u0648\u0644"], [65015, 1, "\u0639\u0644\u064A\u0647"], [65016, 1, "\u0648\u0633\u0644\u0645"], [65017, 1, "\u0635\u0644\u0649"], [65018, 5, "\u0635\u0644\u0649 \u0627\u0644\u0644\u0647 \u0639\u0644\u064A\u0647 \u0648\u0633\u0644\u0645"], [65019, 5, "\u062C\u0644 \u062C\u0644\u0627\u0644\u0647"], [65020, 1, "\u0631\u06CC\u0627\u0644"], [65021, 2], [[65022, 65023], 2], [[65024, 65039], 7], [65040, 5, ","], [65041, 1, "\u3001"], [65042, 3], [65043, 5, ":"], [65044, 5, ";"], [65045, 5, "!"], [65046, 5, "?"], [65047, 1, "\u3016"], [65048, 1, "\u3017"], [65049, 3], [[65050, 65055], 3], [[65056, 65059], 2], [[65060, 65062], 2], [[65063, 65069], 2], [[65070, 65071], 2], [65072, 3], [65073, 1, "\u2014"], [65074, 1, "\u2013"], [[65075, 65076], 5, "_"], [65077, 5, "("], [65078, 5, ")"], [65079, 5, "{"], [65080, 5, "}"], [65081, 1, "\u3014"], [65082, 1, "\u3015"], [65083, 1, "\u3010"], [65084, 1, "\u3011"], [65085, 1, "\u300A"], [65086, 1, "\u300B"], [65087, 1, "\u3008"], [65088, 1, "\u3009"], [65089, 1, "\u300C"], [65090, 1, "\u300D"], [65091, 1, "\u300E"], [65092, 1, "\u300F"], [[65093, 65094], 2], [65095, 5, "["], [65096, 5, "]"], [[65097, 65100], 5, " \u0305"], [[65101, 65103], 5, "_"], [65104, 5, ","], [65105, 1, "\u3001"], [65106, 3], [65107, 3], [65108, 5, ";"], [65109, 5, ":"], [65110, 5, "?"], [65111, 5, "!"], [65112, 1, "\u2014"], [65113, 5, "("], [65114, 5, ")"], [65115, 5, "{"], [65116, 5, "}"], [65117, 1, "\u3014"], [65118, 1, "\u3015"], [65119, 5, "#"], [65120, 5, "&"], [65121, 5, "*"], [65122, 5, "+"], [65123, 1, "-"], [65124, 5, "<"], [65125, 5, ">"], [65126, 5, "="], [65127, 3], [65128, 5, "\\"], [65129, 5, "$"], [65130, 5, "%"], [65131, 5, "@"], [[65132, 65135], 3], [65136, 5, " \u064B"], [65137, 1, "\u0640\u064B"], [65138, 5, " \u064C"], [65139, 2], [65140, 5, " \u064D"], [65141, 3], [65142, 5, " \u064E"], [65143, 1, "\u0640\u064E"], [65144, 5, " \u064F"], [65145, 1, "\u0640\u064F"], [65146, 5, " \u0650"], [65147, 1, "\u0640\u0650"], [65148, 5, " \u0651"], [65149, 1, "\u0640\u0651"], [65150, 5, " \u0652"], [65151, 1, "\u0640\u0652"], [65152, 1, "\u0621"], [[65153, 65154], 1, "\u0622"], [[65155, 65156], 1, "\u0623"], [[65157, 65158], 1, "\u0624"], [[65159, 65160], 1, "\u0625"], [[65161, 65164], 1, "\u0626"], [[65165, 65166], 1, "\u0627"], [[65167, 65170], 1, "\u0628"], [[65171, 65172], 1, "\u0629"], [[65173, 65176], 1, "\u062A"], [[65177, 65180], 1, "\u062B"], [[65181, 65184], 1, "\u062C"], [[65185, 65188], 1, "\u062D"], [[65189, 65192], 1, "\u062E"], [[65193, 65194], 1, "\u062F"], [[65195, 65196], 1, "\u0630"], [[65197, 65198], 1, "\u0631"], [[65199, 65200], 1, "\u0632"], [[65201, 65204], 1, "\u0633"], [[65205, 65208], 1, "\u0634"], [[65209, 65212], 1, "\u0635"], [[65213, 65216], 1, "\u0636"], [[65217, 65220], 1, "\u0637"], [[65221, 65224], 1, "\u0638"], [[65225, 65228], 1, "\u0639"], [[65229, 65232], 1, "\u063A"], [[65233, 65236], 1, "\u0641"], [[65237, 65240], 1, "\u0642"], [[65241, 65244], 1, "\u0643"], [[65245, 65248], 1, "\u0644"], [[65249, 65252], 1, "\u0645"], [[65253, 65256], 1, "\u0646"], [[65257, 65260], 1, "\u0647"], [[65261, 65262], 1, "\u0648"], [[65263, 65264], 1, "\u0649"], [[65265, 65268], 1, "\u064A"], [[65269, 65270], 1, "\u0644\u0622"], [[65271, 65272], 1, "\u0644\u0623"], [[65273, 65274], 1, "\u0644\u0625"], [[65275, 65276], 1, "\u0644\u0627"], [[65277, 65278], 3], [65279, 7], [65280, 3], [65281, 5, "!"], [65282, 5, '"'], [65283, 5, "#"], [65284, 5, "$"], [65285, 5, "%"], [65286, 5, "&"], [65287, 5, "'"], [65288, 5, "("], [65289, 5, ")"], [65290, 5, "*"], [65291, 5, "+"], [65292, 5, ","], [65293, 1, "-"], [65294, 1, "."], [65295, 5, "/"], [65296, 1, "0"], [65297, 1, "1"], [65298, 1, "2"], [65299, 1, "3"], [65300, 1, "4"], [65301, 1, "5"], [65302, 1, "6"], [65303, 1, "7"], [65304, 1, "8"], [65305, 1, "9"], [65306, 5, ":"], [65307, 5, ";"], [65308, 5, "<"], [65309, 5, "="], [65310, 5, ">"], [65311, 5, "?"], [65312, 5, "@"], [65313, 1, "a"], [65314, 1, "b"], [65315, 1, "c"], [65316, 1, "d"], [65317, 1, "e"], [65318, 1, "f"], [65319, 1, "g"], [65320, 1, "h"], [65321, 1, "i"], [65322, 1, "j"], [65323, 1, "k"], [65324, 1, "l"], [65325, 1, "m"], [65326, 1, "n"], [65327, 1, "o"], [65328, 1, "p"], [65329, 1, "q"], [65330, 1, "r"], [65331, 1, "s"], [65332, 1, "t"], [65333, 1, "u"], [65334, 1, "v"], [65335, 1, "w"], [65336, 1, "x"], [65337, 1, "y"], [65338, 1, "z"], [65339, 5, "["], [65340, 5, "\\"], [65341, 5, "]"], [65342, 5, "^"], [65343, 5, "_"], [65344, 5, "`"], [65345, 1, "a"], [65346, 1, "b"], [65347, 1, "c"], [65348, 1, "d"], [65349, 1, "e"], [65350, 1, "f"], [65351, 1, "g"], [65352, 1, "h"], [65353, 1, "i"], [65354, 1, "j"], [65355, 1, "k"], [65356, 1, "l"], [65357, 1, "m"], [65358, 1, "n"], [65359, 1, "o"], [65360, 1, "p"], [65361, 1, "q"], [65362, 1, "r"], [65363, 1, "s"], [65364, 1, "t"], [65365, 1, "u"], [65366, 1, "v"], [65367, 1, "w"], [65368, 1, "x"], [65369, 1, "y"], [65370, 1, "z"], [65371, 5, "{"], [65372, 5, "|"], [65373, 5, "}"], [65374, 5, "~"], [65375, 1, "\u2985"], [65376, 1, "\u2986"], [65377, 1, "."], [65378, 1, "\u300C"], [65379, 1, "\u300D"], [65380, 1, "\u3001"], [65381, 1, "\u30FB"], [65382, 1, "\u30F2"], [65383, 1, "\u30A1"], [65384, 1, "\u30A3"], [65385, 1, "\u30A5"], [65386, 1, "\u30A7"], [65387, 1, "\u30A9"], [65388, 1, "\u30E3"], [65389, 1, "\u30E5"], [65390, 1, "\u30E7"], [65391, 1, "\u30C3"], [65392, 1, "\u30FC"], [65393, 1, "\u30A2"], [65394, 1, "\u30A4"], [65395, 1, "\u30A6"], [65396, 1, "\u30A8"], [65397, 1, "\u30AA"], [65398, 1, "\u30AB"], [65399, 1, "\u30AD"], [65400, 1, "\u30AF"], [65401, 1, "\u30B1"], [65402, 1, "\u30B3"], [65403, 1, "\u30B5"], [65404, 1, "\u30B7"], [65405, 1, "\u30B9"], [65406, 1, "\u30BB"], [65407, 1, "\u30BD"], [65408, 1, "\u30BF"], [65409, 1, "\u30C1"], [65410, 1, "\u30C4"], [65411, 1, "\u30C6"], [65412, 1, "\u30C8"], [65413, 1, "\u30CA"], [65414, 1, "\u30CB"], [65415, 1, "\u30CC"], [65416, 1, "\u30CD"], [65417, 1, "\u30CE"], [65418, 1, "\u30CF"], [65419, 1, "\u30D2"], [65420, 1, "\u30D5"], [65421, 1, "\u30D8"], [65422, 1, "\u30DB"], [65423, 1, "\u30DE"], [65424, 1, "\u30DF"], [65425, 1, "\u30E0"], [65426, 1, "\u30E1"], [65427, 1, "\u30E2"], [65428, 1, "\u30E4"], [65429, 1, "\u30E6"], [65430, 1, "\u30E8"], [65431, 1, "\u30E9"], [65432, 1, "\u30EA"], [65433, 1, "\u30EB"], [65434, 1, "\u30EC"], [65435, 1, "\u30ED"], [65436, 1, "\u30EF"], [65437, 1, "\u30F3"], [65438, 1, "\u3099"], [65439, 1, "\u309A"], [65440, 3], [65441, 1, "\u1100"], [65442, 1, "\u1101"], [65443, 1, "\u11AA"], [65444, 1, "\u1102"], [65445, 1, "\u11AC"], [65446, 1, "\u11AD"], [65447, 1, "\u1103"], [65448, 1, "\u1104"], [65449, 1, "\u1105"], [65450, 1, "\u11B0"], [65451, 1, "\u11B1"], [65452, 1, "\u11B2"], [65453, 1, "\u11B3"], [65454, 1, "\u11B4"], [65455, 1, "\u11B5"], [65456, 1, "\u111A"], [65457, 1, "\u1106"], [65458, 1, "\u1107"], [65459, 1, "\u1108"], [65460, 1, "\u1121"], [65461, 1, "\u1109"], [65462, 1, "\u110A"], [65463, 1, "\u110B"], [65464, 1, "\u110C"], [65465, 1, "\u110D"], [65466, 1, "\u110E"], [65467, 1, "\u110F"], [65468, 1, "\u1110"], [65469, 1, "\u1111"], [65470, 1, "\u1112"], [[65471, 65473], 3], [65474, 1, "\u1161"], [65475, 1, "\u1162"], [65476, 1, "\u1163"], [65477, 1, "\u1164"], [65478, 1, "\u1165"], [65479, 1, "\u1166"], [[65480, 65481], 3], [65482, 1, "\u1167"], [65483, 1, "\u1168"], [65484, 1, "\u1169"], [65485, 1, "\u116A"], [65486, 1, "\u116B"], [65487, 1, "\u116C"], [[65488, 65489], 3], [65490, 1, "\u116D"], [65491, 1, "\u116E"], [65492, 1, "\u116F"], [65493, 1, "\u1170"], [65494, 1, "\u1171"], [65495, 1, "\u1172"], [[65496, 65497], 3], [65498, 1, "\u1173"], [65499, 1, "\u1174"], [65500, 1, "\u1175"], [[65501, 65503], 3], [65504, 1, "\xA2"], [65505, 1, "\xA3"], [65506, 1, "\xAC"], [65507, 5, " \u0304"], [65508, 1, "\xA6"], [65509, 1, "\xA5"], [65510, 1, "\u20A9"], [65511, 3], [65512, 1, "\u2502"], [65513, 1, "\u2190"], [65514, 1, "\u2191"], [65515, 1, "\u2192"], [65516, 1, "\u2193"], [65517, 1, "\u25A0"], [65518, 1, "\u25CB"], [[65519, 65528], 3], [[65529, 65531], 3], [65532, 3], [65533, 3], [[65534, 65535], 3], [[65536, 65547], 2], [65548, 3], [[65549, 65574], 2], [65575, 3], [[65576, 65594], 2], [65595, 3], [[65596, 65597], 2], [65598, 3], [[65599, 65613], 2], [[65614, 65615], 3], [[65616, 65629], 2], [[65630, 65663], 3], [[65664, 65786], 2], [[65787, 65791], 3], [[65792, 65794], 2], [[65795, 65798], 3], [[65799, 65843], 2], [[65844, 65846], 3], [[65847, 65855], 2], [[65856, 65930], 2], [[65931, 65932], 2], [[65933, 65934], 2], [65935, 3], [[65936, 65947], 2], [65948, 2], [[65949, 65951], 3], [65952, 2], [[65953, 65999], 3], [[66e3, 66044], 2], [66045, 2], [[66046, 66175], 3], [[66176, 66204], 2], [[66205, 66207], 3], [[66208, 66256], 2], [[66257, 66271], 3], [66272, 2], [[66273, 66299], 2], [[66300, 66303], 3], [[66304, 66334], 2], [66335, 2], [[66336, 66339], 2], [[66340, 66348], 3], [[66349, 66351], 2], [[66352, 66368], 2], [66369, 2], [[66370, 66377], 2], [66378, 2], [[66379, 66383], 3], [[66384, 66426], 2], [[66427, 66431], 3], [[66432, 66461], 2], [66462, 3], [66463, 2], [[66464, 66499], 2], [[66500, 66503], 3], [[66504, 66511], 2], [[66512, 66517], 2], [[66518, 66559], 3], [66560, 1, "\u{10428}"], [66561, 1, "\u{10429}"], [66562, 1, "\u{1042A}"], [66563, 1, "\u{1042B}"], [66564, 1, "\u{1042C}"], [66565, 1, "\u{1042D}"], [66566, 1, "\u{1042E}"], [66567, 1, "\u{1042F}"], [66568, 1, "\u{10430}"], [66569, 1, "\u{10431}"], [66570, 1, "\u{10432}"], [66571, 1, "\u{10433}"], [66572, 1, "\u{10434}"], [66573, 1, "\u{10435}"], [66574, 1, "\u{10436}"], [66575, 1, "\u{10437}"], [66576, 1, "\u{10438}"], [66577, 1, "\u{10439}"], [66578, 1, "\u{1043A}"], [66579, 1, "\u{1043B}"], [66580, 1, "\u{1043C}"], [66581, 1, "\u{1043D}"], [66582, 1, "\u{1043E}"], [66583, 1, "\u{1043F}"], [66584, 1, "\u{10440}"], [66585, 1, "\u{10441}"], [66586, 1, "\u{10442}"], [66587, 1, "\u{10443}"], [66588, 1, "\u{10444}"], [66589, 1, "\u{10445}"], [66590, 1, "\u{10446}"], [66591, 1, "\u{10447}"], [66592, 1, "\u{10448}"], [66593, 1, "\u{10449}"], [66594, 1, "\u{1044A}"], [66595, 1, "\u{1044B}"], [66596, 1, "\u{1044C}"], [66597, 1, "\u{1044D}"], [66598, 1, "\u{1044E}"], [66599, 1, "\u{1044F}"], [[66600, 66637], 2], [[66638, 66717], 2], [[66718, 66719], 3], [[66720, 66729], 2], [[66730, 66735], 3], [66736, 1, "\u{104D8}"], [66737, 1, "\u{104D9}"], [66738, 1, "\u{104DA}"], [66739, 1, "\u{104DB}"], [66740, 1, "\u{104DC}"], [66741, 1, "\u{104DD}"], [66742, 1, "\u{104DE}"], [66743, 1, "\u{104DF}"], [66744, 1, "\u{104E0}"], [66745, 1, "\u{104E1}"], [66746, 1, "\u{104E2}"], [66747, 1, "\u{104E3}"], [66748, 1, "\u{104E4}"], [66749, 1, "\u{104E5}"], [66750, 1, "\u{104E6}"], [66751, 1, "\u{104E7}"], [66752, 1, "\u{104E8}"], [66753, 1, "\u{104E9}"], [66754, 1, "\u{104EA}"], [66755, 1, "\u{104EB}"], [66756, 1, "\u{104EC}"], [66757, 1, "\u{104ED}"], [66758, 1, "\u{104EE}"], [66759, 1, "\u{104EF}"], [66760, 1, "\u{104F0}"], [66761, 1, "\u{104F1}"], [66762, 1, "\u{104F2}"], [66763, 1, "\u{104F3}"], [66764, 1, "\u{104F4}"], [66765, 1, "\u{104F5}"], [66766, 1, "\u{104F6}"], [66767, 1, "\u{104F7}"], [66768, 1, "\u{104F8}"], [66769, 1, "\u{104F9}"], [66770, 1, "\u{104FA}"], [66771, 1, "\u{104FB}"], [[66772, 66775], 3], [[66776, 66811], 2], [[66812, 66815], 3], [[66816, 66855], 2], [[66856, 66863], 3], [[66864, 66915], 2], [[66916, 66926], 3], [66927, 2], [66928, 1, "\u{10597}"], [66929, 1, "\u{10598}"], [66930, 1, "\u{10599}"], [66931, 1, "\u{1059A}"], [66932, 1, "\u{1059B}"], [66933, 1, "\u{1059C}"], [66934, 1, "\u{1059D}"], [66935, 1, "\u{1059E}"], [66936, 1, "\u{1059F}"], [66937, 1, "\u{105A0}"], [66938, 1, "\u{105A1}"], [66939, 3], [66940, 1, "\u{105A3}"], [66941, 1, "\u{105A4}"], [66942, 1, "\u{105A5}"], [66943, 1, "\u{105A6}"], [66944, 1, "\u{105A7}"], [66945, 1, "\u{105A8}"], [66946, 1, "\u{105A9}"], [66947, 1, "\u{105AA}"], [66948, 1, "\u{105AB}"], [66949, 1, "\u{105AC}"], [66950, 1, "\u{105AD}"], [66951, 1, "\u{105AE}"], [66952, 1, "\u{105AF}"], [66953, 1, "\u{105B0}"], [66954, 1, "\u{105B1}"], [66955, 3], [66956, 1, "\u{105B3}"], [66957, 1, "\u{105B4}"], [66958, 1, "\u{105B5}"], [66959, 1, "\u{105B6}"], [66960, 1, "\u{105B7}"], [66961, 1, "\u{105B8}"], [66962, 1, "\u{105B9}"], [66963, 3], [66964, 1, "\u{105BB}"], [66965, 1, "\u{105BC}"], [66966, 3], [[66967, 66977], 2], [66978, 3], [[66979, 66993], 2], [66994, 3], [[66995, 67001], 2], [67002, 3], [[67003, 67004], 2], [[67005, 67071], 3], [[67072, 67382], 2], [[67383, 67391], 3], [[67392, 67413], 2], [[67414, 67423], 3], [[67424, 67431], 2], [[67432, 67455], 3], [67456, 2], [67457, 1, "\u02D0"], [67458, 1, "\u02D1"], [67459, 1, "\xE6"], [67460, 1, "\u0299"], [67461, 1, "\u0253"], [67462, 3], [67463, 1, "\u02A3"], [67464, 1, "\uAB66"], [67465, 1, "\u02A5"], [67466, 1, "\u02A4"], [67467, 1, "\u0256"], [67468, 1, "\u0257"], [67469, 1, "\u1D91"], [67470, 1, "\u0258"], [67471, 1, "\u025E"], [67472, 1, "\u02A9"], [67473, 1, "\u0264"], [67474, 1, "\u0262"], [67475, 1, "\u0260"], [67476, 1, "\u029B"], [67477, 1, "\u0127"], [67478, 1, "\u029C"], [67479, 1, "\u0267"], [67480, 1, "\u0284"], [67481, 1, "\u02AA"], [67482, 1, "\u02AB"], [67483, 1, "\u026C"], [67484, 1, "\u{1DF04}"], [67485, 1, "\uA78E"], [67486, 1, "\u026E"], [67487, 1, "\u{1DF05}"], [67488, 1, "\u028E"], [67489, 1, "\u{1DF06}"], [67490, 1, "\xF8"], [67491, 1, "\u0276"], [67492, 1, "\u0277"], [67493, 1, "q"], [67494, 1, "\u027A"], [67495, 1, "\u{1DF08}"], [67496, 1, "\u027D"], [67497, 1, "\u027E"], [67498, 1, "\u0280"], [67499, 1, "\u02A8"], [67500, 1, "\u02A6"], [67501, 1, "\uAB67"], [67502, 1, "\u02A7"], [67503, 1, "\u0288"], [67504, 1, "\u2C71"], [67505, 3], [67506, 1, "\u028F"], [67507, 1, "\u02A1"], [67508, 1, "\u02A2"], [67509, 1, "\u0298"], [67510, 1, "\u01C0"], [67511, 1, "\u01C1"], [67512, 1, "\u01C2"], [67513, 1, "\u{1DF0A}"], [67514, 1, "\u{1DF1E}"], [[67515, 67583], 3], [[67584, 67589], 2], [[67590, 67591], 3], [67592, 2], [67593, 3], [[67594, 67637], 2], [67638, 3], [[67639, 67640], 2], [[67641, 67643], 3], [67644, 2], [[67645, 67646], 3], [67647, 2], [[67648, 67669], 2], [67670, 3], [[67671, 67679], 2], [[67680, 67702], 2], [[67703, 67711], 2], [[67712, 67742], 2], [[67743, 67750], 3], [[67751, 67759], 2], [[67760, 67807], 3], [[67808, 67826], 2], [67827, 3], [[67828, 67829], 2], [[67830, 67834], 3], [[67835, 67839], 2], [[67840, 67861], 2], [[67862, 67865], 2], [[67866, 67867], 2], [[67868, 67870], 3], [67871, 2], [[67872, 67897], 2], [[67898, 67902], 3], [67903, 2], [[67904, 67967], 3], [[67968, 68023], 2], [[68024, 68027], 3], [[68028, 68029], 2], [[68030, 68031], 2], [[68032, 68047], 2], [[68048, 68049], 3], [[68050, 68095], 2], [[68096, 68099], 2], [68100, 3], [[68101, 68102], 2], [[68103, 68107], 3], [[68108, 68115], 2], [68116, 3], [[68117, 68119], 2], [68120, 3], [[68121, 68147], 2], [[68148, 68149], 2], [[68150, 68151], 3], [[68152, 68154], 2], [[68155, 68158], 3], [68159, 2], [[68160, 68167], 2], [68168, 2], [[68169, 68175], 3], [[68176, 68184], 2], [[68185, 68191], 3], [[68192, 68220], 2], [[68221, 68223], 2], [[68224, 68252], 2], [[68253, 68255], 2], [[68256, 68287], 3], [[68288, 68295], 2], [68296, 2], [[68297, 68326], 2], [[68327, 68330], 3], [[68331, 68342], 2], [[68343, 68351], 3], [[68352, 68405], 2], [[68406, 68408], 3], [[68409, 68415], 2], [[68416, 68437], 2], [[68438, 68439], 3], [[68440, 68447], 2], [[68448, 68466], 2], [[68467, 68471], 3], [[68472, 68479], 2], [[68480, 68497], 2], [[68498, 68504], 3], [[68505, 68508], 2], [[68509, 68520], 3], [[68521, 68527], 2], [[68528, 68607], 3], [[68608, 68680], 2], [[68681, 68735], 3], [68736, 1, "\u{10CC0}"], [68737, 1, "\u{10CC1}"], [68738, 1, "\u{10CC2}"], [68739, 1, "\u{10CC3}"], [68740, 1, "\u{10CC4}"], [68741, 1, "\u{10CC5}"], [68742, 1, "\u{10CC6}"], [68743, 1, "\u{10CC7}"], [68744, 1, "\u{10CC8}"], [68745, 1, "\u{10CC9}"], [68746, 1, "\u{10CCA}"], [68747, 1, "\u{10CCB}"], [68748, 1, "\u{10CCC}"], [68749, 1, "\u{10CCD}"], [68750, 1, "\u{10CCE}"], [68751, 1, "\u{10CCF}"], [68752, 1, "\u{10CD0}"], [68753, 1, "\u{10CD1}"], [68754, 1, "\u{10CD2}"], [68755, 1, "\u{10CD3}"], [68756, 1, "\u{10CD4}"], [68757, 1, "\u{10CD5}"], [68758, 1, "\u{10CD6}"], [68759, 1, "\u{10CD7}"], [68760, 1, "\u{10CD8}"], [68761, 1, "\u{10CD9}"], [68762, 1, "\u{10CDA}"], [68763, 1, "\u{10CDB}"], [68764, 1, "\u{10CDC}"], [68765, 1, "\u{10CDD}"], [68766, 1, "\u{10CDE}"], [68767, 1, "\u{10CDF}"], [68768, 1, "\u{10CE0}"], [68769, 1, "\u{10CE1}"], [68770, 1, "\u{10CE2}"], [68771, 1, "\u{10CE3}"], [68772, 1, "\u{10CE4}"], [68773, 1, "\u{10CE5}"], [68774, 1, "\u{10CE6}"], [68775, 1, "\u{10CE7}"], [68776, 1, "\u{10CE8}"], [68777, 1, "\u{10CE9}"], [68778, 1, "\u{10CEA}"], [68779, 1, "\u{10CEB}"], [68780, 1, "\u{10CEC}"], [68781, 1, "\u{10CED}"], [68782, 1, "\u{10CEE}"], [68783, 1, "\u{10CEF}"], [68784, 1, "\u{10CF0}"], [68785, 1, "\u{10CF1}"], [68786, 1, "\u{10CF2}"], [[68787, 68799], 3], [[68800, 68850], 2], [[68851, 68857], 3], [[68858, 68863], 2], [[68864, 68903], 2], [[68904, 68911], 3], [[68912, 68921], 2], [[68922, 69215], 3], [[69216, 69246], 2], [69247, 3], [[69248, 69289], 2], [69290, 3], [[69291, 69292], 2], [69293, 2], [[69294, 69295], 3], [[69296, 69297], 2], [[69298, 69372], 3], [[69373, 69375], 2], [[69376, 69404], 2], [[69405, 69414], 2], [69415, 2], [[69416, 69423], 3], [[69424, 69456], 2], [[69457, 69465], 2], [[69466, 69487], 3], [[69488, 69509], 2], [[69510, 69513], 2], [[69514, 69551], 3], [[69552, 69572], 2], [[69573, 69579], 2], [[69580, 69599], 3], [[69600, 69622], 2], [[69623, 69631], 3], [[69632, 69702], 2], [[69703, 69709], 2], [[69710, 69713], 3], [[69714, 69733], 2], [[69734, 69743], 2], [[69744, 69749], 2], [[69750, 69758], 3], [69759, 2], [[69760, 69818], 2], [[69819, 69820], 2], [69821, 3], [[69822, 69825], 2], [69826, 2], [[69827, 69836], 3], [69837, 3], [[69838, 69839], 3], [[69840, 69864], 2], [[69865, 69871], 3], [[69872, 69881], 2], [[69882, 69887], 3], [[69888, 69940], 2], [69941, 3], [[69942, 69951], 2], [[69952, 69955], 2], [[69956, 69958], 2], [69959, 2], [[69960, 69967], 3], [[69968, 70003], 2], [[70004, 70005], 2], [70006, 2], [[70007, 70015], 3], [[70016, 70084], 2], [[70085, 70088], 2], [[70089, 70092], 2], [70093, 2], [[70094, 70095], 2], [[70096, 70105], 2], [70106, 2], [70107, 2], [70108, 2], [[70109, 70111], 2], [70112, 3], [[70113, 70132], 2], [[70133, 70143], 3], [[70144, 70161], 2], [70162, 3], [[70163, 70199], 2], [[70200, 70205], 2], [70206, 2], [[70207, 70209], 2], [[70210, 70271], 3], [[70272, 70278], 2], [70279, 3], [70280, 2], [70281, 3], [[70282, 70285], 2], [70286, 3], [[70287, 70301], 2], [70302, 3], [[70303, 70312], 2], [70313, 2], [[70314, 70319], 3], [[70320, 70378], 2], [[70379, 70383], 3], [[70384, 70393], 2], [[70394, 70399], 3], [70400, 2], [[70401, 70403], 2], [70404, 3], [[70405, 70412], 2], [[70413, 70414], 3], [[70415, 70416], 2], [[70417, 70418], 3], [[70419, 70440], 2], [70441, 3], [[70442, 70448], 2], [70449, 3], [[70450, 70451], 2], [70452, 3], [[70453, 70457], 2], [70458, 3], [70459, 2], [[70460, 70468], 2], [[70469, 70470], 3], [[70471, 70472], 2], [[70473, 70474], 3], [[70475, 70477], 2], [[70478, 70479], 3], [70480, 2], [[70481, 70486], 3], [70487, 2], [[70488, 70492], 3], [[70493, 70499], 2], [[70500, 70501], 3], [[70502, 70508], 2], [[70509, 70511], 3], [[70512, 70516], 2], [[70517, 70655], 3], [[70656, 70730], 2], [[70731, 70735], 2], [[70736, 70745], 2], [70746, 2], [70747, 2], [70748, 3], [70749, 2], [70750, 2], [70751, 2], [[70752, 70753], 2], [[70754, 70783], 3], [[70784, 70853], 2], [70854, 2], [70855, 2], [[70856, 70863], 3], [[70864, 70873], 2], [[70874, 71039], 3], [[71040, 71093], 2], [[71094, 71095], 3], [[71096, 71104], 2], [[71105, 71113], 2], [[71114, 71127], 2], [[71128, 71133], 2], [[71134, 71167], 3], [[71168, 71232], 2], [[71233, 71235], 2], [71236, 2], [[71237, 71247], 3], [[71248, 71257], 2], [[71258, 71263], 3], [[71264, 71276], 2], [[71277, 71295], 3], [[71296, 71351], 2], [71352, 2], [71353, 2], [[71354, 71359], 3], [[71360, 71369], 2], [[71370, 71423], 3], [[71424, 71449], 2], [71450, 2], [[71451, 71452], 3], [[71453, 71467], 2], [[71468, 71471], 3], [[71472, 71481], 2], [[71482, 71487], 2], [[71488, 71494], 2], [[71495, 71679], 3], [[71680, 71738], 2], [71739, 2], [[71740, 71839], 3], [71840, 1, "\u{118C0}"], [71841, 1, "\u{118C1}"], [71842, 1, "\u{118C2}"], [71843, 1, "\u{118C3}"], [71844, 1, "\u{118C4}"], [71845, 1, "\u{118C5}"], [71846, 1, "\u{118C6}"], [71847, 1, "\u{118C7}"], [71848, 1, "\u{118C8}"], [71849, 1, "\u{118C9}"], [71850, 1, "\u{118CA}"], [71851, 1, "\u{118CB}"], [71852, 1, "\u{118CC}"], [71853, 1, "\u{118CD}"], [71854, 1, "\u{118CE}"], [71855, 1, "\u{118CF}"], [71856, 1, "\u{118D0}"], [71857, 1, "\u{118D1}"], [71858, 1, "\u{118D2}"], [71859, 1, "\u{118D3}"], [71860, 1, "\u{118D4}"], [71861, 1, "\u{118D5}"], [71862, 1, "\u{118D6}"], [71863, 1, "\u{118D7}"], [71864, 1, "\u{118D8}"], [71865, 1, "\u{118D9}"], [71866, 1, "\u{118DA}"], [71867, 1, "\u{118DB}"], [71868, 1, "\u{118DC}"], [71869, 1, "\u{118DD}"], [71870, 1, "\u{118DE}"], [71871, 1, "\u{118DF}"], [[71872, 71913], 2], [[71914, 71922], 2], [[71923, 71934], 3], [71935, 2], [[71936, 71942], 2], [[71943, 71944], 3], [71945, 2], [[71946, 71947], 3], [[71948, 71955], 2], [71956, 3], [[71957, 71958], 2], [71959, 3], [[71960, 71989], 2], [71990, 3], [[71991, 71992], 2], [[71993, 71994], 3], [[71995, 72003], 2], [[72004, 72006], 2], [[72007, 72015], 3], [[72016, 72025], 2], [[72026, 72095], 3], [[72096, 72103], 2], [[72104, 72105], 3], [[72106, 72151], 2], [[72152, 72153], 3], [[72154, 72161], 2], [72162, 2], [[72163, 72164], 2], [[72165, 72191], 3], [[72192, 72254], 2], [[72255, 72262], 2], [72263, 2], [[72264, 72271], 3], [[72272, 72323], 2], [[72324, 72325], 2], [[72326, 72345], 2], [[72346, 72348], 2], [72349, 2], [[72350, 72354], 2], [[72355, 72367], 3], [[72368, 72383], 2], [[72384, 72440], 2], [[72441, 72447], 3], [[72448, 72457], 2], [[72458, 72703], 3], [[72704, 72712], 2], [72713, 3], [[72714, 72758], 2], [72759, 3], [[72760, 72768], 2], [[72769, 72773], 2], [[72774, 72783], 3], [[72784, 72793], 2], [[72794, 72812], 2], [[72813, 72815], 3], [[72816, 72817], 2], [[72818, 72847], 2], [[72848, 72849], 3], [[72850, 72871], 2], [72872, 3], [[72873, 72886], 2], [[72887, 72959], 3], [[72960, 72966], 2], [72967, 3], [[72968, 72969], 2], [72970, 3], [[72971, 73014], 2], [[73015, 73017], 3], [73018, 2], [73019, 3], [[73020, 73021], 2], [73022, 3], [[73023, 73031], 2], [[73032, 73039], 3], [[73040, 73049], 2], [[73050, 73055], 3], [[73056, 73061], 2], [73062, 3], [[73063, 73064], 2], [73065, 3], [[73066, 73102], 2], [73103, 3], [[73104, 73105], 2], [73106, 3], [[73107, 73112], 2], [[73113, 73119], 3], [[73120, 73129], 2], [[73130, 73439], 3], [[73440, 73462], 2], [[73463, 73464], 2], [[73465, 73471], 3], [[73472, 73488], 2], [73489, 3], [[73490, 73530], 2], [[73531, 73533], 3], [[73534, 73538], 2], [[73539, 73551], 2], [[73552, 73561], 2], [[73562, 73647], 3], [73648, 2], [[73649, 73663], 3], [[73664, 73713], 2], [[73714, 73726], 3], [73727, 2], [[73728, 74606], 2], [[74607, 74648], 2], [74649, 2], [[74650, 74751], 3], [[74752, 74850], 2], [[74851, 74862], 2], [74863, 3], [[74864, 74867], 2], [74868, 2], [[74869, 74879], 3], [[74880, 75075], 2], [[75076, 77711], 3], [[77712, 77808], 2], [[77809, 77810], 2], [[77811, 77823], 3], [[77824, 78894], 2], [78895, 2], [[78896, 78904], 3], [[78905, 78911], 3], [[78912, 78933], 2], [[78934, 82943], 3], [[82944, 83526], 2], [[83527, 92159], 3], [[92160, 92728], 2], [[92729, 92735], 3], [[92736, 92766], 2], [92767, 3], [[92768, 92777], 2], [[92778, 92781], 3], [[92782, 92783], 2], [[92784, 92862], 2], [92863, 3], [[92864, 92873], 2], [[92874, 92879], 3], [[92880, 92909], 2], [[92910, 92911], 3], [[92912, 92916], 2], [92917, 2], [[92918, 92927], 3], [[92928, 92982], 2], [[92983, 92991], 2], [[92992, 92995], 2], [[92996, 92997], 2], [[92998, 93007], 3], [[93008, 93017], 2], [93018, 3], [[93019, 93025], 2], [93026, 3], [[93027, 93047], 2], [[93048, 93052], 3], [[93053, 93071], 2], [[93072, 93759], 3], [93760, 1, "\u{16E60}"], [93761, 1, "\u{16E61}"], [93762, 1, "\u{16E62}"], [93763, 1, "\u{16E63}"], [93764, 1, "\u{16E64}"], [93765, 1, "\u{16E65}"], [93766, 1, "\u{16E66}"], [93767, 1, "\u{16E67}"], [93768, 1, "\u{16E68}"], [93769, 1, "\u{16E69}"], [93770, 1, "\u{16E6A}"], [93771, 1, "\u{16E6B}"], [93772, 1, "\u{16E6C}"], [93773, 1, "\u{16E6D}"], [93774, 1, "\u{16E6E}"], [93775, 1, "\u{16E6F}"], [93776, 1, "\u{16E70}"], [93777, 1, "\u{16E71}"], [93778, 1, "\u{16E72}"], [93779, 1, "\u{16E73}"], [93780, 1, "\u{16E74}"], [93781, 1, "\u{16E75}"], [93782, 1, "\u{16E76}"], [93783, 1, "\u{16E77}"], [93784, 1, "\u{16E78}"], [93785, 1, "\u{16E79}"], [93786, 1, "\u{16E7A}"], [93787, 1, "\u{16E7B}"], [93788, 1, "\u{16E7C}"], [93789, 1, "\u{16E7D}"], [93790, 1, "\u{16E7E}"], [93791, 1, "\u{16E7F}"], [[93792, 93823], 2], [[93824, 93850], 2], [[93851, 93951], 3], [[93952, 94020], 2], [[94021, 94026], 2], [[94027, 94030], 3], [94031, 2], [[94032, 94078], 2], [[94079, 94087], 2], [[94088, 94094], 3], [[94095, 94111], 2], [[94112, 94175], 3], [94176, 2], [94177, 2], [94178, 2], [94179, 2], [94180, 2], [[94181, 94191], 3], [[94192, 94193], 2], [[94194, 94207], 3], [[94208, 100332], 2], [[100333, 100337], 2], [[100338, 100343], 2], [[100344, 100351], 3], [[100352, 101106], 2], [[101107, 101589], 2], [[101590, 101631], 3], [[101632, 101640], 2], [[101641, 110575], 3], [[110576, 110579], 2], [110580, 3], [[110581, 110587], 2], [110588, 3], [[110589, 110590], 2], [110591, 3], [[110592, 110593], 2], [[110594, 110878], 2], [[110879, 110882], 2], [[110883, 110897], 3], [110898, 2], [[110899, 110927], 3], [[110928, 110930], 2], [[110931, 110932], 3], [110933, 2], [[110934, 110947], 3], [[110948, 110951], 2], [[110952, 110959], 3], [[110960, 111355], 2], [[111356, 113663], 3], [[113664, 113770], 2], [[113771, 113775], 3], [[113776, 113788], 2], [[113789, 113791], 3], [[113792, 113800], 2], [[113801, 113807], 3], [[113808, 113817], 2], [[113818, 113819], 3], [113820, 2], [[113821, 113822], 2], [113823, 2], [[113824, 113827], 7], [[113828, 118527], 3], [[118528, 118573], 2], [[118574, 118575], 3], [[118576, 118598], 2], [[118599, 118607], 3], [[118608, 118723], 2], [[118724, 118783], 3], [[118784, 119029], 2], [[119030, 119039], 3], [[119040, 119078], 2], [[119079, 119080], 3], [119081, 2], [[119082, 119133], 2], [119134, 1, "\u{1D157}\u{1D165}"], [119135, 1, "\u{1D158}\u{1D165}"], [119136, 1, "\u{1D158}\u{1D165}\u{1D16E}"], [119137, 1, "\u{1D158}\u{1D165}\u{1D16F}"], [119138, 1, "\u{1D158}\u{1D165}\u{1D170}"], [119139, 1, "\u{1D158}\u{1D165}\u{1D171}"], [119140, 1, "\u{1D158}\u{1D165}\u{1D172}"], [[119141, 119154], 2], [[119155, 119162], 3], [[119163, 119226], 2], [119227, 1, "\u{1D1B9}\u{1D165}"], [119228, 1, "\u{1D1BA}\u{1D165}"], [119229, 1, "\u{1D1B9}\u{1D165}\u{1D16E}"], [119230, 1, "\u{1D1BA}\u{1D165}\u{1D16E}"], [119231, 1, "\u{1D1B9}\u{1D165}\u{1D16F}"], [119232, 1, "\u{1D1BA}\u{1D165}\u{1D16F}"], [[119233, 119261], 2], [[119262, 119272], 2], [[119273, 119274], 2], [[119275, 119295], 3], [[119296, 119365], 2], [[119366, 119487], 3], [[119488, 119507], 2], [[119508, 119519], 3], [[119520, 119539], 2], [[119540, 119551], 3], [[119552, 119638], 2], [[119639, 119647], 3], [[119648, 119665], 2], [[119666, 119672], 2], [[119673, 119807], 3], [119808, 1, "a"], [119809, 1, "b"], [119810, 1, "c"], [119811, 1, "d"], [119812, 1, "e"], [119813, 1, "f"], [119814, 1, "g"], [119815, 1, "h"], [119816, 1, "i"], [119817, 1, "j"], [119818, 1, "k"], [119819, 1, "l"], [119820, 1, "m"], [119821, 1, "n"], [119822, 1, "o"], [119823, 1, "p"], [119824, 1, "q"], [119825, 1, "r"], [119826, 1, "s"], [119827, 1, "t"], [119828, 1, "u"], [119829, 1, "v"], [119830, 1, "w"], [119831, 1, "x"], [119832, 1, "y"], [119833, 1, "z"], [119834, 1, "a"], [119835, 1, "b"], [119836, 1, "c"], [119837, 1, "d"], [119838, 1, "e"], [119839, 1, "f"], [119840, 1, "g"], [119841, 1, "h"], [119842, 1, "i"], [119843, 1, "j"], [119844, 1, "k"], [119845, 1, "l"], [119846, 1, "m"], [119847, 1, "n"], [119848, 1, "o"], [119849, 1, "p"], [119850, 1, "q"], [119851, 1, "r"], [119852, 1, "s"], [119853, 1, "t"], [119854, 1, "u"], [119855, 1, "v"], [119856, 1, "w"], [119857, 1, "x"], [119858, 1, "y"], [119859, 1, "z"], [119860, 1, "a"], [119861, 1, "b"], [119862, 1, "c"], [119863, 1, "d"], [119864, 1, "e"], [119865, 1, "f"], [119866, 1, "g"], [119867, 1, "h"], [119868, 1, "i"], [119869, 1, "j"], [119870, 1, "k"], [119871, 1, "l"], [119872, 1, "m"], [119873, 1, "n"], [119874, 1, "o"], [119875, 1, "p"], [119876, 1, "q"], [119877, 1, "r"], [119878, 1, "s"], [119879, 1, "t"], [119880, 1, "u"], [119881, 1, "v"], [119882, 1, "w"], [119883, 1, "x"], [119884, 1, "y"], [119885, 1, "z"], [119886, 1, "a"], [119887, 1, "b"], [119888, 1, "c"], [119889, 1, "d"], [119890, 1, "e"], [119891, 1, "f"], [119892, 1, "g"], [119893, 3], [119894, 1, "i"], [119895, 1, "j"], [119896, 1, "k"], [119897, 1, "l"], [119898, 1, "m"], [119899, 1, "n"], [119900, 1, "o"], [119901, 1, "p"], [119902, 1, "q"], [119903, 1, "r"], [119904, 1, "s"], [119905, 1, "t"], [119906, 1, "u"], [119907, 1, "v"], [119908, 1, "w"], [119909, 1, "x"], [119910, 1, "y"], [119911, 1, "z"], [119912, 1, "a"], [119913, 1, "b"], [119914, 1, "c"], [119915, 1, "d"], [119916, 1, "e"], [119917, 1, "f"], [119918, 1, "g"], [119919, 1, "h"], [119920, 1, "i"], [119921, 1, "j"], [119922, 1, "k"], [119923, 1, "l"], [119924, 1, "m"], [119925, 1, "n"], [119926, 1, "o"], [119927, 1, "p"], [119928, 1, "q"], [119929, 1, "r"], [119930, 1, "s"], [119931, 1, "t"], [119932, 1, "u"], [119933, 1, "v"], [119934, 1, "w"], [119935, 1, "x"], [119936, 1, "y"], [119937, 1, "z"], [119938, 1, "a"], [119939, 1, "b"], [119940, 1, "c"], [119941, 1, "d"], [119942, 1, "e"], [119943, 1, "f"], [119944, 1, "g"], [119945, 1, "h"], [119946, 1, "i"], [119947, 1, "j"], [119948, 1, "k"], [119949, 1, "l"], [119950, 1, "m"], [119951, 1, "n"], [119952, 1, "o"], [119953, 1, "p"], [119954, 1, "q"], [119955, 1, "r"], [119956, 1, "s"], [119957, 1, "t"], [119958, 1, "u"], [119959, 1, "v"], [119960, 1, "w"], [119961, 1, "x"], [119962, 1, "y"], [119963, 1, "z"], [119964, 1, "a"], [119965, 3], [119966, 1, "c"], [119967, 1, "d"], [[119968, 119969], 3], [119970, 1, "g"], [[119971, 119972], 3], [119973, 1, "j"], [119974, 1, "k"], [[119975, 119976], 3], [119977, 1, "n"], [119978, 1, "o"], [119979, 1, "p"], [119980, 1, "q"], [119981, 3], [119982, 1, "s"], [119983, 1, "t"], [119984, 1, "u"], [119985, 1, "v"], [119986, 1, "w"], [119987, 1, "x"], [119988, 1, "y"], [119989, 1, "z"], [119990, 1, "a"], [119991, 1, "b"], [119992, 1, "c"], [119993, 1, "d"], [119994, 3], [119995, 1, "f"], [119996, 3], [119997, 1, "h"], [119998, 1, "i"], [119999, 1, "j"], [12e4, 1, "k"], [120001, 1, "l"], [120002, 1, "m"], [120003, 1, "n"], [120004, 3], [120005, 1, "p"], [120006, 1, "q"], [120007, 1, "r"], [120008, 1, "s"], [120009, 1, "t"], [120010, 1, "u"], [120011, 1, "v"], [120012, 1, "w"], [120013, 1, "x"], [120014, 1, "y"], [120015, 1, "z"], [120016, 1, "a"], [120017, 1, "b"], [120018, 1, "c"], [120019, 1, "d"], [120020, 1, "e"], [120021, 1, "f"], [120022, 1, "g"], [120023, 1, "h"], [120024, 1, "i"], [120025, 1, "j"], [120026, 1, "k"], [120027, 1, "l"], [120028, 1, "m"], [120029, 1, "n"], [120030, 1, "o"], [120031, 1, "p"], [120032, 1, "q"], [120033, 1, "r"], [120034, 1, "s"], [120035, 1, "t"], [120036, 1, "u"], [120037, 1, "v"], [120038, 1, "w"], [120039, 1, "x"], [120040, 1, "y"], [120041, 1, "z"], [120042, 1, "a"], [120043, 1, "b"], [120044, 1, "c"], [120045, 1, "d"], [120046, 1, "e"], [120047, 1, "f"], [120048, 1, "g"], [120049, 1, "h"], [120050, 1, "i"], [120051, 1, "j"], [120052, 1, "k"], [120053, 1, "l"], [120054, 1, "m"], [120055, 1, "n"], [120056, 1, "o"], [120057, 1, "p"], [120058, 1, "q"], [120059, 1, "r"], [120060, 1, "s"], [120061, 1, "t"], [120062, 1, "u"], [120063, 1, "v"], [120064, 1, "w"], [120065, 1, "x"], [120066, 1, "y"], [120067, 1, "z"], [120068, 1, "a"], [120069, 1, "b"], [120070, 3], [120071, 1, "d"], [120072, 1, "e"], [120073, 1, "f"], [120074, 1, "g"], [[120075, 120076], 3], [120077, 1, "j"], [120078, 1, "k"], [120079, 1, "l"], [120080, 1, "m"], [120081, 1, "n"], [120082, 1, "o"], [120083, 1, "p"], [120084, 1, "q"], [120085, 3], [120086, 1, "s"], [120087, 1, "t"], [120088, 1, "u"], [120089, 1, "v"], [120090, 1, "w"], [120091, 1, "x"], [120092, 1, "y"], [120093, 3], [120094, 1, "a"], [120095, 1, "b"], [120096, 1, "c"], [120097, 1, "d"], [120098, 1, "e"], [120099, 1, "f"], [120100, 1, "g"], [120101, 1, "h"], [120102, 1, "i"], [120103, 1, "j"], [120104, 1, "k"], [120105, 1, "l"], [120106, 1, "m"], [120107, 1, "n"], [120108, 1, "o"], [120109, 1, "p"], [120110, 1, "q"], [120111, 1, "r"], [120112, 1, "s"], [120113, 1, "t"], [120114, 1, "u"], [120115, 1, "v"], [120116, 1, "w"], [120117, 1, "x"], [120118, 1, "y"], [120119, 1, "z"], [120120, 1, "a"], [120121, 1, "b"], [120122, 3], [120123, 1, "d"], [120124, 1, "e"], [120125, 1, "f"], [120126, 1, "g"], [120127, 3], [120128, 1, "i"], [120129, 1, "j"], [120130, 1, "k"], [120131, 1, "l"], [120132, 1, "m"], [120133, 3], [120134, 1, "o"], [[120135, 120137], 3], [120138, 1, "s"], [120139, 1, "t"], [120140, 1, "u"], [120141, 1, "v"], [120142, 1, "w"], [120143, 1, "x"], [120144, 1, "y"], [120145, 3], [120146, 1, "a"], [120147, 1, "b"], [120148, 1, "c"], [120149, 1, "d"], [120150, 1, "e"], [120151, 1, "f"], [120152, 1, "g"], [120153, 1, "h"], [120154, 1, "i"], [120155, 1, "j"], [120156, 1, "k"], [120157, 1, "l"], [120158, 1, "m"], [120159, 1, "n"], [120160, 1, "o"], [120161, 1, "p"], [120162, 1, "q"], [120163, 1, "r"], [120164, 1, "s"], [120165, 1, "t"], [120166, 1, "u"], [120167, 1, "v"], [120168, 1, "w"], [120169, 1, "x"], [120170, 1, "y"], [120171, 1, "z"], [120172, 1, "a"], [120173, 1, "b"], [120174, 1, "c"], [120175, 1, "d"], [120176, 1, "e"], [120177, 1, "f"], [120178, 1, "g"], [120179, 1, "h"], [120180, 1, "i"], [120181, 1, "j"], [120182, 1, "k"], [120183, 1, "l"], [120184, 1, "m"], [120185, 1, "n"], [120186, 1, "o"], [120187, 1, "p"], [120188, 1, "q"], [120189, 1, "r"], [120190, 1, "s"], [120191, 1, "t"], [120192, 1, "u"], [120193, 1, "v"], [120194, 1, "w"], [120195, 1, "x"], [120196, 1, "y"], [120197, 1, "z"], [120198, 1, "a"], [120199, 1, "b"], [120200, 1, "c"], [120201, 1, "d"], [120202, 1, "e"], [120203, 1, "f"], [120204, 1, "g"], [120205, 1, "h"], [120206, 1, "i"], [120207, 1, "j"], [120208, 1, "k"], [120209, 1, "l"], [120210, 1, "m"], [120211, 1, "n"], [120212, 1, "o"], [120213, 1, "p"], [120214, 1, "q"], [120215, 1, "r"], [120216, 1, "s"], [120217, 1, "t"], [120218, 1, "u"], [120219, 1, "v"], [120220, 1, "w"], [120221, 1, "x"], [120222, 1, "y"], [120223, 1, "z"], [120224, 1, "a"], [120225, 1, "b"], [120226, 1, "c"], [120227, 1, "d"], [120228, 1, "e"], [120229, 1, "f"], [120230, 1, "g"], [120231, 1, "h"], [120232, 1, "i"], [120233, 1, "j"], [120234, 1, "k"], [120235, 1, "l"], [120236, 1, "m"], [120237, 1, "n"], [120238, 1, "o"], [120239, 1, "p"], [120240, 1, "q"], [120241, 1, "r"], [120242, 1, "s"], [120243, 1, "t"], [120244, 1, "u"], [120245, 1, "v"], [120246, 1, "w"], [120247, 1, "x"], [120248, 1, "y"], [120249, 1, "z"], [120250, 1, "a"], [120251, 1, "b"], [120252, 1, "c"], [120253, 1, "d"], [120254, 1, "e"], [120255, 1, "f"], [120256, 1, "g"], [120257, 1, "h"], [120258, 1, "i"], [120259, 1, "j"], [120260, 1, "k"], [120261, 1, "l"], [120262, 1, "m"], [120263, 1, "n"], [120264, 1, "o"], [120265, 1, "p"], [120266, 1, "q"], [120267, 1, "r"], [120268, 1, "s"], [120269, 1, "t"], [120270, 1, "u"], [120271, 1, "v"], [120272, 1, "w"], [120273, 1, "x"], [120274, 1, "y"], [120275, 1, "z"], [120276, 1, "a"], [120277, 1, "b"], [120278, 1, "c"], [120279, 1, "d"], [120280, 1, "e"], [120281, 1, "f"], [120282, 1, "g"], [120283, 1, "h"], [120284, 1, "i"], [120285, 1, "j"], [120286, 1, "k"], [120287, 1, "l"], [120288, 1, "m"], [120289, 1, "n"], [120290, 1, "o"], [120291, 1, "p"], [120292, 1, "q"], [120293, 1, "r"], [120294, 1, "s"], [120295, 1, "t"], [120296, 1, "u"], [120297, 1, "v"], [120298, 1, "w"], [120299, 1, "x"], [120300, 1, "y"], [120301, 1, "z"], [120302, 1, "a"], [120303, 1, "b"], [120304, 1, "c"], [120305, 1, "d"], [120306, 1, "e"], [120307, 1, "f"], [120308, 1, "g"], [120309, 1, "h"], [120310, 1, "i"], [120311, 1, "j"], [120312, 1, "k"], [120313, 1, "l"], [120314, 1, "m"], [120315, 1, "n"], [120316, 1, "o"], [120317, 1, "p"], [120318, 1, "q"], [120319, 1, "r"], [120320, 1, "s"], [120321, 1, "t"], [120322, 1, "u"], [120323, 1, "v"], [120324, 1, "w"], [120325, 1, "x"], [120326, 1, "y"], [120327, 1, "z"], [120328, 1, "a"], [120329, 1, "b"], [120330, 1, "c"], [120331, 1, "d"], [120332, 1, "e"], [120333, 1, "f"], [120334, 1, "g"], [120335, 1, "h"], [120336, 1, "i"], [120337, 1, "j"], [120338, 1, "k"], [120339, 1, "l"], [120340, 1, "m"], [120341, 1, "n"], [120342, 1, "o"], [120343, 1, "p"], [120344, 1, "q"], [120345, 1, "r"], [120346, 1, "s"], [120347, 1, "t"], [120348, 1, "u"], [120349, 1, "v"], [120350, 1, "w"], [120351, 1, "x"], [120352, 1, "y"], [120353, 1, "z"], [120354, 1, "a"], [120355, 1, "b"], [120356, 1, "c"], [120357, 1, "d"], [120358, 1, "e"], [120359, 1, "f"], [120360, 1, "g"], [120361, 1, "h"], [120362, 1, "i"], [120363, 1, "j"], [120364, 1, "k"], [120365, 1, "l"], [120366, 1, "m"], [120367, 1, "n"], [120368, 1, "o"], [120369, 1, "p"], [120370, 1, "q"], [120371, 1, "r"], [120372, 1, "s"], [120373, 1, "t"], [120374, 1, "u"], [120375, 1, "v"], [120376, 1, "w"], [120377, 1, "x"], [120378, 1, "y"], [120379, 1, "z"], [120380, 1, "a"], [120381, 1, "b"], [120382, 1, "c"], [120383, 1, "d"], [120384, 1, "e"], [120385, 1, "f"], [120386, 1, "g"], [120387, 1, "h"], [120388, 1, "i"], [120389, 1, "j"], [120390, 1, "k"], [120391, 1, "l"], [120392, 1, "m"], [120393, 1, "n"], [120394, 1, "o"], [120395, 1, "p"], [120396, 1, "q"], [120397, 1, "r"], [120398, 1, "s"], [120399, 1, "t"], [120400, 1, "u"], [120401, 1, "v"], [120402, 1, "w"], [120403, 1, "x"], [120404, 1, "y"], [120405, 1, "z"], [120406, 1, "a"], [120407, 1, "b"], [120408, 1, "c"], [120409, 1, "d"], [120410, 1, "e"], [120411, 1, "f"], [120412, 1, "g"], [120413, 1, "h"], [120414, 1, "i"], [120415, 1, "j"], [120416, 1, "k"], [120417, 1, "l"], [120418, 1, "m"], [120419, 1, "n"], [120420, 1, "o"], [120421, 1, "p"], [120422, 1, "q"], [120423, 1, "r"], [120424, 1, "s"], [120425, 1, "t"], [120426, 1, "u"], [120427, 1, "v"], [120428, 1, "w"], [120429, 1, "x"], [120430, 1, "y"], [120431, 1, "z"], [120432, 1, "a"], [120433, 1, "b"], [120434, 1, "c"], [120435, 1, "d"], [120436, 1, "e"], [120437, 1, "f"], [120438, 1, "g"], [120439, 1, "h"], [120440, 1, "i"], [120441, 1, "j"], [120442, 1, "k"], [120443, 1, "l"], [120444, 1, "m"], [120445, 1, "n"], [120446, 1, "o"], [120447, 1, "p"], [120448, 1, "q"], [120449, 1, "r"], [120450, 1, "s"], [120451, 1, "t"], [120452, 1, "u"], [120453, 1, "v"], [120454, 1, "w"], [120455, 1, "x"], [120456, 1, "y"], [120457, 1, "z"], [120458, 1, "a"], [120459, 1, "b"], [120460, 1, "c"], [120461, 1, "d"], [120462, 1, "e"], [120463, 1, "f"], [120464, 1, "g"], [120465, 1, "h"], [120466, 1, "i"], [120467, 1, "j"], [120468, 1, "k"], [120469, 1, "l"], [120470, 1, "m"], [120471, 1, "n"], [120472, 1, "o"], [120473, 1, "p"], [120474, 1, "q"], [120475, 1, "r"], [120476, 1, "s"], [120477, 1, "t"], [120478, 1, "u"], [120479, 1, "v"], [120480, 1, "w"], [120481, 1, "x"], [120482, 1, "y"], [120483, 1, "z"], [120484, 1, "\u0131"], [120485, 1, "\u0237"], [[120486, 120487], 3], [120488, 1, "\u03B1"], [120489, 1, "\u03B2"], [120490, 1, "\u03B3"], [120491, 1, "\u03B4"], [120492, 1, "\u03B5"], [120493, 1, "\u03B6"], [120494, 1, "\u03B7"], [120495, 1, "\u03B8"], [120496, 1, "\u03B9"], [120497, 1, "\u03BA"], [120498, 1, "\u03BB"], [120499, 1, "\u03BC"], [120500, 1, "\u03BD"], [120501, 1, "\u03BE"], [120502, 1, "\u03BF"], [120503, 1, "\u03C0"], [120504, 1, "\u03C1"], [120505, 1, "\u03B8"], [120506, 1, "\u03C3"], [120507, 1, "\u03C4"], [120508, 1, "\u03C5"], [120509, 1, "\u03C6"], [120510, 1, "\u03C7"], [120511, 1, "\u03C8"], [120512, 1, "\u03C9"], [120513, 1, "\u2207"], [120514, 1, "\u03B1"], [120515, 1, "\u03B2"], [120516, 1, "\u03B3"], [120517, 1, "\u03B4"], [120518, 1, "\u03B5"], [120519, 1, "\u03B6"], [120520, 1, "\u03B7"], [120521, 1, "\u03B8"], [120522, 1, "\u03B9"], [120523, 1, "\u03BA"], [120524, 1, "\u03BB"], [120525, 1, "\u03BC"], [120526, 1, "\u03BD"], [120527, 1, "\u03BE"], [120528, 1, "\u03BF"], [120529, 1, "\u03C0"], [120530, 1, "\u03C1"], [[120531, 120532], 1, "\u03C3"], [120533, 1, "\u03C4"], [120534, 1, "\u03C5"], [120535, 1, "\u03C6"], [120536, 1, "\u03C7"], [120537, 1, "\u03C8"], [120538, 1, "\u03C9"], [120539, 1, "\u2202"], [120540, 1, "\u03B5"], [120541, 1, "\u03B8"], [120542, 1, "\u03BA"], [120543, 1, "\u03C6"], [120544, 1, "\u03C1"], [120545, 1, "\u03C0"], [120546, 1, "\u03B1"], [120547, 1, "\u03B2"], [120548, 1, "\u03B3"], [120549, 1, "\u03B4"], [120550, 1, "\u03B5"], [120551, 1, "\u03B6"], [120552, 1, "\u03B7"], [120553, 1, "\u03B8"], [120554, 1, "\u03B9"], [120555, 1, "\u03BA"], [120556, 1, "\u03BB"], [120557, 1, "\u03BC"], [120558, 1, "\u03BD"], [120559, 1, "\u03BE"], [120560, 1, "\u03BF"], [120561, 1, "\u03C0"], [120562, 1, "\u03C1"], [120563, 1, "\u03B8"], [120564, 1, "\u03C3"], [120565, 1, "\u03C4"], [120566, 1, "\u03C5"], [120567, 1, "\u03C6"], [120568, 1, "\u03C7"], [120569, 1, "\u03C8"], [120570, 1, "\u03C9"], [120571, 1, "\u2207"], [120572, 1, "\u03B1"], [120573, 1, "\u03B2"], [120574, 1, "\u03B3"], [120575, 1, "\u03B4"], [120576, 1, "\u03B5"], [120577, 1, "\u03B6"], [120578, 1, "\u03B7"], [120579, 1, "\u03B8"], [120580, 1, "\u03B9"], [120581, 1, "\u03BA"], [120582, 1, "\u03BB"], [120583, 1, "\u03BC"], [120584, 1, "\u03BD"], [120585, 1, "\u03BE"], [120586, 1, "\u03BF"], [120587, 1, "\u03C0"], [120588, 1, "\u03C1"], [[120589, 120590], 1, "\u03C3"], [120591, 1, "\u03C4"], [120592, 1, "\u03C5"], [120593, 1, "\u03C6"], [120594, 1, "\u03C7"], [120595, 1, "\u03C8"], [120596, 1, "\u03C9"], [120597, 1, "\u2202"], [120598, 1, "\u03B5"], [120599, 1, "\u03B8"], [120600, 1, "\u03BA"], [120601, 1, "\u03C6"], [120602, 1, "\u03C1"], [120603, 1, "\u03C0"], [120604, 1, "\u03B1"], [120605, 1, "\u03B2"], [120606, 1, "\u03B3"], [120607, 1, "\u03B4"], [120608, 1, "\u03B5"], [120609, 1, "\u03B6"], [120610, 1, "\u03B7"], [120611, 1, "\u03B8"], [120612, 1, "\u03B9"], [120613, 1, "\u03BA"], [120614, 1, "\u03BB"], [120615, 1, "\u03BC"], [120616, 1, "\u03BD"], [120617, 1, "\u03BE"], [120618, 1, "\u03BF"], [120619, 1, "\u03C0"], [120620, 1, "\u03C1"], [120621, 1, "\u03B8"], [120622, 1, "\u03C3"], [120623, 1, "\u03C4"], [120624, 1, "\u03C5"], [120625, 1, "\u03C6"], [120626, 1, "\u03C7"], [120627, 1, "\u03C8"], [120628, 1, "\u03C9"], [120629, 1, "\u2207"], [120630, 1, "\u03B1"], [120631, 1, "\u03B2"], [120632, 1, "\u03B3"], [120633, 1, "\u03B4"], [120634, 1, "\u03B5"], [120635, 1, "\u03B6"], [120636, 1, "\u03B7"], [120637, 1, "\u03B8"], [120638, 1, "\u03B9"], [120639, 1, "\u03BA"], [120640, 1, "\u03BB"], [120641, 1, "\u03BC"], [120642, 1, "\u03BD"], [120643, 1, "\u03BE"], [120644, 1, "\u03BF"], [120645, 1, "\u03C0"], [120646, 1, "\u03C1"], [[120647, 120648], 1, "\u03C3"], [120649, 1, "\u03C4"], [120650, 1, "\u03C5"], [120651, 1, "\u03C6"], [120652, 1, "\u03C7"], [120653, 1, "\u03C8"], [120654, 1, "\u03C9"], [120655, 1, "\u2202"], [120656, 1, "\u03B5"], [120657, 1, "\u03B8"], [120658, 1, "\u03BA"], [120659, 1, "\u03C6"], [120660, 1, "\u03C1"], [120661, 1, "\u03C0"], [120662, 1, "\u03B1"], [120663, 1, "\u03B2"], [120664, 1, "\u03B3"], [120665, 1, "\u03B4"], [120666, 1, "\u03B5"], [120667, 1, "\u03B6"], [120668, 1, "\u03B7"], [120669, 1, "\u03B8"], [120670, 1, "\u03B9"], [120671, 1, "\u03BA"], [120672, 1, "\u03BB"], [120673, 1, "\u03BC"], [120674, 1, "\u03BD"], [120675, 1, "\u03BE"], [120676, 1, "\u03BF"], [120677, 1, "\u03C0"], [120678, 1, "\u03C1"], [120679, 1, "\u03B8"], [120680, 1, "\u03C3"], [120681, 1, "\u03C4"], [120682, 1, "\u03C5"], [120683, 1, "\u03C6"], [120684, 1, "\u03C7"], [120685, 1, "\u03C8"], [120686, 1, "\u03C9"], [120687, 1, "\u2207"], [120688, 1, "\u03B1"], [120689, 1, "\u03B2"], [120690, 1, "\u03B3"], [120691, 1, "\u03B4"], [120692, 1, "\u03B5"], [120693, 1, "\u03B6"], [120694, 1, "\u03B7"], [120695, 1, "\u03B8"], [120696, 1, "\u03B9"], [120697, 1, "\u03BA"], [120698, 1, "\u03BB"], [120699, 1, "\u03BC"], [120700, 1, "\u03BD"], [120701, 1, "\u03BE"], [120702, 1, "\u03BF"], [120703, 1, "\u03C0"], [120704, 1, "\u03C1"], [[120705, 120706], 1, "\u03C3"], [120707, 1, "\u03C4"], [120708, 1, "\u03C5"], [120709, 1, "\u03C6"], [120710, 1, "\u03C7"], [120711, 1, "\u03C8"], [120712, 1, "\u03C9"], [120713, 1, "\u2202"], [120714, 1, "\u03B5"], [120715, 1, "\u03B8"], [120716, 1, "\u03BA"], [120717, 1, "\u03C6"], [120718, 1, "\u03C1"], [120719, 1, "\u03C0"], [120720, 1, "\u03B1"], [120721, 1, "\u03B2"], [120722, 1, "\u03B3"], [120723, 1, "\u03B4"], [120724, 1, "\u03B5"], [120725, 1, "\u03B6"], [120726, 1, "\u03B7"], [120727, 1, "\u03B8"], [120728, 1, "\u03B9"], [120729, 1, "\u03BA"], [120730, 1, "\u03BB"], [120731, 1, "\u03BC"], [120732, 1, "\u03BD"], [120733, 1, "\u03BE"], [120734, 1, "\u03BF"], [120735, 1, "\u03C0"], [120736, 1, "\u03C1"], [120737, 1, "\u03B8"], [120738, 1, "\u03C3"], [120739, 1, "\u03C4"], [120740, 1, "\u03C5"], [120741, 1, "\u03C6"], [120742, 1, "\u03C7"], [120743, 1, "\u03C8"], [120744, 1, "\u03C9"], [120745, 1, "\u2207"], [120746, 1, "\u03B1"], [120747, 1, "\u03B2"], [120748, 1, "\u03B3"], [120749, 1, "\u03B4"], [120750, 1, "\u03B5"], [120751, 1, "\u03B6"], [120752, 1, "\u03B7"], [120753, 1, "\u03B8"], [120754, 1, "\u03B9"], [120755, 1, "\u03BA"], [120756, 1, "\u03BB"], [120757, 1, "\u03BC"], [120758, 1, "\u03BD"], [120759, 1, "\u03BE"], [120760, 1, "\u03BF"], [120761, 1, "\u03C0"], [120762, 1, "\u03C1"], [[120763, 120764], 1, "\u03C3"], [120765, 1, "\u03C4"], [120766, 1, "\u03C5"], [120767, 1, "\u03C6"], [120768, 1, "\u03C7"], [120769, 1, "\u03C8"], [120770, 1, "\u03C9"], [120771, 1, "\u2202"], [120772, 1, "\u03B5"], [120773, 1, "\u03B8"], [120774, 1, "\u03BA"], [120775, 1, "\u03C6"], [120776, 1, "\u03C1"], [120777, 1, "\u03C0"], [[120778, 120779], 1, "\u03DD"], [[120780, 120781], 3], [120782, 1, "0"], [120783, 1, "1"], [120784, 1, "2"], [120785, 1, "3"], [120786, 1, "4"], [120787, 1, "5"], [120788, 1, "6"], [120789, 1, "7"], [120790, 1, "8"], [120791, 1, "9"], [120792, 1, "0"], [120793, 1, "1"], [120794, 1, "2"], [120795, 1, "3"], [120796, 1, "4"], [120797, 1, "5"], [120798, 1, "6"], [120799, 1, "7"], [120800, 1, "8"], [120801, 1, "9"], [120802, 1, "0"], [120803, 1, "1"], [120804, 1, "2"], [120805, 1, "3"], [120806, 1, "4"], [120807, 1, "5"], [120808, 1, "6"], [120809, 1, "7"], [120810, 1, "8"], [120811, 1, "9"], [120812, 1, "0"], [120813, 1, "1"], [120814, 1, "2"], [120815, 1, "3"], [120816, 1, "4"], [120817, 1, "5"], [120818, 1, "6"], [120819, 1, "7"], [120820, 1, "8"], [120821, 1, "9"], [120822, 1, "0"], [120823, 1, "1"], [120824, 1, "2"], [120825, 1, "3"], [120826, 1, "4"], [120827, 1, "5"], [120828, 1, "6"], [120829, 1, "7"], [120830, 1, "8"], [120831, 1, "9"], [[120832, 121343], 2], [[121344, 121398], 2], [[121399, 121402], 2], [[121403, 121452], 2], [[121453, 121460], 2], [121461, 2], [[121462, 121475], 2], [121476, 2], [[121477, 121483], 2], [[121484, 121498], 3], [[121499, 121503], 2], [121504, 3], [[121505, 121519], 2], [[121520, 122623], 3], [[122624, 122654], 2], [[122655, 122660], 3], [[122661, 122666], 2], [[122667, 122879], 3], [[122880, 122886], 2], [122887, 3], [[122888, 122904], 2], [[122905, 122906], 3], [[122907, 122913], 2], [122914, 3], [[122915, 122916], 2], [122917, 3], [[122918, 122922], 2], [[122923, 122927], 3], [122928, 1, "\u0430"], [122929, 1, "\u0431"], [122930, 1, "\u0432"], [122931, 1, "\u0433"], [122932, 1, "\u0434"], [122933, 1, "\u0435"], [122934, 1, "\u0436"], [122935, 1, "\u0437"], [122936, 1, "\u0438"], [122937, 1, "\u043A"], [122938, 1, "\u043B"], [122939, 1, "\u043C"], [122940, 1, "\u043E"], [122941, 1, "\u043F"], [122942, 1, "\u0440"], [122943, 1, "\u0441"], [122944, 1, "\u0442"], [122945, 1, "\u0443"], [122946, 1, "\u0444"], [122947, 1, "\u0445"], [122948, 1, "\u0446"], [122949, 1, "\u0447"], [122950, 1, "\u0448"], [122951, 1, "\u044B"], [122952, 1, "\u044D"], [122953, 1, "\u044E"], [122954, 1, "\uA689"], [122955, 1, "\u04D9"], [122956, 1, "\u0456"], [122957, 1, "\u0458"], [122958, 1, "\u04E9"], [122959, 1, "\u04AF"], [122960, 1, "\u04CF"], [122961, 1, "\u0430"], [122962, 1, "\u0431"], [122963, 1, "\u0432"], [122964, 1, "\u0433"], [122965, 1, "\u0434"], [122966, 1, "\u0435"], [122967, 1, "\u0436"], [122968, 1, "\u0437"], [122969, 1, "\u0438"], [122970, 1, "\u043A"], [122971, 1, "\u043B"], [122972, 1, "\u043E"], [122973, 1, "\u043F"], [122974, 1, "\u0441"], [122975, 1, "\u0443"], [122976, 1, "\u0444"], [122977, 1, "\u0445"], [122978, 1, "\u0446"], [122979, 1, "\u0447"], [122980, 1, "\u0448"], [122981, 1, "\u044A"], [122982, 1, "\u044B"], [122983, 1, "\u0491"], [122984, 1, "\u0456"], [122985, 1, "\u0455"], [122986, 1, "\u045F"], [122987, 1, "\u04AB"], [122988, 1, "\uA651"], [122989, 1, "\u04B1"], [[122990, 123022], 3], [123023, 2], [[123024, 123135], 3], [[123136, 123180], 2], [[123181, 123183], 3], [[123184, 123197], 2], [[123198, 123199], 3], [[123200, 123209], 2], [[123210, 123213], 3], [123214, 2], [123215, 2], [[123216, 123535], 3], [[123536, 123566], 2], [[123567, 123583], 3], [[123584, 123641], 2], [[123642, 123646], 3], [123647, 2], [[123648, 124111], 3], [[124112, 124153], 2], [[124154, 124895], 3], [[124896, 124902], 2], [124903, 3], [[124904, 124907], 2], [124908, 3], [[124909, 124910], 2], [124911, 3], [[124912, 124926], 2], [124927, 3], [[124928, 125124], 2], [[125125, 125126], 3], [[125127, 125135], 2], [[125136, 125142], 2], [[125143, 125183], 3], [125184, 1, "\u{1E922}"], [125185, 1, "\u{1E923}"], [125186, 1, "\u{1E924}"], [125187, 1, "\u{1E925}"], [125188, 1, "\u{1E926}"], [125189, 1, "\u{1E927}"], [125190, 1, "\u{1E928}"], [125191, 1, "\u{1E929}"], [125192, 1, "\u{1E92A}"], [125193, 1, "\u{1E92B}"], [125194, 1, "\u{1E92C}"], [125195, 1, "\u{1E92D}"], [125196, 1, "\u{1E92E}"], [125197, 1, "\u{1E92F}"], [125198, 1, "\u{1E930}"], [125199, 1, "\u{1E931}"], [125200, 1, "\u{1E932}"], [125201, 1, "\u{1E933}"], [125202, 1, "\u{1E934}"], [125203, 1, "\u{1E935}"], [125204, 1, "\u{1E936}"], [125205, 1, "\u{1E937}"], [125206, 1, "\u{1E938}"], [125207, 1, "\u{1E939}"], [125208, 1, "\u{1E93A}"], [125209, 1, "\u{1E93B}"], [125210, 1, "\u{1E93C}"], [125211, 1, "\u{1E93D}"], [125212, 1, "\u{1E93E}"], [125213, 1, "\u{1E93F}"], [125214, 1, "\u{1E940}"], [125215, 1, "\u{1E941}"], [125216, 1, "\u{1E942}"], [125217, 1, "\u{1E943}"], [[125218, 125258], 2], [125259, 2], [[125260, 125263], 3], [[125264, 125273], 2], [[125274, 125277], 3], [[125278, 125279], 2], [[125280, 126064], 3], [[126065, 126132], 2], [[126133, 126208], 3], [[126209, 126269], 2], [[126270, 126463], 3], [126464, 1, "\u0627"], [126465, 1, "\u0628"], [126466, 1, "\u062C"], [126467, 1, "\u062F"], [126468, 3], [126469, 1, "\u0648"], [126470, 1, "\u0632"], [126471, 1, "\u062D"], [126472, 1, "\u0637"], [126473, 1, "\u064A"], [126474, 1, "\u0643"], [126475, 1, "\u0644"], [126476, 1, "\u0645"], [126477, 1, "\u0646"], [126478, 1, "\u0633"], [126479, 1, "\u0639"], [126480, 1, "\u0641"], [126481, 1, "\u0635"], [126482, 1, "\u0642"], [126483, 1, "\u0631"], [126484, 1, "\u0634"], [126485, 1, "\u062A"], [126486, 1, "\u062B"], [126487, 1, "\u062E"], [126488, 1, "\u0630"], [126489, 1, "\u0636"], [126490, 1, "\u0638"], [126491, 1, "\u063A"], [126492, 1, "\u066E"], [126493, 1, "\u06BA"], [126494, 1, "\u06A1"], [126495, 1, "\u066F"], [126496, 3], [126497, 1, "\u0628"], [126498, 1, "\u062C"], [126499, 3], [126500, 1, "\u0647"], [[126501, 126502], 3], [126503, 1, "\u062D"], [126504, 3], [126505, 1, "\u064A"], [126506, 1, "\u0643"], [126507, 1, "\u0644"], [126508, 1, "\u0645"], [126509, 1, "\u0646"], [126510, 1, "\u0633"], [126511, 1, "\u0639"], [126512, 1, "\u0641"], [126513, 1, "\u0635"], [126514, 1, "\u0642"], [126515, 3], [126516, 1, "\u0634"], [126517, 1, "\u062A"], [126518, 1, "\u062B"], [126519, 1, "\u062E"], [126520, 3], [126521, 1, "\u0636"], [126522, 3], [126523, 1, "\u063A"], [[126524, 126529], 3], [126530, 1, "\u062C"], [[126531, 126534], 3], [126535, 1, "\u062D"], [126536, 3], [126537, 1, "\u064A"], [126538, 3], [126539, 1, "\u0644"], [126540, 3], [126541, 1, "\u0646"], [126542, 1, "\u0633"], [126543, 1, "\u0639"], [126544, 3], [126545, 1, "\u0635"], [126546, 1, "\u0642"], [126547, 3], [126548, 1, "\u0634"], [[126549, 126550], 3], [126551, 1, "\u062E"], [126552, 3], [126553, 1, "\u0636"], [126554, 3], [126555, 1, "\u063A"], [126556, 3], [126557, 1, "\u06BA"], [126558, 3], [126559, 1, "\u066F"], [126560, 3], [126561, 1, "\u0628"], [126562, 1, "\u062C"], [126563, 3], [126564, 1, "\u0647"], [[126565, 126566], 3], [126567, 1, "\u062D"], [126568, 1, "\u0637"], [126569, 1, "\u064A"], [126570, 1, "\u0643"], [126571, 3], [126572, 1, "\u0645"], [126573, 1, "\u0646"], [126574, 1, "\u0633"], [126575, 1, "\u0639"], [126576, 1, "\u0641"], [126577, 1, "\u0635"], [126578, 1, "\u0642"], [126579, 3], [126580, 1, "\u0634"], [126581, 1, "\u062A"], [126582, 1, "\u062B"], [126583, 1, "\u062E"], [126584, 3], [126585, 1, "\u0636"], [126586, 1, "\u0638"], [126587, 1, "\u063A"], [126588, 1, "\u066E"], [126589, 3], [126590, 1, "\u06A1"], [126591, 3], [126592, 1, "\u0627"], [126593, 1, "\u0628"], [126594, 1, "\u062C"], [126595, 1, "\u062F"], [126596, 1, "\u0647"], [126597, 1, "\u0648"], [126598, 1, "\u0632"], [126599, 1, "\u062D"], [126600, 1, "\u0637"], [126601, 1, "\u064A"], [126602, 3], [126603, 1, "\u0644"], [126604, 1, "\u0645"], [126605, 1, "\u0646"], [126606, 1, "\u0633"], [126607, 1, "\u0639"], [126608, 1, "\u0641"], [126609, 1, "\u0635"], [126610, 1, "\u0642"], [126611, 1, "\u0631"], [126612, 1, "\u0634"], [126613, 1, "\u062A"], [126614, 1, "\u062B"], [126615, 1, "\u062E"], [126616, 1, "\u0630"], [126617, 1, "\u0636"], [126618, 1, "\u0638"], [126619, 1, "\u063A"], [[126620, 126624], 3], [126625, 1, "\u0628"], [126626, 1, "\u062C"], [126627, 1, "\u062F"], [126628, 3], [126629, 1, "\u0648"], [126630, 1, "\u0632"], [126631, 1, "\u062D"], [126632, 1, "\u0637"], [126633, 1, "\u064A"], [126634, 3], [126635, 1, "\u0644"], [126636, 1, "\u0645"], [126637, 1, "\u0646"], [126638, 1, "\u0633"], [126639, 1, "\u0639"], [126640, 1, "\u0641"], [126641, 1, "\u0635"], [126642, 1, "\u0642"], [126643, 1, "\u0631"], [126644, 1, "\u0634"], [126645, 1, "\u062A"], [126646, 1, "\u062B"], [126647, 1, "\u062E"], [126648, 1, "\u0630"], [126649, 1, "\u0636"], [126650, 1, "\u0638"], [126651, 1, "\u063A"], [[126652, 126703], 3], [[126704, 126705], 2], [[126706, 126975], 3], [[126976, 127019], 2], [[127020, 127023], 3], [[127024, 127123], 2], [[127124, 127135], 3], [[127136, 127150], 2], [[127151, 127152], 3], [[127153, 127166], 2], [127167, 2], [127168, 3], [[127169, 127183], 2], [127184, 3], [[127185, 127199], 2], [[127200, 127221], 2], [[127222, 127231], 3], [127232, 3], [127233, 5, "0,"], [127234, 5, "1,"], [127235, 5, "2,"], [127236, 5, "3,"], [127237, 5, "4,"], [127238, 5, "5,"], [127239, 5, "6,"], [127240, 5, "7,"], [127241, 5, "8,"], [127242, 5, "9,"], [[127243, 127244], 2], [[127245, 127247], 2], [127248, 5, "(a)"], [127249, 5, "(b)"], [127250, 5, "(c)"], [127251, 5, "(d)"], [127252, 5, "(e)"], [127253, 5, "(f)"], [127254, 5, "(g)"], [127255, 5, "(h)"], [127256, 5, "(i)"], [127257, 5, "(j)"], [127258, 5, "(k)"], [127259, 5, "(l)"], [127260, 5, "(m)"], [127261, 5, "(n)"], [127262, 5, "(o)"], [127263, 5, "(p)"], [127264, 5, "(q)"], [127265, 5, "(r)"], [127266, 5, "(s)"], [127267, 5, "(t)"], [127268, 5, "(u)"], [127269, 5, "(v)"], [127270, 5, "(w)"], [127271, 5, "(x)"], [127272, 5, "(y)"], [127273, 5, "(z)"], [127274, 1, "\u3014s\u3015"], [127275, 1, "c"], [127276, 1, "r"], [127277, 1, "cd"], [127278, 1, "wz"], [127279, 2], [127280, 1, "a"], [127281, 1, "b"], [127282, 1, "c"], [127283, 1, "d"], [127284, 1, "e"], [127285, 1, "f"], [127286, 1, "g"], [127287, 1, "h"], [127288, 1, "i"], [127289, 1, "j"], [127290, 1, "k"], [127291, 1, "l"], [127292, 1, "m"], [127293, 1, "n"], [127294, 1, "o"], [127295, 1, "p"], [127296, 1, "q"], [127297, 1, "r"], [127298, 1, "s"], [127299, 1, "t"], [127300, 1, "u"], [127301, 1, "v"], [127302, 1, "w"], [127303, 1, "x"], [127304, 1, "y"], [127305, 1, "z"], [127306, 1, "hv"], [127307, 1, "mv"], [127308, 1, "sd"], [127309, 1, "ss"], [127310, 1, "ppv"], [127311, 1, "wc"], [[127312, 127318], 2], [127319, 2], [[127320, 127326], 2], [127327, 2], [[127328, 127337], 2], [127338, 1, "mc"], [127339, 1, "md"], [127340, 1, "mr"], [[127341, 127343], 2], [[127344, 127352], 2], [127353, 2], [127354, 2], [[127355, 127356], 2], [[127357, 127358], 2], [127359, 2], [[127360, 127369], 2], [[127370, 127373], 2], [[127374, 127375], 2], [127376, 1, "dj"], [[127377, 127386], 2], [[127387, 127404], 2], [127405, 2], [[127406, 127461], 3], [[127462, 127487], 2], [127488, 1, "\u307B\u304B"], [127489, 1, "\u30B3\u30B3"], [127490, 1, "\u30B5"], [[127491, 127503], 3], [127504, 1, "\u624B"], [127505, 1, "\u5B57"], [127506, 1, "\u53CC"], [127507, 1, "\u30C7"], [127508, 1, "\u4E8C"], [127509, 1, "\u591A"], [127510, 1, "\u89E3"], [127511, 1, "\u5929"], [127512, 1, "\u4EA4"], [127513, 1, "\u6620"], [127514, 1, "\u7121"], [127515, 1, "\u6599"], [127516, 1, "\u524D"], [127517, 1, "\u5F8C"], [127518, 1, "\u518D"], [127519, 1, "\u65B0"], [127520, 1, "\u521D"], [127521, 1, "\u7D42"], [127522, 1, "\u751F"], [127523, 1, "\u8CA9"], [127524, 1, "\u58F0"], [127525, 1, "\u5439"], [127526, 1, "\u6F14"], [127527, 1, "\u6295"], [127528, 1, "\u6355"], [127529, 1, "\u4E00"], [127530, 1, "\u4E09"], [127531, 1, "\u904A"], [127532, 1, "\u5DE6"], [127533, 1, "\u4E2D"], [127534, 1, "\u53F3"], [127535, 1, "\u6307"], [127536, 1, "\u8D70"], [127537, 1, "\u6253"], [127538, 1, "\u7981"], [127539, 1, "\u7A7A"], [127540, 1, "\u5408"], [127541, 1, "\u6E80"], [127542, 1, "\u6709"], [127543, 1, "\u6708"], [127544, 1, "\u7533"], [127545, 1, "\u5272"], [127546, 1, "\u55B6"], [127547, 1, "\u914D"], [[127548, 127551], 3], [127552, 1, "\u3014\u672C\u3015"], [127553, 1, "\u3014\u4E09\u3015"], [127554, 1, "\u3014\u4E8C\u3015"], [127555, 1, "\u3014\u5B89\u3015"], [127556, 1, "\u3014\u70B9\u3015"], [127557, 1, "\u3014\u6253\u3015"], [127558, 1, "\u3014\u76D7\u3015"], [127559, 1, "\u3014\u52DD\u3015"], [127560, 1, "\u3014\u6557\u3015"], [[127561, 127567], 3], [127568, 1, "\u5F97"], [127569, 1, "\u53EF"], [[127570, 127583], 3], [[127584, 127589], 2], [[127590, 127743], 3], [[127744, 127776], 2], [[127777, 127788], 2], [[127789, 127791], 2], [[127792, 127797], 2], [127798, 2], [[127799, 127868], 2], [127869, 2], [[127870, 127871], 2], [[127872, 127891], 2], [[127892, 127903], 2], [[127904, 127940], 2], [127941, 2], [[127942, 127946], 2], [[127947, 127950], 2], [[127951, 127955], 2], [[127956, 127967], 2], [[127968, 127984], 2], [[127985, 127991], 2], [[127992, 127999], 2], [[128e3, 128062], 2], [128063, 2], [128064, 2], [128065, 2], [[128066, 128247], 2], [128248, 2], [[128249, 128252], 2], [[128253, 128254], 2], [128255, 2], [[128256, 128317], 2], [[128318, 128319], 2], [[128320, 128323], 2], [[128324, 128330], 2], [[128331, 128335], 2], [[128336, 128359], 2], [[128360, 128377], 2], [128378, 2], [[128379, 128419], 2], [128420, 2], [[128421, 128506], 2], [[128507, 128511], 2], [128512, 2], [[128513, 128528], 2], [128529, 2], [[128530, 128532], 2], [128533, 2], [128534, 2], [128535, 2], [128536, 2], [128537, 2], [128538, 2], [128539, 2], [[128540, 128542], 2], [128543, 2], [[128544, 128549], 2], [[128550, 128551], 2], [[128552, 128555], 2], [128556, 2], [128557, 2], [[128558, 128559], 2], [[128560, 128563], 2], [128564, 2], [[128565, 128576], 2], [[128577, 128578], 2], [[128579, 128580], 2], [[128581, 128591], 2], [[128592, 128639], 2], [[128640, 128709], 2], [[128710, 128719], 2], [128720, 2], [[128721, 128722], 2], [[128723, 128724], 2], [128725, 2], [[128726, 128727], 2], [[128728, 128731], 3], [128732, 2], [[128733, 128735], 2], [[128736, 128748], 2], [[128749, 128751], 3], [[128752, 128755], 2], [[128756, 128758], 2], [[128759, 128760], 2], [128761, 2], [128762, 2], [[128763, 128764], 2], [[128765, 128767], 3], [[128768, 128883], 2], [[128884, 128886], 2], [[128887, 128890], 3], [[128891, 128895], 2], [[128896, 128980], 2], [[128981, 128984], 2], [128985, 2], [[128986, 128991], 3], [[128992, 129003], 2], [[129004, 129007], 3], [129008, 2], [[129009, 129023], 3], [[129024, 129035], 2], [[129036, 129039], 3], [[129040, 129095], 2], [[129096, 129103], 3], [[129104, 129113], 2], [[129114, 129119], 3], [[129120, 129159], 2], [[129160, 129167], 3], [[129168, 129197], 2], [[129198, 129199], 3], [[129200, 129201], 2], [[129202, 129279], 3], [[129280, 129291], 2], [129292, 2], [[129293, 129295], 2], [[129296, 129304], 2], [[129305, 129310], 2], [129311, 2], [[129312, 129319], 2], [[129320, 129327], 2], [129328, 2], [[129329, 129330], 2], [[129331, 129342], 2], [129343, 2], [[129344, 129355], 2], [129356, 2], [[129357, 129359], 2], [[129360, 129374], 2], [[129375, 129387], 2], [[129388, 129392], 2], [129393, 2], [129394, 2], [[129395, 129398], 2], [[129399, 129400], 2], [129401, 2], [129402, 2], [129403, 2], [[129404, 129407], 2], [[129408, 129412], 2], [[129413, 129425], 2], [[129426, 129431], 2], [[129432, 129442], 2], [[129443, 129444], 2], [[129445, 129450], 2], [[129451, 129453], 2], [[129454, 129455], 2], [[129456, 129465], 2], [[129466, 129471], 2], [129472, 2], [[129473, 129474], 2], [[129475, 129482], 2], [129483, 2], [129484, 2], [[129485, 129487], 2], [[129488, 129510], 2], [[129511, 129535], 2], [[129536, 129619], 2], [[129620, 129631], 3], [[129632, 129645], 2], [[129646, 129647], 3], [[129648, 129651], 2], [129652, 2], [[129653, 129655], 2], [[129656, 129658], 2], [[129659, 129660], 2], [[129661, 129663], 3], [[129664, 129666], 2], [[129667, 129670], 2], [[129671, 129672], 2], [[129673, 129679], 3], [[129680, 129685], 2], [[129686, 129704], 2], [[129705, 129708], 2], [[129709, 129711], 2], [[129712, 129718], 2], [[129719, 129722], 2], [[129723, 129725], 2], [129726, 3], [129727, 2], [[129728, 129730], 2], [[129731, 129733], 2], [[129734, 129741], 3], [[129742, 129743], 2], [[129744, 129750], 2], [[129751, 129753], 2], [[129754, 129755], 2], [[129756, 129759], 3], [[129760, 129767], 2], [129768, 2], [[129769, 129775], 3], [[129776, 129782], 2], [[129783, 129784], 2], [[129785, 129791], 3], [[129792, 129938], 2], [129939, 3], [[129940, 129994], 2], [[129995, 130031], 3], [130032, 1, "0"], [130033, 1, "1"], [130034, 1, "2"], [130035, 1, "3"], [130036, 1, "4"], [130037, 1, "5"], [130038, 1, "6"], [130039, 1, "7"], [130040, 1, "8"], [130041, 1, "9"], [[130042, 131069], 3], [[131070, 131071], 3], [[131072, 173782], 2], [[173783, 173789], 2], [[173790, 173791], 2], [[173792, 173823], 3], [[173824, 177972], 2], [[177973, 177976], 2], [177977, 2], [[177978, 177983], 3], [[177984, 178205], 2], [[178206, 178207], 3], [[178208, 183969], 2], [[183970, 183983], 3], [[183984, 191456], 2], [[191457, 194559], 3], [194560, 1, "\u4E3D"], [194561, 1, "\u4E38"], [194562, 1, "\u4E41"], [194563, 1, "\u{20122}"], [194564, 1, "\u4F60"], [194565, 1, "\u4FAE"], [194566, 1, "\u4FBB"], [194567, 1, "\u5002"], [194568, 1, "\u507A"], [194569, 1, "\u5099"], [194570, 1, "\u50E7"], [194571, 1, "\u50CF"], [194572, 1, "\u349E"], [194573, 1, "\u{2063A}"], [194574, 1, "\u514D"], [194575, 1, "\u5154"], [194576, 1, "\u5164"], [194577, 1, "\u5177"], [194578, 1, "\u{2051C}"], [194579, 1, "\u34B9"], [194580, 1, "\u5167"], [194581, 1, "\u518D"], [194582, 1, "\u{2054B}"], [194583, 1, "\u5197"], [194584, 1, "\u51A4"], [194585, 1, "\u4ECC"], [194586, 1, "\u51AC"], [194587, 1, "\u51B5"], [194588, 1, "\u{291DF}"], [194589, 1, "\u51F5"], [194590, 1, "\u5203"], [194591, 1, "\u34DF"], [194592, 1, "\u523B"], [194593, 1, "\u5246"], [194594, 1, "\u5272"], [194595, 1, "\u5277"], [194596, 1, "\u3515"], [194597, 1, "\u52C7"], [194598, 1, "\u52C9"], [194599, 1, "\u52E4"], [194600, 1, "\u52FA"], [194601, 1, "\u5305"], [194602, 1, "\u5306"], [194603, 1, "\u5317"], [194604, 1, "\u5349"], [194605, 1, "\u5351"], [194606, 1, "\u535A"], [194607, 1, "\u5373"], [194608, 1, "\u537D"], [[194609, 194611], 1, "\u537F"], [194612, 1, "\u{20A2C}"], [194613, 1, "\u7070"], [194614, 1, "\u53CA"], [194615, 1, "\u53DF"], [194616, 1, "\u{20B63}"], [194617, 1, "\u53EB"], [194618, 1, "\u53F1"], [194619, 1, "\u5406"], [194620, 1, "\u549E"], [194621, 1, "\u5438"], [194622, 1, "\u5448"], [194623, 1, "\u5468"], [194624, 1, "\u54A2"], [194625, 1, "\u54F6"], [194626, 1, "\u5510"], [194627, 1, "\u5553"], [194628, 1, "\u5563"], [[194629, 194630], 1, "\u5584"], [194631, 1, "\u5599"], [194632, 1, "\u55AB"], [194633, 1, "\u55B3"], [194634, 1, "\u55C2"], [194635, 1, "\u5716"], [194636, 1, "\u5606"], [194637, 1, "\u5717"], [194638, 1, "\u5651"], [194639, 1, "\u5674"], [194640, 1, "\u5207"], [194641, 1, "\u58EE"], [194642, 1, "\u57CE"], [194643, 1, "\u57F4"], [194644, 1, "\u580D"], [194645, 1, "\u578B"], [194646, 1, "\u5832"], [194647, 1, "\u5831"], [194648, 1, "\u58AC"], [194649, 1, "\u{214E4}"], [194650, 1, "\u58F2"], [194651, 1, "\u58F7"], [194652, 1, "\u5906"], [194653, 1, "\u591A"], [194654, 1, "\u5922"], [194655, 1, "\u5962"], [194656, 1, "\u{216A8}"], [194657, 1, "\u{216EA}"], [194658, 1, "\u59EC"], [194659, 1, "\u5A1B"], [194660, 1, "\u5A27"], [194661, 1, "\u59D8"], [194662, 1, "\u5A66"], [194663, 1, "\u36EE"], [194664, 3], [194665, 1, "\u5B08"], [[194666, 194667], 1, "\u5B3E"], [194668, 1, "\u{219C8}"], [194669, 1, "\u5BC3"], [194670, 1, "\u5BD8"], [194671, 1, "\u5BE7"], [194672, 1, "\u5BF3"], [194673, 1, "\u{21B18}"], [194674, 1, "\u5BFF"], [194675, 1, "\u5C06"], [194676, 3], [194677, 1, "\u5C22"], [194678, 1, "\u3781"], [194679, 1, "\u5C60"], [194680, 1, "\u5C6E"], [194681, 1, "\u5CC0"], [194682, 1, "\u5C8D"], [194683, 1, "\u{21DE4}"], [194684, 1, "\u5D43"], [194685, 1, "\u{21DE6}"], [194686, 1, "\u5D6E"], [194687, 1, "\u5D6B"], [194688, 1, "\u5D7C"], [194689, 1, "\u5DE1"], [194690, 1, "\u5DE2"], [194691, 1, "\u382F"], [194692, 1, "\u5DFD"], [194693, 1, "\u5E28"], [194694, 1, "\u5E3D"], [194695, 1, "\u5E69"], [194696, 1, "\u3862"], [194697, 1, "\u{22183}"], [194698, 1, "\u387C"], [194699, 1, "\u5EB0"], [194700, 1, "\u5EB3"], [194701, 1, "\u5EB6"], [194702, 1, "\u5ECA"], [194703, 1, "\u{2A392}"], [194704, 1, "\u5EFE"], [[194705, 194706], 1, "\u{22331}"], [194707, 1, "\u8201"], [[194708, 194709], 1, "\u5F22"], [194710, 1, "\u38C7"], [194711, 1, "\u{232B8}"], [194712, 1, "\u{261DA}"], [194713, 1, "\u5F62"], [194714, 1, "\u5F6B"], [194715, 1, "\u38E3"], [194716, 1, "\u5F9A"], [194717, 1, "\u5FCD"], [194718, 1, "\u5FD7"], [194719, 1, "\u5FF9"], [194720, 1, "\u6081"], [194721, 1, "\u393A"], [194722, 1, "\u391C"], [194723, 1, "\u6094"], [194724, 1, "\u{226D4}"], [194725, 1, "\u60C7"], [194726, 1, "\u6148"], [194727, 1, "\u614C"], [194728, 1, "\u614E"], [194729, 1, "\u614C"], [194730, 1, "\u617A"], [194731, 1, "\u618E"], [194732, 1, "\u61B2"], [194733, 1, "\u61A4"], [194734, 1, "\u61AF"], [194735, 1, "\u61DE"], [194736, 1, "\u61F2"], [194737, 1, "\u61F6"], [194738, 1, "\u6210"], [194739, 1, "\u621B"], [194740, 1, "\u625D"], [194741, 1, "\u62B1"], [194742, 1, "\u62D4"], [194743, 1, "\u6350"], [194744, 1, "\u{22B0C}"], [194745, 1, "\u633D"], [194746, 1, "\u62FC"], [194747, 1, "\u6368"], [194748, 1, "\u6383"], [194749, 1, "\u63E4"], [194750, 1, "\u{22BF1}"], [194751, 1, "\u6422"], [194752, 1, "\u63C5"], [194753, 1, "\u63A9"], [194754, 1, "\u3A2E"], [194755, 1, "\u6469"], [194756, 1, "\u647E"], [194757, 1, "\u649D"], [194758, 1, "\u6477"], [194759, 1, "\u3A6C"], [194760, 1, "\u654F"], [194761, 1, "\u656C"], [194762, 1, "\u{2300A}"], [194763, 1, "\u65E3"], [194764, 1, "\u66F8"], [194765, 1, "\u6649"], [194766, 1, "\u3B19"], [194767, 1, "\u6691"], [194768, 1, "\u3B08"], [194769, 1, "\u3AE4"], [194770, 1, "\u5192"], [194771, 1, "\u5195"], [194772, 1, "\u6700"], [194773, 1, "\u669C"], [194774, 1, "\u80AD"], [194775, 1, "\u43D9"], [194776, 1, "\u6717"], [194777, 1, "\u671B"], [194778, 1, "\u6721"], [194779, 1, "\u675E"], [194780, 1, "\u6753"], [194781, 1, "\u{233C3}"], [194782, 1, "\u3B49"], [194783, 1, "\u67FA"], [194784, 1, "\u6785"], [194785, 1, "\u6852"], [194786, 1, "\u6885"], [194787, 1, "\u{2346D}"], [194788, 1, "\u688E"], [194789, 1, "\u681F"], [194790, 1, "\u6914"], [194791, 1, "\u3B9D"], [194792, 1, "\u6942"], [194793, 1, "\u69A3"], [194794, 1, "\u69EA"], [194795, 1, "\u6AA8"], [194796, 1, "\u{236A3}"], [194797, 1, "\u6ADB"], [194798, 1, "\u3C18"], [194799, 1, "\u6B21"], [194800, 1, "\u{238A7}"], [194801, 1, "\u6B54"], [194802, 1, "\u3C4E"], [194803, 1, "\u6B72"], [194804, 1, "\u6B9F"], [194805, 1, "\u6BBA"], [194806, 1, "\u6BBB"], [194807, 1, "\u{23A8D}"], [194808, 1, "\u{21D0B}"], [194809, 1, "\u{23AFA}"], [194810, 1, "\u6C4E"], [194811, 1, "\u{23CBC}"], [194812, 1, "\u6CBF"], [194813, 1, "\u6CCD"], [194814, 1, "\u6C67"], [194815, 1, "\u6D16"], [194816, 1, "\u6D3E"], [194817, 1, "\u6D77"], [194818, 1, "\u6D41"], [194819, 1, "\u6D69"], [194820, 1, "\u6D78"], [194821, 1, "\u6D85"], [194822, 1, "\u{23D1E}"], [194823, 1, "\u6D34"], [194824, 1, "\u6E2F"], [194825, 1, "\u6E6E"], [194826, 1, "\u3D33"], [194827, 1, "\u6ECB"], [194828, 1, "\u6EC7"], [194829, 1, "\u{23ED1}"], [194830, 1, "\u6DF9"], [194831, 1, "\u6F6E"], [194832, 1, "\u{23F5E}"], [194833, 1, "\u{23F8E}"], [194834, 1, "\u6FC6"], [194835, 1, "\u7039"], [194836, 1, "\u701E"], [194837, 1, "\u701B"], [194838, 1, "\u3D96"], [194839, 1, "\u704A"], [194840, 1, "\u707D"], [194841, 1, "\u7077"], [194842, 1, "\u70AD"], [194843, 1, "\u{20525}"], [194844, 1, "\u7145"], [194845, 1, "\u{24263}"], [194846, 1, "\u719C"], [194847, 3], [194848, 1, "\u7228"], [194849, 1, "\u7235"], [194850, 1, "\u7250"], [194851, 1, "\u{24608}"], [194852, 1, "\u7280"], [194853, 1, "\u7295"], [194854, 1, "\u{24735}"], [194855, 1, "\u{24814}"], [194856, 1, "\u737A"], [194857, 1, "\u738B"], [194858, 1, "\u3EAC"], [194859, 1, "\u73A5"], [[194860, 194861], 1, "\u3EB8"], [194862, 1, "\u7447"], [194863, 1, "\u745C"], [194864, 1, "\u7471"], [194865, 1, "\u7485"], [194866, 1, "\u74CA"], [194867, 1, "\u3F1B"], [194868, 1, "\u7524"], [194869, 1, "\u{24C36}"], [194870, 1, "\u753E"], [194871, 1, "\u{24C92}"], [194872, 1, "\u7570"], [194873, 1, "\u{2219F}"], [194874, 1, "\u7610"], [194875, 1, "\u{24FA1}"], [194876, 1, "\u{24FB8}"], [194877, 1, "\u{25044}"], [194878, 1, "\u3FFC"], [194879, 1, "\u4008"], [194880, 1, "\u76F4"], [194881, 1, "\u{250F3}"], [194882, 1, "\u{250F2}"], [194883, 1, "\u{25119}"], [194884, 1, "\u{25133}"], [194885, 1, "\u771E"], [[194886, 194887], 1, "\u771F"], [194888, 1, "\u774A"], [194889, 1, "\u4039"], [194890, 1, "\u778B"], [194891, 1, "\u4046"], [194892, 1, "\u4096"], [194893, 1, "\u{2541D}"], [194894, 1, "\u784E"], [194895, 1, "\u788C"], [194896, 1, "\u78CC"], [194897, 1, "\u40E3"], [194898, 1, "\u{25626}"], [194899, 1, "\u7956"], [194900, 1, "\u{2569A}"], [194901, 1, "\u{256C5}"], [194902, 1, "\u798F"], [194903, 1, "\u79EB"], [194904, 1, "\u412F"], [194905, 1, "\u7A40"], [194906, 1, "\u7A4A"], [194907, 1, "\u7A4F"], [194908, 1, "\u{2597C}"], [[194909, 194910], 1, "\u{25AA7}"], [194911, 3], [194912, 1, "\u4202"], [194913, 1, "\u{25BAB}"], [194914, 1, "\u7BC6"], [194915, 1, "\u7BC9"], [194916, 1, "\u4227"], [194917, 1, "\u{25C80}"], [194918, 1, "\u7CD2"], [194919, 1, "\u42A0"], [194920, 1, "\u7CE8"], [194921, 1, "\u7CE3"], [194922, 1, "\u7D00"], [194923, 1, "\u{25F86}"], [194924, 1, "\u7D63"], [194925, 1, "\u4301"], [194926, 1, "\u7DC7"], [194927, 1, "\u7E02"], [194928, 1, "\u7E45"], [194929, 1, "\u4334"], [194930, 1, "\u{26228}"], [194931, 1, "\u{26247}"], [194932, 1, "\u4359"], [194933, 1, "\u{262D9}"], [194934, 1, "\u7F7A"], [194935, 1, "\u{2633E}"], [194936, 1, "\u7F95"], [194937, 1, "\u7FFA"], [194938, 1, "\u8005"], [194939, 1, "\u{264DA}"], [194940, 1, "\u{26523}"], [194941, 1, "\u8060"], [194942, 1, "\u{265A8}"], [194943, 1, "\u8070"], [194944, 1, "\u{2335F}"], [194945, 1, "\u43D5"], [194946, 1, "\u80B2"], [194947, 1, "\u8103"], [194948, 1, "\u440B"], [194949, 1, "\u813E"], [194950, 1, "\u5AB5"], [194951, 1, "\u{267A7}"], [194952, 1, "\u{267B5}"], [194953, 1, "\u{23393}"], [194954, 1, "\u{2339C}"], [194955, 1, "\u8201"], [194956, 1, "\u8204"], [194957, 1, "\u8F9E"], [194958, 1, "\u446B"], [194959, 1, "\u8291"], [194960, 1, "\u828B"], [194961, 1, "\u829D"], [194962, 1, "\u52B3"], [194963, 1, "\u82B1"], [194964, 1, "\u82B3"], [194965, 1, "\u82BD"], [194966, 1, "\u82E6"], [194967, 1, "\u{26B3C}"], [194968, 1, "\u82E5"], [194969, 1, "\u831D"], [194970, 1, "\u8363"], [194971, 1, "\u83AD"], [194972, 1, "\u8323"], [194973, 1, "\u83BD"], [194974, 1, "\u83E7"], [194975, 1, "\u8457"], [194976, 1, "\u8353"], [194977, 1, "\u83CA"], [194978, 1, "\u83CC"], [194979, 1, "\u83DC"], [194980, 1, "\u{26C36}"], [194981, 1, "\u{26D6B}"], [194982, 1, "\u{26CD5}"], [194983, 1, "\u452B"], [194984, 1, "\u84F1"], [194985, 1, "\u84F3"], [194986, 1, "\u8516"], [194987, 1, "\u{273CA}"], [194988, 1, "\u8564"], [194989, 1, "\u{26F2C}"], [194990, 1, "\u455D"], [194991, 1, "\u4561"], [194992, 1, "\u{26FB1}"], [194993, 1, "\u{270D2}"], [194994, 1, "\u456B"], [194995, 1, "\u8650"], [194996, 1, "\u865C"], [194997, 1, "\u8667"], [194998, 1, "\u8669"], [194999, 1, "\u86A9"], [195e3, 1, "\u8688"], [195001, 1, "\u870E"], [195002, 1, "\u86E2"], [195003, 1, "\u8779"], [195004, 1, "\u8728"], [195005, 1, "\u876B"], [195006, 1, "\u8786"], [195007, 3], [195008, 1, "\u87E1"], [195009, 1, "\u8801"], [195010, 1, "\u45F9"], [195011, 1, "\u8860"], [195012, 1, "\u8863"], [195013, 1, "\u{27667}"], [195014, 1, "\u88D7"], [195015, 1, "\u88DE"], [195016, 1, "\u4635"], [195017, 1, "\u88FA"], [195018, 1, "\u34BB"], [195019, 1, "\u{278AE}"], [195020, 1, "\u{27966}"], [195021, 1, "\u46BE"], [195022, 1, "\u46C7"], [195023, 1, "\u8AA0"], [195024, 1, "\u8AED"], [195025, 1, "\u8B8A"], [195026, 1, "\u8C55"], [195027, 1, "\u{27CA8}"], [195028, 1, "\u8CAB"], [195029, 1, "\u8CC1"], [195030, 1, "\u8D1B"], [195031, 1, "\u8D77"], [195032, 1, "\u{27F2F}"], [195033, 1, "\u{20804}"], [195034, 1, "\u8DCB"], [195035, 1, "\u8DBC"], [195036, 1, "\u8DF0"], [195037, 1, "\u{208DE}"], [195038, 1, "\u8ED4"], [195039, 1, "\u8F38"], [195040, 1, "\u{285D2}"], [195041, 1, "\u{285ED}"], [195042, 1, "\u9094"], [195043, 1, "\u90F1"], [195044, 1, "\u9111"], [195045, 1, "\u{2872E}"], [195046, 1, "\u911B"], [195047, 1, "\u9238"], [195048, 1, "\u92D7"], [195049, 1, "\u92D8"], [195050, 1, "\u927C"], [195051, 1, "\u93F9"], [195052, 1, "\u9415"], [195053, 1, "\u{28BFA}"], [195054, 1, "\u958B"], [195055, 1, "\u4995"], [195056, 1, "\u95B7"], [195057, 1, "\u{28D77}"], [195058, 1, "\u49E6"], [195059, 1, "\u96C3"], [195060, 1, "\u5DB2"], [195061, 1, "\u9723"], [195062, 1, "\u{29145}"], [195063, 1, "\u{2921A}"], [195064, 1, "\u4A6E"], [195065, 1, "\u4A76"], [195066, 1, "\u97E0"], [195067, 1, "\u{2940A}"], [195068, 1, "\u4AB2"], [195069, 1, "\u{29496}"], [[195070, 195071], 1, "\u980B"], [195072, 1, "\u9829"], [195073, 1, "\u{295B6}"], [195074, 1, "\u98E2"], [195075, 1, "\u4B33"], [195076, 1, "\u9929"], [195077, 1, "\u99A7"], [195078, 1, "\u99C2"], [195079, 1, "\u99FE"], [195080, 1, "\u4BCE"], [195081, 1, "\u{29B30}"], [195082, 1, "\u9B12"], [195083, 1, "\u9C40"], [195084, 1, "\u9CFD"], [195085, 1, "\u4CCE"], [195086, 1, "\u4CED"], [195087, 1, "\u9D67"], [195088, 1, "\u{2A0CE}"], [195089, 1, "\u4CF8"], [195090, 1, "\u{2A105}"], [195091, 1, "\u{2A20E}"], [195092, 1, "\u{2A291}"], [195093, 1, "\u9EBB"], [195094, 1, "\u4D56"], [195095, 1, "\u9EF9"], [195096, 1, "\u9EFE"], [195097, 1, "\u9F05"], [195098, 1, "\u9F0F"], [195099, 1, "\u9F16"], [195100, 1, "\u9F3B"], [195101, 1, "\u{2A600}"], [[195102, 196605], 3], [[196606, 196607], 3], [[196608, 201546], 2], [[201547, 201551], 3], [[201552, 205743], 2], [[205744, 262141], 3], [[262142, 262143], 3], [[262144, 327677], 3], [[327678, 327679], 3], [[327680, 393213], 3], [[393214, 393215], 3], [[393216, 458749], 3], [[458750, 458751], 3], [[458752, 524285], 3], [[524286, 524287], 3], [[524288, 589821], 3], [[589822, 589823], 3], [[589824, 655357], 3], [[655358, 655359], 3], [[655360, 720893], 3], [[720894, 720895], 3], [[720896, 786429], 3], [[786430, 786431], 3], [[786432, 851965], 3], [[851966, 851967], 3], [[851968, 917501], 3], [[917502, 917503], 3], [917504, 3], [917505, 3], [[917506, 917535], 3], [[917536, 917631], 3], [[917632, 917759], 3], [[917760, 917999], 7], [[918e3, 983037], 3], [[983038, 983039], 3], [[983040, 1048573], 3], [[1048574, 1048575], 3], [[1048576, 1114109], 3], [[1114110, 1114111], 3]]; + module.exports = [[[0, 44], 4], [[45, 46], 2], [47, 4], [[48, 57], 2], [[58, 64], 4], [65, 1, "a"], [66, 1, "b"], [67, 1, "c"], [68, 1, "d"], [69, 1, "e"], [70, 1, "f"], [71, 1, "g"], [72, 1, "h"], [73, 1, "i"], [74, 1, "j"], [75, 1, "k"], [76, 1, "l"], [77, 1, "m"], [78, 1, "n"], [79, 1, "o"], [80, 1, "p"], [81, 1, "q"], [82, 1, "r"], [83, 1, "s"], [84, 1, "t"], [85, 1, "u"], [86, 1, "v"], [87, 1, "w"], [88, 1, "x"], [89, 1, "y"], [90, 1, "z"], [[91, 96], 4], [[97, 122], 2], [[123, 127], 4], [[128, 159], 3], [160, 5, " "], [[161, 167], 2], [168, 5, " \u0308"], [169, 2], [170, 1, "a"], [[171, 172], 2], [173, 7], [174, 2], [175, 5, " \u0304"], [[176, 177], 2], [178, 1, "2"], [179, 1, "3"], [180, 5, " \u0301"], [181, 1, "\u03BC"], [182, 2], [183, 2], [184, 5, " \u0327"], [185, 1, "1"], [186, 1, "o"], [187, 2], [188, 1, "1\u20444"], [189, 1, "1\u20442"], [190, 1, "3\u20444"], [191, 2], [192, 1, "\xE0"], [193, 1, "\xE1"], [194, 1, "\xE2"], [195, 1, "\xE3"], [196, 1, "\xE4"], [197, 1, "\xE5"], [198, 1, "\xE6"], [199, 1, "\xE7"], [200, 1, "\xE8"], [201, 1, "\xE9"], [202, 1, "\xEA"], [203, 1, "\xEB"], [204, 1, "\xEC"], [205, 1, "\xED"], [206, 1, "\xEE"], [207, 1, "\xEF"], [208, 1, "\xF0"], [209, 1, "\xF1"], [210, 1, "\xF2"], [211, 1, "\xF3"], [212, 1, "\xF4"], [213, 1, "\xF5"], [214, 1, "\xF6"], [215, 2], [216, 1, "\xF8"], [217, 1, "\xF9"], [218, 1, "\xFA"], [219, 1, "\xFB"], [220, 1, "\xFC"], [221, 1, "\xFD"], [222, 1, "\xFE"], [223, 6, "ss"], [[224, 246], 2], [247, 2], [[248, 255], 2], [256, 1, "\u0101"], [257, 2], [258, 1, "\u0103"], [259, 2], [260, 1, "\u0105"], [261, 2], [262, 1, "\u0107"], [263, 2], [264, 1, "\u0109"], [265, 2], [266, 1, "\u010B"], [267, 2], [268, 1, "\u010D"], [269, 2], [270, 1, "\u010F"], [271, 2], [272, 1, "\u0111"], [273, 2], [274, 1, "\u0113"], [275, 2], [276, 1, "\u0115"], [277, 2], [278, 1, "\u0117"], [279, 2], [280, 1, "\u0119"], [281, 2], [282, 1, "\u011B"], [283, 2], [284, 1, "\u011D"], [285, 2], [286, 1, "\u011F"], [287, 2], [288, 1, "\u0121"], [289, 2], [290, 1, "\u0123"], [291, 2], [292, 1, "\u0125"], [293, 2], [294, 1, "\u0127"], [295, 2], [296, 1, "\u0129"], [297, 2], [298, 1, "\u012B"], [299, 2], [300, 1, "\u012D"], [301, 2], [302, 1, "\u012F"], [303, 2], [304, 1, "i\u0307"], [305, 2], [[306, 307], 1, "ij"], [308, 1, "\u0135"], [309, 2], [310, 1, "\u0137"], [[311, 312], 2], [313, 1, "\u013A"], [314, 2], [315, 1, "\u013C"], [316, 2], [317, 1, "\u013E"], [318, 2], [[319, 320], 1, "l\xB7"], [321, 1, "\u0142"], [322, 2], [323, 1, "\u0144"], [324, 2], [325, 1, "\u0146"], [326, 2], [327, 1, "\u0148"], [328, 2], [329, 1, "\u02BCn"], [330, 1, "\u014B"], [331, 2], [332, 1, "\u014D"], [333, 2], [334, 1, "\u014F"], [335, 2], [336, 1, "\u0151"], [337, 2], [338, 1, "\u0153"], [339, 2], [340, 1, "\u0155"], [341, 2], [342, 1, "\u0157"], [343, 2], [344, 1, "\u0159"], [345, 2], [346, 1, "\u015B"], [347, 2], [348, 1, "\u015D"], [349, 2], [350, 1, "\u015F"], [351, 2], [352, 1, "\u0161"], [353, 2], [354, 1, "\u0163"], [355, 2], [356, 1, "\u0165"], [357, 2], [358, 1, "\u0167"], [359, 2], [360, 1, "\u0169"], [361, 2], [362, 1, "\u016B"], [363, 2], [364, 1, "\u016D"], [365, 2], [366, 1, "\u016F"], [367, 2], [368, 1, "\u0171"], [369, 2], [370, 1, "\u0173"], [371, 2], [372, 1, "\u0175"], [373, 2], [374, 1, "\u0177"], [375, 2], [376, 1, "\xFF"], [377, 1, "\u017A"], [378, 2], [379, 1, "\u017C"], [380, 2], [381, 1, "\u017E"], [382, 2], [383, 1, "s"], [384, 2], [385, 1, "\u0253"], [386, 1, "\u0183"], [387, 2], [388, 1, "\u0185"], [389, 2], [390, 1, "\u0254"], [391, 1, "\u0188"], [392, 2], [393, 1, "\u0256"], [394, 1, "\u0257"], [395, 1, "\u018C"], [[396, 397], 2], [398, 1, "\u01DD"], [399, 1, "\u0259"], [400, 1, "\u025B"], [401, 1, "\u0192"], [402, 2], [403, 1, "\u0260"], [404, 1, "\u0263"], [405, 2], [406, 1, "\u0269"], [407, 1, "\u0268"], [408, 1, "\u0199"], [[409, 411], 2], [412, 1, "\u026F"], [413, 1, "\u0272"], [414, 2], [415, 1, "\u0275"], [416, 1, "\u01A1"], [417, 2], [418, 1, "\u01A3"], [419, 2], [420, 1, "\u01A5"], [421, 2], [422, 1, "\u0280"], [423, 1, "\u01A8"], [424, 2], [425, 1, "\u0283"], [[426, 427], 2], [428, 1, "\u01AD"], [429, 2], [430, 1, "\u0288"], [431, 1, "\u01B0"], [432, 2], [433, 1, "\u028A"], [434, 1, "\u028B"], [435, 1, "\u01B4"], [436, 2], [437, 1, "\u01B6"], [438, 2], [439, 1, "\u0292"], [440, 1, "\u01B9"], [[441, 443], 2], [444, 1, "\u01BD"], [[445, 451], 2], [[452, 454], 1, "d\u017E"], [[455, 457], 1, "lj"], [[458, 460], 1, "nj"], [461, 1, "\u01CE"], [462, 2], [463, 1, "\u01D0"], [464, 2], [465, 1, "\u01D2"], [466, 2], [467, 1, "\u01D4"], [468, 2], [469, 1, "\u01D6"], [470, 2], [471, 1, "\u01D8"], [472, 2], [473, 1, "\u01DA"], [474, 2], [475, 1, "\u01DC"], [[476, 477], 2], [478, 1, "\u01DF"], [479, 2], [480, 1, "\u01E1"], [481, 2], [482, 1, "\u01E3"], [483, 2], [484, 1, "\u01E5"], [485, 2], [486, 1, "\u01E7"], [487, 2], [488, 1, "\u01E9"], [489, 2], [490, 1, "\u01EB"], [491, 2], [492, 1, "\u01ED"], [493, 2], [494, 1, "\u01EF"], [[495, 496], 2], [[497, 499], 1, "dz"], [500, 1, "\u01F5"], [501, 2], [502, 1, "\u0195"], [503, 1, "\u01BF"], [504, 1, "\u01F9"], [505, 2], [506, 1, "\u01FB"], [507, 2], [508, 1, "\u01FD"], [509, 2], [510, 1, "\u01FF"], [511, 2], [512, 1, "\u0201"], [513, 2], [514, 1, "\u0203"], [515, 2], [516, 1, "\u0205"], [517, 2], [518, 1, "\u0207"], [519, 2], [520, 1, "\u0209"], [521, 2], [522, 1, "\u020B"], [523, 2], [524, 1, "\u020D"], [525, 2], [526, 1, "\u020F"], [527, 2], [528, 1, "\u0211"], [529, 2], [530, 1, "\u0213"], [531, 2], [532, 1, "\u0215"], [533, 2], [534, 1, "\u0217"], [535, 2], [536, 1, "\u0219"], [537, 2], [538, 1, "\u021B"], [539, 2], [540, 1, "\u021D"], [541, 2], [542, 1, "\u021F"], [543, 2], [544, 1, "\u019E"], [545, 2], [546, 1, "\u0223"], [547, 2], [548, 1, "\u0225"], [549, 2], [550, 1, "\u0227"], [551, 2], [552, 1, "\u0229"], [553, 2], [554, 1, "\u022B"], [555, 2], [556, 1, "\u022D"], [557, 2], [558, 1, "\u022F"], [559, 2], [560, 1, "\u0231"], [561, 2], [562, 1, "\u0233"], [563, 2], [[564, 566], 2], [[567, 569], 2], [570, 1, "\u2C65"], [571, 1, "\u023C"], [572, 2], [573, 1, "\u019A"], [574, 1, "\u2C66"], [[575, 576], 2], [577, 1, "\u0242"], [578, 2], [579, 1, "\u0180"], [580, 1, "\u0289"], [581, 1, "\u028C"], [582, 1, "\u0247"], [583, 2], [584, 1, "\u0249"], [585, 2], [586, 1, "\u024B"], [587, 2], [588, 1, "\u024D"], [589, 2], [590, 1, "\u024F"], [591, 2], [[592, 680], 2], [[681, 685], 2], [[686, 687], 2], [688, 1, "h"], [689, 1, "\u0266"], [690, 1, "j"], [691, 1, "r"], [692, 1, "\u0279"], [693, 1, "\u027B"], [694, 1, "\u0281"], [695, 1, "w"], [696, 1, "y"], [[697, 705], 2], [[706, 709], 2], [[710, 721], 2], [[722, 727], 2], [728, 5, " \u0306"], [729, 5, " \u0307"], [730, 5, " \u030A"], [731, 5, " \u0328"], [732, 5, " \u0303"], [733, 5, " \u030B"], [734, 2], [735, 2], [736, 1, "\u0263"], [737, 1, "l"], [738, 1, "s"], [739, 1, "x"], [740, 1, "\u0295"], [[741, 745], 2], [[746, 747], 2], [748, 2], [749, 2], [750, 2], [[751, 767], 2], [[768, 831], 2], [832, 1, "\u0300"], [833, 1, "\u0301"], [834, 2], [835, 1, "\u0313"], [836, 1, "\u0308\u0301"], [837, 1, "\u03B9"], [[838, 846], 2], [847, 7], [[848, 855], 2], [[856, 860], 2], [[861, 863], 2], [[864, 865], 2], [866, 2], [[867, 879], 2], [880, 1, "\u0371"], [881, 2], [882, 1, "\u0373"], [883, 2], [884, 1, "\u02B9"], [885, 2], [886, 1, "\u0377"], [887, 2], [[888, 889], 3], [890, 5, " \u03B9"], [[891, 893], 2], [894, 5, ";"], [895, 1, "\u03F3"], [[896, 899], 3], [900, 5, " \u0301"], [901, 5, " \u0308\u0301"], [902, 1, "\u03AC"], [903, 1, "\xB7"], [904, 1, "\u03AD"], [905, 1, "\u03AE"], [906, 1, "\u03AF"], [907, 3], [908, 1, "\u03CC"], [909, 3], [910, 1, "\u03CD"], [911, 1, "\u03CE"], [912, 2], [913, 1, "\u03B1"], [914, 1, "\u03B2"], [915, 1, "\u03B3"], [916, 1, "\u03B4"], [917, 1, "\u03B5"], [918, 1, "\u03B6"], [919, 1, "\u03B7"], [920, 1, "\u03B8"], [921, 1, "\u03B9"], [922, 1, "\u03BA"], [923, 1, "\u03BB"], [924, 1, "\u03BC"], [925, 1, "\u03BD"], [926, 1, "\u03BE"], [927, 1, "\u03BF"], [928, 1, "\u03C0"], [929, 1, "\u03C1"], [930, 3], [931, 1, "\u03C3"], [932, 1, "\u03C4"], [933, 1, "\u03C5"], [934, 1, "\u03C6"], [935, 1, "\u03C7"], [936, 1, "\u03C8"], [937, 1, "\u03C9"], [938, 1, "\u03CA"], [939, 1, "\u03CB"], [[940, 961], 2], [962, 6, "\u03C3"], [[963, 974], 2], [975, 1, "\u03D7"], [976, 1, "\u03B2"], [977, 1, "\u03B8"], [978, 1, "\u03C5"], [979, 1, "\u03CD"], [980, 1, "\u03CB"], [981, 1, "\u03C6"], [982, 1, "\u03C0"], [983, 2], [984, 1, "\u03D9"], [985, 2], [986, 1, "\u03DB"], [987, 2], [988, 1, "\u03DD"], [989, 2], [990, 1, "\u03DF"], [991, 2], [992, 1, "\u03E1"], [993, 2], [994, 1, "\u03E3"], [995, 2], [996, 1, "\u03E5"], [997, 2], [998, 1, "\u03E7"], [999, 2], [1e3, 1, "\u03E9"], [1001, 2], [1002, 1, "\u03EB"], [1003, 2], [1004, 1, "\u03ED"], [1005, 2], [1006, 1, "\u03EF"], [1007, 2], [1008, 1, "\u03BA"], [1009, 1, "\u03C1"], [1010, 1, "\u03C3"], [1011, 2], [1012, 1, "\u03B8"], [1013, 1, "\u03B5"], [1014, 2], [1015, 1, "\u03F8"], [1016, 2], [1017, 1, "\u03C3"], [1018, 1, "\u03FB"], [1019, 2], [1020, 2], [1021, 1, "\u037B"], [1022, 1, "\u037C"], [1023, 1, "\u037D"], [1024, 1, "\u0450"], [1025, 1, "\u0451"], [1026, 1, "\u0452"], [1027, 1, "\u0453"], [1028, 1, "\u0454"], [1029, 1, "\u0455"], [1030, 1, "\u0456"], [1031, 1, "\u0457"], [1032, 1, "\u0458"], [1033, 1, "\u0459"], [1034, 1, "\u045A"], [1035, 1, "\u045B"], [1036, 1, "\u045C"], [1037, 1, "\u045D"], [1038, 1, "\u045E"], [1039, 1, "\u045F"], [1040, 1, "\u0430"], [1041, 1, "\u0431"], [1042, 1, "\u0432"], [1043, 1, "\u0433"], [1044, 1, "\u0434"], [1045, 1, "\u0435"], [1046, 1, "\u0436"], [1047, 1, "\u0437"], [1048, 1, "\u0438"], [1049, 1, "\u0439"], [1050, 1, "\u043A"], [1051, 1, "\u043B"], [1052, 1, "\u043C"], [1053, 1, "\u043D"], [1054, 1, "\u043E"], [1055, 1, "\u043F"], [1056, 1, "\u0440"], [1057, 1, "\u0441"], [1058, 1, "\u0442"], [1059, 1, "\u0443"], [1060, 1, "\u0444"], [1061, 1, "\u0445"], [1062, 1, "\u0446"], [1063, 1, "\u0447"], [1064, 1, "\u0448"], [1065, 1, "\u0449"], [1066, 1, "\u044A"], [1067, 1, "\u044B"], [1068, 1, "\u044C"], [1069, 1, "\u044D"], [1070, 1, "\u044E"], [1071, 1, "\u044F"], [[1072, 1103], 2], [1104, 2], [[1105, 1116], 2], [1117, 2], [[1118, 1119], 2], [1120, 1, "\u0461"], [1121, 2], [1122, 1, "\u0463"], [1123, 2], [1124, 1, "\u0465"], [1125, 2], [1126, 1, "\u0467"], [1127, 2], [1128, 1, "\u0469"], [1129, 2], [1130, 1, "\u046B"], [1131, 2], [1132, 1, "\u046D"], [1133, 2], [1134, 1, "\u046F"], [1135, 2], [1136, 1, "\u0471"], [1137, 2], [1138, 1, "\u0473"], [1139, 2], [1140, 1, "\u0475"], [1141, 2], [1142, 1, "\u0477"], [1143, 2], [1144, 1, "\u0479"], [1145, 2], [1146, 1, "\u047B"], [1147, 2], [1148, 1, "\u047D"], [1149, 2], [1150, 1, "\u047F"], [1151, 2], [1152, 1, "\u0481"], [1153, 2], [1154, 2], [[1155, 1158], 2], [1159, 2], [[1160, 1161], 2], [1162, 1, "\u048B"], [1163, 2], [1164, 1, "\u048D"], [1165, 2], [1166, 1, "\u048F"], [1167, 2], [1168, 1, "\u0491"], [1169, 2], [1170, 1, "\u0493"], [1171, 2], [1172, 1, "\u0495"], [1173, 2], [1174, 1, "\u0497"], [1175, 2], [1176, 1, "\u0499"], [1177, 2], [1178, 1, "\u049B"], [1179, 2], [1180, 1, "\u049D"], [1181, 2], [1182, 1, "\u049F"], [1183, 2], [1184, 1, "\u04A1"], [1185, 2], [1186, 1, "\u04A3"], [1187, 2], [1188, 1, "\u04A5"], [1189, 2], [1190, 1, "\u04A7"], [1191, 2], [1192, 1, "\u04A9"], [1193, 2], [1194, 1, "\u04AB"], [1195, 2], [1196, 1, "\u04AD"], [1197, 2], [1198, 1, "\u04AF"], [1199, 2], [1200, 1, "\u04B1"], [1201, 2], [1202, 1, "\u04B3"], [1203, 2], [1204, 1, "\u04B5"], [1205, 2], [1206, 1, "\u04B7"], [1207, 2], [1208, 1, "\u04B9"], [1209, 2], [1210, 1, "\u04BB"], [1211, 2], [1212, 1, "\u04BD"], [1213, 2], [1214, 1, "\u04BF"], [1215, 2], [1216, 3], [1217, 1, "\u04C2"], [1218, 2], [1219, 1, "\u04C4"], [1220, 2], [1221, 1, "\u04C6"], [1222, 2], [1223, 1, "\u04C8"], [1224, 2], [1225, 1, "\u04CA"], [1226, 2], [1227, 1, "\u04CC"], [1228, 2], [1229, 1, "\u04CE"], [1230, 2], [1231, 2], [1232, 1, "\u04D1"], [1233, 2], [1234, 1, "\u04D3"], [1235, 2], [1236, 1, "\u04D5"], [1237, 2], [1238, 1, "\u04D7"], [1239, 2], [1240, 1, "\u04D9"], [1241, 2], [1242, 1, "\u04DB"], [1243, 2], [1244, 1, "\u04DD"], [1245, 2], [1246, 1, "\u04DF"], [1247, 2], [1248, 1, "\u04E1"], [1249, 2], [1250, 1, "\u04E3"], [1251, 2], [1252, 1, "\u04E5"], [1253, 2], [1254, 1, "\u04E7"], [1255, 2], [1256, 1, "\u04E9"], [1257, 2], [1258, 1, "\u04EB"], [1259, 2], [1260, 1, "\u04ED"], [1261, 2], [1262, 1, "\u04EF"], [1263, 2], [1264, 1, "\u04F1"], [1265, 2], [1266, 1, "\u04F3"], [1267, 2], [1268, 1, "\u04F5"], [1269, 2], [1270, 1, "\u04F7"], [1271, 2], [1272, 1, "\u04F9"], [1273, 2], [1274, 1, "\u04FB"], [1275, 2], [1276, 1, "\u04FD"], [1277, 2], [1278, 1, "\u04FF"], [1279, 2], [1280, 1, "\u0501"], [1281, 2], [1282, 1, "\u0503"], [1283, 2], [1284, 1, "\u0505"], [1285, 2], [1286, 1, "\u0507"], [1287, 2], [1288, 1, "\u0509"], [1289, 2], [1290, 1, "\u050B"], [1291, 2], [1292, 1, "\u050D"], [1293, 2], [1294, 1, "\u050F"], [1295, 2], [1296, 1, "\u0511"], [1297, 2], [1298, 1, "\u0513"], [1299, 2], [1300, 1, "\u0515"], [1301, 2], [1302, 1, "\u0517"], [1303, 2], [1304, 1, "\u0519"], [1305, 2], [1306, 1, "\u051B"], [1307, 2], [1308, 1, "\u051D"], [1309, 2], [1310, 1, "\u051F"], [1311, 2], [1312, 1, "\u0521"], [1313, 2], [1314, 1, "\u0523"], [1315, 2], [1316, 1, "\u0525"], [1317, 2], [1318, 1, "\u0527"], [1319, 2], [1320, 1, "\u0529"], [1321, 2], [1322, 1, "\u052B"], [1323, 2], [1324, 1, "\u052D"], [1325, 2], [1326, 1, "\u052F"], [1327, 2], [1328, 3], [1329, 1, "\u0561"], [1330, 1, "\u0562"], [1331, 1, "\u0563"], [1332, 1, "\u0564"], [1333, 1, "\u0565"], [1334, 1, "\u0566"], [1335, 1, "\u0567"], [1336, 1, "\u0568"], [1337, 1, "\u0569"], [1338, 1, "\u056A"], [1339, 1, "\u056B"], [1340, 1, "\u056C"], [1341, 1, "\u056D"], [1342, 1, "\u056E"], [1343, 1, "\u056F"], [1344, 1, "\u0570"], [1345, 1, "\u0571"], [1346, 1, "\u0572"], [1347, 1, "\u0573"], [1348, 1, "\u0574"], [1349, 1, "\u0575"], [1350, 1, "\u0576"], [1351, 1, "\u0577"], [1352, 1, "\u0578"], [1353, 1, "\u0579"], [1354, 1, "\u057A"], [1355, 1, "\u057B"], [1356, 1, "\u057C"], [1357, 1, "\u057D"], [1358, 1, "\u057E"], [1359, 1, "\u057F"], [1360, 1, "\u0580"], [1361, 1, "\u0581"], [1362, 1, "\u0582"], [1363, 1, "\u0583"], [1364, 1, "\u0584"], [1365, 1, "\u0585"], [1366, 1, "\u0586"], [[1367, 1368], 3], [1369, 2], [[1370, 1375], 2], [1376, 2], [[1377, 1414], 2], [1415, 1, "\u0565\u0582"], [1416, 2], [1417, 2], [1418, 2], [[1419, 1420], 3], [[1421, 1422], 2], [1423, 2], [1424, 3], [[1425, 1441], 2], [1442, 2], [[1443, 1455], 2], [[1456, 1465], 2], [1466, 2], [[1467, 1469], 2], [1470, 2], [1471, 2], [1472, 2], [[1473, 1474], 2], [1475, 2], [1476, 2], [1477, 2], [1478, 2], [1479, 2], [[1480, 1487], 3], [[1488, 1514], 2], [[1515, 1518], 3], [1519, 2], [[1520, 1524], 2], [[1525, 1535], 3], [[1536, 1539], 3], [1540, 3], [1541, 3], [[1542, 1546], 2], [1547, 2], [1548, 2], [[1549, 1551], 2], [[1552, 1557], 2], [[1558, 1562], 2], [1563, 2], [1564, 3], [1565, 2], [1566, 2], [1567, 2], [1568, 2], [[1569, 1594], 2], [[1595, 1599], 2], [1600, 2], [[1601, 1618], 2], [[1619, 1621], 2], [[1622, 1624], 2], [[1625, 1630], 2], [1631, 2], [[1632, 1641], 2], [[1642, 1645], 2], [[1646, 1647], 2], [[1648, 1652], 2], [1653, 1, "\u0627\u0674"], [1654, 1, "\u0648\u0674"], [1655, 1, "\u06C7\u0674"], [1656, 1, "\u064A\u0674"], [[1657, 1719], 2], [[1720, 1721], 2], [[1722, 1726], 2], [1727, 2], [[1728, 1742], 2], [1743, 2], [[1744, 1747], 2], [1748, 2], [[1749, 1756], 2], [1757, 3], [1758, 2], [[1759, 1768], 2], [1769, 2], [[1770, 1773], 2], [[1774, 1775], 2], [[1776, 1785], 2], [[1786, 1790], 2], [1791, 2], [[1792, 1805], 2], [1806, 3], [1807, 3], [[1808, 1836], 2], [[1837, 1839], 2], [[1840, 1866], 2], [[1867, 1868], 3], [[1869, 1871], 2], [[1872, 1901], 2], [[1902, 1919], 2], [[1920, 1968], 2], [1969, 2], [[1970, 1983], 3], [[1984, 2037], 2], [[2038, 2042], 2], [[2043, 2044], 3], [2045, 2], [[2046, 2047], 2], [[2048, 2093], 2], [[2094, 2095], 3], [[2096, 2110], 2], [2111, 3], [[2112, 2139], 2], [[2140, 2141], 3], [2142, 2], [2143, 3], [[2144, 2154], 2], [[2155, 2159], 3], [[2160, 2183], 2], [2184, 2], [[2185, 2190], 2], [2191, 3], [[2192, 2193], 3], [[2194, 2199], 3], [[2200, 2207], 2], [2208, 2], [2209, 2], [[2210, 2220], 2], [[2221, 2226], 2], [[2227, 2228], 2], [2229, 2], [[2230, 2237], 2], [[2238, 2247], 2], [[2248, 2258], 2], [2259, 2], [[2260, 2273], 2], [2274, 3], [2275, 2], [[2276, 2302], 2], [2303, 2], [2304, 2], [[2305, 2307], 2], [2308, 2], [[2309, 2361], 2], [[2362, 2363], 2], [[2364, 2381], 2], [2382, 2], [2383, 2], [[2384, 2388], 2], [2389, 2], [[2390, 2391], 2], [2392, 1, "\u0915\u093C"], [2393, 1, "\u0916\u093C"], [2394, 1, "\u0917\u093C"], [2395, 1, "\u091C\u093C"], [2396, 1, "\u0921\u093C"], [2397, 1, "\u0922\u093C"], [2398, 1, "\u092B\u093C"], [2399, 1, "\u092F\u093C"], [[2400, 2403], 2], [[2404, 2405], 2], [[2406, 2415], 2], [2416, 2], [[2417, 2418], 2], [[2419, 2423], 2], [2424, 2], [[2425, 2426], 2], [[2427, 2428], 2], [2429, 2], [[2430, 2431], 2], [2432, 2], [[2433, 2435], 2], [2436, 3], [[2437, 2444], 2], [[2445, 2446], 3], [[2447, 2448], 2], [[2449, 2450], 3], [[2451, 2472], 2], [2473, 3], [[2474, 2480], 2], [2481, 3], [2482, 2], [[2483, 2485], 3], [[2486, 2489], 2], [[2490, 2491], 3], [2492, 2], [2493, 2], [[2494, 2500], 2], [[2501, 2502], 3], [[2503, 2504], 2], [[2505, 2506], 3], [[2507, 2509], 2], [2510, 2], [[2511, 2518], 3], [2519, 2], [[2520, 2523], 3], [2524, 1, "\u09A1\u09BC"], [2525, 1, "\u09A2\u09BC"], [2526, 3], [2527, 1, "\u09AF\u09BC"], [[2528, 2531], 2], [[2532, 2533], 3], [[2534, 2545], 2], [[2546, 2554], 2], [2555, 2], [2556, 2], [2557, 2], [2558, 2], [[2559, 2560], 3], [2561, 2], [2562, 2], [2563, 2], [2564, 3], [[2565, 2570], 2], [[2571, 2574], 3], [[2575, 2576], 2], [[2577, 2578], 3], [[2579, 2600], 2], [2601, 3], [[2602, 2608], 2], [2609, 3], [2610, 2], [2611, 1, "\u0A32\u0A3C"], [2612, 3], [2613, 2], [2614, 1, "\u0A38\u0A3C"], [2615, 3], [[2616, 2617], 2], [[2618, 2619], 3], [2620, 2], [2621, 3], [[2622, 2626], 2], [[2627, 2630], 3], [[2631, 2632], 2], [[2633, 2634], 3], [[2635, 2637], 2], [[2638, 2640], 3], [2641, 2], [[2642, 2648], 3], [2649, 1, "\u0A16\u0A3C"], [2650, 1, "\u0A17\u0A3C"], [2651, 1, "\u0A1C\u0A3C"], [2652, 2], [2653, 3], [2654, 1, "\u0A2B\u0A3C"], [[2655, 2661], 3], [[2662, 2676], 2], [2677, 2], [2678, 2], [[2679, 2688], 3], [[2689, 2691], 2], [2692, 3], [[2693, 2699], 2], [2700, 2], [2701, 2], [2702, 3], [[2703, 2705], 2], [2706, 3], [[2707, 2728], 2], [2729, 3], [[2730, 2736], 2], [2737, 3], [[2738, 2739], 2], [2740, 3], [[2741, 2745], 2], [[2746, 2747], 3], [[2748, 2757], 2], [2758, 3], [[2759, 2761], 2], [2762, 3], [[2763, 2765], 2], [[2766, 2767], 3], [2768, 2], [[2769, 2783], 3], [2784, 2], [[2785, 2787], 2], [[2788, 2789], 3], [[2790, 2799], 2], [2800, 2], [2801, 2], [[2802, 2808], 3], [2809, 2], [[2810, 2815], 2], [2816, 3], [[2817, 2819], 2], [2820, 3], [[2821, 2828], 2], [[2829, 2830], 3], [[2831, 2832], 2], [[2833, 2834], 3], [[2835, 2856], 2], [2857, 3], [[2858, 2864], 2], [2865, 3], [[2866, 2867], 2], [2868, 3], [2869, 2], [[2870, 2873], 2], [[2874, 2875], 3], [[2876, 2883], 2], [2884, 2], [[2885, 2886], 3], [[2887, 2888], 2], [[2889, 2890], 3], [[2891, 2893], 2], [[2894, 2900], 3], [2901, 2], [[2902, 2903], 2], [[2904, 2907], 3], [2908, 1, "\u0B21\u0B3C"], [2909, 1, "\u0B22\u0B3C"], [2910, 3], [[2911, 2913], 2], [[2914, 2915], 2], [[2916, 2917], 3], [[2918, 2927], 2], [2928, 2], [2929, 2], [[2930, 2935], 2], [[2936, 2945], 3], [[2946, 2947], 2], [2948, 3], [[2949, 2954], 2], [[2955, 2957], 3], [[2958, 2960], 2], [2961, 3], [[2962, 2965], 2], [[2966, 2968], 3], [[2969, 2970], 2], [2971, 3], [2972, 2], [2973, 3], [[2974, 2975], 2], [[2976, 2978], 3], [[2979, 2980], 2], [[2981, 2983], 3], [[2984, 2986], 2], [[2987, 2989], 3], [[2990, 2997], 2], [2998, 2], [[2999, 3001], 2], [[3002, 3005], 3], [[3006, 3010], 2], [[3011, 3013], 3], [[3014, 3016], 2], [3017, 3], [[3018, 3021], 2], [[3022, 3023], 3], [3024, 2], [[3025, 3030], 3], [3031, 2], [[3032, 3045], 3], [3046, 2], [[3047, 3055], 2], [[3056, 3058], 2], [[3059, 3066], 2], [[3067, 3071], 3], [3072, 2], [[3073, 3075], 2], [3076, 2], [[3077, 3084], 2], [3085, 3], [[3086, 3088], 2], [3089, 3], [[3090, 3112], 2], [3113, 3], [[3114, 3123], 2], [3124, 2], [[3125, 3129], 2], [[3130, 3131], 3], [3132, 2], [3133, 2], [[3134, 3140], 2], [3141, 3], [[3142, 3144], 2], [3145, 3], [[3146, 3149], 2], [[3150, 3156], 3], [[3157, 3158], 2], [3159, 3], [[3160, 3161], 2], [3162, 2], [[3163, 3164], 3], [3165, 2], [[3166, 3167], 3], [[3168, 3169], 2], [[3170, 3171], 2], [[3172, 3173], 3], [[3174, 3183], 2], [[3184, 3190], 3], [3191, 2], [[3192, 3199], 2], [3200, 2], [3201, 2], [[3202, 3203], 2], [3204, 2], [[3205, 3212], 2], [3213, 3], [[3214, 3216], 2], [3217, 3], [[3218, 3240], 2], [3241, 3], [[3242, 3251], 2], [3252, 3], [[3253, 3257], 2], [[3258, 3259], 3], [[3260, 3261], 2], [[3262, 3268], 2], [3269, 3], [[3270, 3272], 2], [3273, 3], [[3274, 3277], 2], [[3278, 3284], 3], [[3285, 3286], 2], [[3287, 3292], 3], [3293, 2], [3294, 2], [3295, 3], [[3296, 3297], 2], [[3298, 3299], 2], [[3300, 3301], 3], [[3302, 3311], 2], [3312, 3], [[3313, 3314], 2], [3315, 2], [[3316, 3327], 3], [3328, 2], [3329, 2], [[3330, 3331], 2], [3332, 2], [[3333, 3340], 2], [3341, 3], [[3342, 3344], 2], [3345, 3], [[3346, 3368], 2], [3369, 2], [[3370, 3385], 2], [3386, 2], [[3387, 3388], 2], [3389, 2], [[3390, 3395], 2], [3396, 2], [3397, 3], [[3398, 3400], 2], [3401, 3], [[3402, 3405], 2], [3406, 2], [3407, 2], [[3408, 3411], 3], [[3412, 3414], 2], [3415, 2], [[3416, 3422], 2], [3423, 2], [[3424, 3425], 2], [[3426, 3427], 2], [[3428, 3429], 3], [[3430, 3439], 2], [[3440, 3445], 2], [[3446, 3448], 2], [3449, 2], [[3450, 3455], 2], [3456, 3], [3457, 2], [[3458, 3459], 2], [3460, 3], [[3461, 3478], 2], [[3479, 3481], 3], [[3482, 3505], 2], [3506, 3], [[3507, 3515], 2], [3516, 3], [3517, 2], [[3518, 3519], 3], [[3520, 3526], 2], [[3527, 3529], 3], [3530, 2], [[3531, 3534], 3], [[3535, 3540], 2], [3541, 3], [3542, 2], [3543, 3], [[3544, 3551], 2], [[3552, 3557], 3], [[3558, 3567], 2], [[3568, 3569], 3], [[3570, 3571], 2], [3572, 2], [[3573, 3584], 3], [[3585, 3634], 2], [3635, 1, "\u0E4D\u0E32"], [[3636, 3642], 2], [[3643, 3646], 3], [3647, 2], [[3648, 3662], 2], [3663, 2], [[3664, 3673], 2], [[3674, 3675], 2], [[3676, 3712], 3], [[3713, 3714], 2], [3715, 3], [3716, 2], [3717, 3], [3718, 2], [[3719, 3720], 2], [3721, 2], [3722, 2], [3723, 3], [3724, 2], [3725, 2], [[3726, 3731], 2], [[3732, 3735], 2], [3736, 2], [[3737, 3743], 2], [3744, 2], [[3745, 3747], 2], [3748, 3], [3749, 2], [3750, 3], [3751, 2], [[3752, 3753], 2], [[3754, 3755], 2], [3756, 2], [[3757, 3762], 2], [3763, 1, "\u0ECD\u0EB2"], [[3764, 3769], 2], [3770, 2], [[3771, 3773], 2], [[3774, 3775], 3], [[3776, 3780], 2], [3781, 3], [3782, 2], [3783, 3], [[3784, 3789], 2], [3790, 2], [3791, 3], [[3792, 3801], 2], [[3802, 3803], 3], [3804, 1, "\u0EAB\u0E99"], [3805, 1, "\u0EAB\u0EA1"], [[3806, 3807], 2], [[3808, 3839], 3], [3840, 2], [[3841, 3850], 2], [3851, 2], [3852, 1, "\u0F0B"], [[3853, 3863], 2], [[3864, 3865], 2], [[3866, 3871], 2], [[3872, 3881], 2], [[3882, 3892], 2], [3893, 2], [3894, 2], [3895, 2], [3896, 2], [3897, 2], [[3898, 3901], 2], [[3902, 3906], 2], [3907, 1, "\u0F42\u0FB7"], [[3908, 3911], 2], [3912, 3], [[3913, 3916], 2], [3917, 1, "\u0F4C\u0FB7"], [[3918, 3921], 2], [3922, 1, "\u0F51\u0FB7"], [[3923, 3926], 2], [3927, 1, "\u0F56\u0FB7"], [[3928, 3931], 2], [3932, 1, "\u0F5B\u0FB7"], [[3933, 3944], 2], [3945, 1, "\u0F40\u0FB5"], [3946, 2], [[3947, 3948], 2], [[3949, 3952], 3], [[3953, 3954], 2], [3955, 1, "\u0F71\u0F72"], [3956, 2], [3957, 1, "\u0F71\u0F74"], [3958, 1, "\u0FB2\u0F80"], [3959, 1, "\u0FB2\u0F71\u0F80"], [3960, 1, "\u0FB3\u0F80"], [3961, 1, "\u0FB3\u0F71\u0F80"], [[3962, 3968], 2], [3969, 1, "\u0F71\u0F80"], [[3970, 3972], 2], [3973, 2], [[3974, 3979], 2], [[3980, 3983], 2], [[3984, 3986], 2], [3987, 1, "\u0F92\u0FB7"], [[3988, 3989], 2], [3990, 2], [3991, 2], [3992, 3], [[3993, 3996], 2], [3997, 1, "\u0F9C\u0FB7"], [[3998, 4001], 2], [4002, 1, "\u0FA1\u0FB7"], [[4003, 4006], 2], [4007, 1, "\u0FA6\u0FB7"], [[4008, 4011], 2], [4012, 1, "\u0FAB\u0FB7"], [4013, 2], [[4014, 4016], 2], [[4017, 4023], 2], [4024, 2], [4025, 1, "\u0F90\u0FB5"], [[4026, 4028], 2], [4029, 3], [[4030, 4037], 2], [4038, 2], [[4039, 4044], 2], [4045, 3], [4046, 2], [4047, 2], [[4048, 4049], 2], [[4050, 4052], 2], [[4053, 4056], 2], [[4057, 4058], 2], [[4059, 4095], 3], [[4096, 4129], 2], [4130, 2], [[4131, 4135], 2], [4136, 2], [[4137, 4138], 2], [4139, 2], [[4140, 4146], 2], [[4147, 4149], 2], [[4150, 4153], 2], [[4154, 4159], 2], [[4160, 4169], 2], [[4170, 4175], 2], [[4176, 4185], 2], [[4186, 4249], 2], [[4250, 4253], 2], [[4254, 4255], 2], [[4256, 4293], 3], [4294, 3], [4295, 1, "\u2D27"], [[4296, 4300], 3], [4301, 1, "\u2D2D"], [[4302, 4303], 3], [[4304, 4342], 2], [[4343, 4344], 2], [[4345, 4346], 2], [4347, 2], [4348, 1, "\u10DC"], [[4349, 4351], 2], [[4352, 4441], 2], [[4442, 4446], 2], [[4447, 4448], 3], [[4449, 4514], 2], [[4515, 4519], 2], [[4520, 4601], 2], [[4602, 4607], 2], [[4608, 4614], 2], [4615, 2], [[4616, 4678], 2], [4679, 2], [4680, 2], [4681, 3], [[4682, 4685], 2], [[4686, 4687], 3], [[4688, 4694], 2], [4695, 3], [4696, 2], [4697, 3], [[4698, 4701], 2], [[4702, 4703], 3], [[4704, 4742], 2], [4743, 2], [4744, 2], [4745, 3], [[4746, 4749], 2], [[4750, 4751], 3], [[4752, 4782], 2], [4783, 2], [4784, 2], [4785, 3], [[4786, 4789], 2], [[4790, 4791], 3], [[4792, 4798], 2], [4799, 3], [4800, 2], [4801, 3], [[4802, 4805], 2], [[4806, 4807], 3], [[4808, 4814], 2], [4815, 2], [[4816, 4822], 2], [4823, 3], [[4824, 4846], 2], [4847, 2], [[4848, 4878], 2], [4879, 2], [4880, 2], [4881, 3], [[4882, 4885], 2], [[4886, 4887], 3], [[4888, 4894], 2], [4895, 2], [[4896, 4934], 2], [4935, 2], [[4936, 4954], 2], [[4955, 4956], 3], [[4957, 4958], 2], [4959, 2], [4960, 2], [[4961, 4988], 2], [[4989, 4991], 3], [[4992, 5007], 2], [[5008, 5017], 2], [[5018, 5023], 3], [[5024, 5108], 2], [5109, 2], [[5110, 5111], 3], [5112, 1, "\u13F0"], [5113, 1, "\u13F1"], [5114, 1, "\u13F2"], [5115, 1, "\u13F3"], [5116, 1, "\u13F4"], [5117, 1, "\u13F5"], [[5118, 5119], 3], [5120, 2], [[5121, 5740], 2], [[5741, 5742], 2], [[5743, 5750], 2], [[5751, 5759], 2], [5760, 3], [[5761, 5786], 2], [[5787, 5788], 2], [[5789, 5791], 3], [[5792, 5866], 2], [[5867, 5872], 2], [[5873, 5880], 2], [[5881, 5887], 3], [[5888, 5900], 2], [5901, 2], [[5902, 5908], 2], [5909, 2], [[5910, 5918], 3], [5919, 2], [[5920, 5940], 2], [[5941, 5942], 2], [[5943, 5951], 3], [[5952, 5971], 2], [[5972, 5983], 3], [[5984, 5996], 2], [5997, 3], [[5998, 6e3], 2], [6001, 3], [[6002, 6003], 2], [[6004, 6015], 3], [[6016, 6067], 2], [[6068, 6069], 3], [[6070, 6099], 2], [[6100, 6102], 2], [6103, 2], [[6104, 6107], 2], [6108, 2], [6109, 2], [[6110, 6111], 3], [[6112, 6121], 2], [[6122, 6127], 3], [[6128, 6137], 2], [[6138, 6143], 3], [[6144, 6149], 2], [6150, 3], [[6151, 6154], 2], [[6155, 6157], 7], [6158, 3], [6159, 7], [[6160, 6169], 2], [[6170, 6175], 3], [[6176, 6263], 2], [6264, 2], [[6265, 6271], 3], [[6272, 6313], 2], [6314, 2], [[6315, 6319], 3], [[6320, 6389], 2], [[6390, 6399], 3], [[6400, 6428], 2], [[6429, 6430], 2], [6431, 3], [[6432, 6443], 2], [[6444, 6447], 3], [[6448, 6459], 2], [[6460, 6463], 3], [6464, 2], [[6465, 6467], 3], [[6468, 6469], 2], [[6470, 6509], 2], [[6510, 6511], 3], [[6512, 6516], 2], [[6517, 6527], 3], [[6528, 6569], 2], [[6570, 6571], 2], [[6572, 6575], 3], [[6576, 6601], 2], [[6602, 6607], 3], [[6608, 6617], 2], [6618, 2], [[6619, 6621], 3], [[6622, 6623], 2], [[6624, 6655], 2], [[6656, 6683], 2], [[6684, 6685], 3], [[6686, 6687], 2], [[6688, 6750], 2], [6751, 3], [[6752, 6780], 2], [[6781, 6782], 3], [[6783, 6793], 2], [[6794, 6799], 3], [[6800, 6809], 2], [[6810, 6815], 3], [[6816, 6822], 2], [6823, 2], [[6824, 6829], 2], [[6830, 6831], 3], [[6832, 6845], 2], [6846, 2], [[6847, 6848], 2], [[6849, 6862], 2], [[6863, 6911], 3], [[6912, 6987], 2], [6988, 2], [[6989, 6991], 3], [[6992, 7001], 2], [[7002, 7018], 2], [[7019, 7027], 2], [[7028, 7036], 2], [[7037, 7038], 2], [7039, 3], [[7040, 7082], 2], [[7083, 7085], 2], [[7086, 7097], 2], [[7098, 7103], 2], [[7104, 7155], 2], [[7156, 7163], 3], [[7164, 7167], 2], [[7168, 7223], 2], [[7224, 7226], 3], [[7227, 7231], 2], [[7232, 7241], 2], [[7242, 7244], 3], [[7245, 7293], 2], [[7294, 7295], 2], [7296, 1, "\u0432"], [7297, 1, "\u0434"], [7298, 1, "\u043E"], [7299, 1, "\u0441"], [[7300, 7301], 1, "\u0442"], [7302, 1, "\u044A"], [7303, 1, "\u0463"], [7304, 1, "\uA64B"], [[7305, 7311], 3], [7312, 1, "\u10D0"], [7313, 1, "\u10D1"], [7314, 1, "\u10D2"], [7315, 1, "\u10D3"], [7316, 1, "\u10D4"], [7317, 1, "\u10D5"], [7318, 1, "\u10D6"], [7319, 1, "\u10D7"], [7320, 1, "\u10D8"], [7321, 1, "\u10D9"], [7322, 1, "\u10DA"], [7323, 1, "\u10DB"], [7324, 1, "\u10DC"], [7325, 1, "\u10DD"], [7326, 1, "\u10DE"], [7327, 1, "\u10DF"], [7328, 1, "\u10E0"], [7329, 1, "\u10E1"], [7330, 1, "\u10E2"], [7331, 1, "\u10E3"], [7332, 1, "\u10E4"], [7333, 1, "\u10E5"], [7334, 1, "\u10E6"], [7335, 1, "\u10E7"], [7336, 1, "\u10E8"], [7337, 1, "\u10E9"], [7338, 1, "\u10EA"], [7339, 1, "\u10EB"], [7340, 1, "\u10EC"], [7341, 1, "\u10ED"], [7342, 1, "\u10EE"], [7343, 1, "\u10EF"], [7344, 1, "\u10F0"], [7345, 1, "\u10F1"], [7346, 1, "\u10F2"], [7347, 1, "\u10F3"], [7348, 1, "\u10F4"], [7349, 1, "\u10F5"], [7350, 1, "\u10F6"], [7351, 1, "\u10F7"], [7352, 1, "\u10F8"], [7353, 1, "\u10F9"], [7354, 1, "\u10FA"], [[7355, 7356], 3], [7357, 1, "\u10FD"], [7358, 1, "\u10FE"], [7359, 1, "\u10FF"], [[7360, 7367], 2], [[7368, 7375], 3], [[7376, 7378], 2], [7379, 2], [[7380, 7410], 2], [[7411, 7414], 2], [7415, 2], [[7416, 7417], 2], [7418, 2], [[7419, 7423], 3], [[7424, 7467], 2], [7468, 1, "a"], [7469, 1, "\xE6"], [7470, 1, "b"], [7471, 2], [7472, 1, "d"], [7473, 1, "e"], [7474, 1, "\u01DD"], [7475, 1, "g"], [7476, 1, "h"], [7477, 1, "i"], [7478, 1, "j"], [7479, 1, "k"], [7480, 1, "l"], [7481, 1, "m"], [7482, 1, "n"], [7483, 2], [7484, 1, "o"], [7485, 1, "\u0223"], [7486, 1, "p"], [7487, 1, "r"], [7488, 1, "t"], [7489, 1, "u"], [7490, 1, "w"], [7491, 1, "a"], [7492, 1, "\u0250"], [7493, 1, "\u0251"], [7494, 1, "\u1D02"], [7495, 1, "b"], [7496, 1, "d"], [7497, 1, "e"], [7498, 1, "\u0259"], [7499, 1, "\u025B"], [7500, 1, "\u025C"], [7501, 1, "g"], [7502, 2], [7503, 1, "k"], [7504, 1, "m"], [7505, 1, "\u014B"], [7506, 1, "o"], [7507, 1, "\u0254"], [7508, 1, "\u1D16"], [7509, 1, "\u1D17"], [7510, 1, "p"], [7511, 1, "t"], [7512, 1, "u"], [7513, 1, "\u1D1D"], [7514, 1, "\u026F"], [7515, 1, "v"], [7516, 1, "\u1D25"], [7517, 1, "\u03B2"], [7518, 1, "\u03B3"], [7519, 1, "\u03B4"], [7520, 1, "\u03C6"], [7521, 1, "\u03C7"], [7522, 1, "i"], [7523, 1, "r"], [7524, 1, "u"], [7525, 1, "v"], [7526, 1, "\u03B2"], [7527, 1, "\u03B3"], [7528, 1, "\u03C1"], [7529, 1, "\u03C6"], [7530, 1, "\u03C7"], [7531, 2], [[7532, 7543], 2], [7544, 1, "\u043D"], [[7545, 7578], 2], [7579, 1, "\u0252"], [7580, 1, "c"], [7581, 1, "\u0255"], [7582, 1, "\xF0"], [7583, 1, "\u025C"], [7584, 1, "f"], [7585, 1, "\u025F"], [7586, 1, "\u0261"], [7587, 1, "\u0265"], [7588, 1, "\u0268"], [7589, 1, "\u0269"], [7590, 1, "\u026A"], [7591, 1, "\u1D7B"], [7592, 1, "\u029D"], [7593, 1, "\u026D"], [7594, 1, "\u1D85"], [7595, 1, "\u029F"], [7596, 1, "\u0271"], [7597, 1, "\u0270"], [7598, 1, "\u0272"], [7599, 1, "\u0273"], [7600, 1, "\u0274"], [7601, 1, "\u0275"], [7602, 1, "\u0278"], [7603, 1, "\u0282"], [7604, 1, "\u0283"], [7605, 1, "\u01AB"], [7606, 1, "\u0289"], [7607, 1, "\u028A"], [7608, 1, "\u1D1C"], [7609, 1, "\u028B"], [7610, 1, "\u028C"], [7611, 1, "z"], [7612, 1, "\u0290"], [7613, 1, "\u0291"], [7614, 1, "\u0292"], [7615, 1, "\u03B8"], [[7616, 7619], 2], [[7620, 7626], 2], [[7627, 7654], 2], [[7655, 7669], 2], [[7670, 7673], 2], [7674, 2], [7675, 2], [7676, 2], [7677, 2], [[7678, 7679], 2], [7680, 1, "\u1E01"], [7681, 2], [7682, 1, "\u1E03"], [7683, 2], [7684, 1, "\u1E05"], [7685, 2], [7686, 1, "\u1E07"], [7687, 2], [7688, 1, "\u1E09"], [7689, 2], [7690, 1, "\u1E0B"], [7691, 2], [7692, 1, "\u1E0D"], [7693, 2], [7694, 1, "\u1E0F"], [7695, 2], [7696, 1, "\u1E11"], [7697, 2], [7698, 1, "\u1E13"], [7699, 2], [7700, 1, "\u1E15"], [7701, 2], [7702, 1, "\u1E17"], [7703, 2], [7704, 1, "\u1E19"], [7705, 2], [7706, 1, "\u1E1B"], [7707, 2], [7708, 1, "\u1E1D"], [7709, 2], [7710, 1, "\u1E1F"], [7711, 2], [7712, 1, "\u1E21"], [7713, 2], [7714, 1, "\u1E23"], [7715, 2], [7716, 1, "\u1E25"], [7717, 2], [7718, 1, "\u1E27"], [7719, 2], [7720, 1, "\u1E29"], [7721, 2], [7722, 1, "\u1E2B"], [7723, 2], [7724, 1, "\u1E2D"], [7725, 2], [7726, 1, "\u1E2F"], [7727, 2], [7728, 1, "\u1E31"], [7729, 2], [7730, 1, "\u1E33"], [7731, 2], [7732, 1, "\u1E35"], [7733, 2], [7734, 1, "\u1E37"], [7735, 2], [7736, 1, "\u1E39"], [7737, 2], [7738, 1, "\u1E3B"], [7739, 2], [7740, 1, "\u1E3D"], [7741, 2], [7742, 1, "\u1E3F"], [7743, 2], [7744, 1, "\u1E41"], [7745, 2], [7746, 1, "\u1E43"], [7747, 2], [7748, 1, "\u1E45"], [7749, 2], [7750, 1, "\u1E47"], [7751, 2], [7752, 1, "\u1E49"], [7753, 2], [7754, 1, "\u1E4B"], [7755, 2], [7756, 1, "\u1E4D"], [7757, 2], [7758, 1, "\u1E4F"], [7759, 2], [7760, 1, "\u1E51"], [7761, 2], [7762, 1, "\u1E53"], [7763, 2], [7764, 1, "\u1E55"], [7765, 2], [7766, 1, "\u1E57"], [7767, 2], [7768, 1, "\u1E59"], [7769, 2], [7770, 1, "\u1E5B"], [7771, 2], [7772, 1, "\u1E5D"], [7773, 2], [7774, 1, "\u1E5F"], [7775, 2], [7776, 1, "\u1E61"], [7777, 2], [7778, 1, "\u1E63"], [7779, 2], [7780, 1, "\u1E65"], [7781, 2], [7782, 1, "\u1E67"], [7783, 2], [7784, 1, "\u1E69"], [7785, 2], [7786, 1, "\u1E6B"], [7787, 2], [7788, 1, "\u1E6D"], [7789, 2], [7790, 1, "\u1E6F"], [7791, 2], [7792, 1, "\u1E71"], [7793, 2], [7794, 1, "\u1E73"], [7795, 2], [7796, 1, "\u1E75"], [7797, 2], [7798, 1, "\u1E77"], [7799, 2], [7800, 1, "\u1E79"], [7801, 2], [7802, 1, "\u1E7B"], [7803, 2], [7804, 1, "\u1E7D"], [7805, 2], [7806, 1, "\u1E7F"], [7807, 2], [7808, 1, "\u1E81"], [7809, 2], [7810, 1, "\u1E83"], [7811, 2], [7812, 1, "\u1E85"], [7813, 2], [7814, 1, "\u1E87"], [7815, 2], [7816, 1, "\u1E89"], [7817, 2], [7818, 1, "\u1E8B"], [7819, 2], [7820, 1, "\u1E8D"], [7821, 2], [7822, 1, "\u1E8F"], [7823, 2], [7824, 1, "\u1E91"], [7825, 2], [7826, 1, "\u1E93"], [7827, 2], [7828, 1, "\u1E95"], [[7829, 7833], 2], [7834, 1, "a\u02BE"], [7835, 1, "\u1E61"], [[7836, 7837], 2], [7838, 1, "\xDF"], [7839, 2], [7840, 1, "\u1EA1"], [7841, 2], [7842, 1, "\u1EA3"], [7843, 2], [7844, 1, "\u1EA5"], [7845, 2], [7846, 1, "\u1EA7"], [7847, 2], [7848, 1, "\u1EA9"], [7849, 2], [7850, 1, "\u1EAB"], [7851, 2], [7852, 1, "\u1EAD"], [7853, 2], [7854, 1, "\u1EAF"], [7855, 2], [7856, 1, "\u1EB1"], [7857, 2], [7858, 1, "\u1EB3"], [7859, 2], [7860, 1, "\u1EB5"], [7861, 2], [7862, 1, "\u1EB7"], [7863, 2], [7864, 1, "\u1EB9"], [7865, 2], [7866, 1, "\u1EBB"], [7867, 2], [7868, 1, "\u1EBD"], [7869, 2], [7870, 1, "\u1EBF"], [7871, 2], [7872, 1, "\u1EC1"], [7873, 2], [7874, 1, "\u1EC3"], [7875, 2], [7876, 1, "\u1EC5"], [7877, 2], [7878, 1, "\u1EC7"], [7879, 2], [7880, 1, "\u1EC9"], [7881, 2], [7882, 1, "\u1ECB"], [7883, 2], [7884, 1, "\u1ECD"], [7885, 2], [7886, 1, "\u1ECF"], [7887, 2], [7888, 1, "\u1ED1"], [7889, 2], [7890, 1, "\u1ED3"], [7891, 2], [7892, 1, "\u1ED5"], [7893, 2], [7894, 1, "\u1ED7"], [7895, 2], [7896, 1, "\u1ED9"], [7897, 2], [7898, 1, "\u1EDB"], [7899, 2], [7900, 1, "\u1EDD"], [7901, 2], [7902, 1, "\u1EDF"], [7903, 2], [7904, 1, "\u1EE1"], [7905, 2], [7906, 1, "\u1EE3"], [7907, 2], [7908, 1, "\u1EE5"], [7909, 2], [7910, 1, "\u1EE7"], [7911, 2], [7912, 1, "\u1EE9"], [7913, 2], [7914, 1, "\u1EEB"], [7915, 2], [7916, 1, "\u1EED"], [7917, 2], [7918, 1, "\u1EEF"], [7919, 2], [7920, 1, "\u1EF1"], [7921, 2], [7922, 1, "\u1EF3"], [7923, 2], [7924, 1, "\u1EF5"], [7925, 2], [7926, 1, "\u1EF7"], [7927, 2], [7928, 1, "\u1EF9"], [7929, 2], [7930, 1, "\u1EFB"], [7931, 2], [7932, 1, "\u1EFD"], [7933, 2], [7934, 1, "\u1EFF"], [7935, 2], [[7936, 7943], 2], [7944, 1, "\u1F00"], [7945, 1, "\u1F01"], [7946, 1, "\u1F02"], [7947, 1, "\u1F03"], [7948, 1, "\u1F04"], [7949, 1, "\u1F05"], [7950, 1, "\u1F06"], [7951, 1, "\u1F07"], [[7952, 7957], 2], [[7958, 7959], 3], [7960, 1, "\u1F10"], [7961, 1, "\u1F11"], [7962, 1, "\u1F12"], [7963, 1, "\u1F13"], [7964, 1, "\u1F14"], [7965, 1, "\u1F15"], [[7966, 7967], 3], [[7968, 7975], 2], [7976, 1, "\u1F20"], [7977, 1, "\u1F21"], [7978, 1, "\u1F22"], [7979, 1, "\u1F23"], [7980, 1, "\u1F24"], [7981, 1, "\u1F25"], [7982, 1, "\u1F26"], [7983, 1, "\u1F27"], [[7984, 7991], 2], [7992, 1, "\u1F30"], [7993, 1, "\u1F31"], [7994, 1, "\u1F32"], [7995, 1, "\u1F33"], [7996, 1, "\u1F34"], [7997, 1, "\u1F35"], [7998, 1, "\u1F36"], [7999, 1, "\u1F37"], [[8e3, 8005], 2], [[8006, 8007], 3], [8008, 1, "\u1F40"], [8009, 1, "\u1F41"], [8010, 1, "\u1F42"], [8011, 1, "\u1F43"], [8012, 1, "\u1F44"], [8013, 1, "\u1F45"], [[8014, 8015], 3], [[8016, 8023], 2], [8024, 3], [8025, 1, "\u1F51"], [8026, 3], [8027, 1, "\u1F53"], [8028, 3], [8029, 1, "\u1F55"], [8030, 3], [8031, 1, "\u1F57"], [[8032, 8039], 2], [8040, 1, "\u1F60"], [8041, 1, "\u1F61"], [8042, 1, "\u1F62"], [8043, 1, "\u1F63"], [8044, 1, "\u1F64"], [8045, 1, "\u1F65"], [8046, 1, "\u1F66"], [8047, 1, "\u1F67"], [8048, 2], [8049, 1, "\u03AC"], [8050, 2], [8051, 1, "\u03AD"], [8052, 2], [8053, 1, "\u03AE"], [8054, 2], [8055, 1, "\u03AF"], [8056, 2], [8057, 1, "\u03CC"], [8058, 2], [8059, 1, "\u03CD"], [8060, 2], [8061, 1, "\u03CE"], [[8062, 8063], 3], [8064, 1, "\u1F00\u03B9"], [8065, 1, "\u1F01\u03B9"], [8066, 1, "\u1F02\u03B9"], [8067, 1, "\u1F03\u03B9"], [8068, 1, "\u1F04\u03B9"], [8069, 1, "\u1F05\u03B9"], [8070, 1, "\u1F06\u03B9"], [8071, 1, "\u1F07\u03B9"], [8072, 1, "\u1F00\u03B9"], [8073, 1, "\u1F01\u03B9"], [8074, 1, "\u1F02\u03B9"], [8075, 1, "\u1F03\u03B9"], [8076, 1, "\u1F04\u03B9"], [8077, 1, "\u1F05\u03B9"], [8078, 1, "\u1F06\u03B9"], [8079, 1, "\u1F07\u03B9"], [8080, 1, "\u1F20\u03B9"], [8081, 1, "\u1F21\u03B9"], [8082, 1, "\u1F22\u03B9"], [8083, 1, "\u1F23\u03B9"], [8084, 1, "\u1F24\u03B9"], [8085, 1, "\u1F25\u03B9"], [8086, 1, "\u1F26\u03B9"], [8087, 1, "\u1F27\u03B9"], [8088, 1, "\u1F20\u03B9"], [8089, 1, "\u1F21\u03B9"], [8090, 1, "\u1F22\u03B9"], [8091, 1, "\u1F23\u03B9"], [8092, 1, "\u1F24\u03B9"], [8093, 1, "\u1F25\u03B9"], [8094, 1, "\u1F26\u03B9"], [8095, 1, "\u1F27\u03B9"], [8096, 1, "\u1F60\u03B9"], [8097, 1, "\u1F61\u03B9"], [8098, 1, "\u1F62\u03B9"], [8099, 1, "\u1F63\u03B9"], [8100, 1, "\u1F64\u03B9"], [8101, 1, "\u1F65\u03B9"], [8102, 1, "\u1F66\u03B9"], [8103, 1, "\u1F67\u03B9"], [8104, 1, "\u1F60\u03B9"], [8105, 1, "\u1F61\u03B9"], [8106, 1, "\u1F62\u03B9"], [8107, 1, "\u1F63\u03B9"], [8108, 1, "\u1F64\u03B9"], [8109, 1, "\u1F65\u03B9"], [8110, 1, "\u1F66\u03B9"], [8111, 1, "\u1F67\u03B9"], [[8112, 8113], 2], [8114, 1, "\u1F70\u03B9"], [8115, 1, "\u03B1\u03B9"], [8116, 1, "\u03AC\u03B9"], [8117, 3], [8118, 2], [8119, 1, "\u1FB6\u03B9"], [8120, 1, "\u1FB0"], [8121, 1, "\u1FB1"], [8122, 1, "\u1F70"], [8123, 1, "\u03AC"], [8124, 1, "\u03B1\u03B9"], [8125, 5, " \u0313"], [8126, 1, "\u03B9"], [8127, 5, " \u0313"], [8128, 5, " \u0342"], [8129, 5, " \u0308\u0342"], [8130, 1, "\u1F74\u03B9"], [8131, 1, "\u03B7\u03B9"], [8132, 1, "\u03AE\u03B9"], [8133, 3], [8134, 2], [8135, 1, "\u1FC6\u03B9"], [8136, 1, "\u1F72"], [8137, 1, "\u03AD"], [8138, 1, "\u1F74"], [8139, 1, "\u03AE"], [8140, 1, "\u03B7\u03B9"], [8141, 5, " \u0313\u0300"], [8142, 5, " \u0313\u0301"], [8143, 5, " \u0313\u0342"], [[8144, 8146], 2], [8147, 1, "\u0390"], [[8148, 8149], 3], [[8150, 8151], 2], [8152, 1, "\u1FD0"], [8153, 1, "\u1FD1"], [8154, 1, "\u1F76"], [8155, 1, "\u03AF"], [8156, 3], [8157, 5, " \u0314\u0300"], [8158, 5, " \u0314\u0301"], [8159, 5, " \u0314\u0342"], [[8160, 8162], 2], [8163, 1, "\u03B0"], [[8164, 8167], 2], [8168, 1, "\u1FE0"], [8169, 1, "\u1FE1"], [8170, 1, "\u1F7A"], [8171, 1, "\u03CD"], [8172, 1, "\u1FE5"], [8173, 5, " \u0308\u0300"], [8174, 5, " \u0308\u0301"], [8175, 5, "`"], [[8176, 8177], 3], [8178, 1, "\u1F7C\u03B9"], [8179, 1, "\u03C9\u03B9"], [8180, 1, "\u03CE\u03B9"], [8181, 3], [8182, 2], [8183, 1, "\u1FF6\u03B9"], [8184, 1, "\u1F78"], [8185, 1, "\u03CC"], [8186, 1, "\u1F7C"], [8187, 1, "\u03CE"], [8188, 1, "\u03C9\u03B9"], [8189, 5, " \u0301"], [8190, 5, " \u0314"], [8191, 3], [[8192, 8202], 5, " "], [8203, 7], [[8204, 8205], 6, ""], [[8206, 8207], 3], [8208, 2], [8209, 1, "\u2010"], [[8210, 8214], 2], [8215, 5, " \u0333"], [[8216, 8227], 2], [[8228, 8230], 3], [8231, 2], [[8232, 8238], 3], [8239, 5, " "], [[8240, 8242], 2], [8243, 1, "\u2032\u2032"], [8244, 1, "\u2032\u2032\u2032"], [8245, 2], [8246, 1, "\u2035\u2035"], [8247, 1, "\u2035\u2035\u2035"], [[8248, 8251], 2], [8252, 5, "!!"], [8253, 2], [8254, 5, " \u0305"], [[8255, 8262], 2], [8263, 5, "??"], [8264, 5, "?!"], [8265, 5, "!?"], [[8266, 8269], 2], [[8270, 8274], 2], [[8275, 8276], 2], [[8277, 8278], 2], [8279, 1, "\u2032\u2032\u2032\u2032"], [[8280, 8286], 2], [8287, 5, " "], [8288, 7], [[8289, 8291], 3], [8292, 7], [8293, 3], [[8294, 8297], 3], [[8298, 8303], 3], [8304, 1, "0"], [8305, 1, "i"], [[8306, 8307], 3], [8308, 1, "4"], [8309, 1, "5"], [8310, 1, "6"], [8311, 1, "7"], [8312, 1, "8"], [8313, 1, "9"], [8314, 5, "+"], [8315, 1, "\u2212"], [8316, 5, "="], [8317, 5, "("], [8318, 5, ")"], [8319, 1, "n"], [8320, 1, "0"], [8321, 1, "1"], [8322, 1, "2"], [8323, 1, "3"], [8324, 1, "4"], [8325, 1, "5"], [8326, 1, "6"], [8327, 1, "7"], [8328, 1, "8"], [8329, 1, "9"], [8330, 5, "+"], [8331, 1, "\u2212"], [8332, 5, "="], [8333, 5, "("], [8334, 5, ")"], [8335, 3], [8336, 1, "a"], [8337, 1, "e"], [8338, 1, "o"], [8339, 1, "x"], [8340, 1, "\u0259"], [8341, 1, "h"], [8342, 1, "k"], [8343, 1, "l"], [8344, 1, "m"], [8345, 1, "n"], [8346, 1, "p"], [8347, 1, "s"], [8348, 1, "t"], [[8349, 8351], 3], [[8352, 8359], 2], [8360, 1, "rs"], [[8361, 8362], 2], [8363, 2], [8364, 2], [[8365, 8367], 2], [[8368, 8369], 2], [[8370, 8373], 2], [[8374, 8376], 2], [8377, 2], [8378, 2], [[8379, 8381], 2], [8382, 2], [8383, 2], [8384, 2], [[8385, 8399], 3], [[8400, 8417], 2], [[8418, 8419], 2], [[8420, 8426], 2], [8427, 2], [[8428, 8431], 2], [8432, 2], [[8433, 8447], 3], [8448, 5, "a/c"], [8449, 5, "a/s"], [8450, 1, "c"], [8451, 1, "\xB0c"], [8452, 2], [8453, 5, "c/o"], [8454, 5, "c/u"], [8455, 1, "\u025B"], [8456, 2], [8457, 1, "\xB0f"], [8458, 1, "g"], [[8459, 8462], 1, "h"], [8463, 1, "\u0127"], [[8464, 8465], 1, "i"], [[8466, 8467], 1, "l"], [8468, 2], [8469, 1, "n"], [8470, 1, "no"], [[8471, 8472], 2], [8473, 1, "p"], [8474, 1, "q"], [[8475, 8477], 1, "r"], [[8478, 8479], 2], [8480, 1, "sm"], [8481, 1, "tel"], [8482, 1, "tm"], [8483, 2], [8484, 1, "z"], [8485, 2], [8486, 1, "\u03C9"], [8487, 2], [8488, 1, "z"], [8489, 2], [8490, 1, "k"], [8491, 1, "\xE5"], [8492, 1, "b"], [8493, 1, "c"], [8494, 2], [[8495, 8496], 1, "e"], [8497, 1, "f"], [8498, 3], [8499, 1, "m"], [8500, 1, "o"], [8501, 1, "\u05D0"], [8502, 1, "\u05D1"], [8503, 1, "\u05D2"], [8504, 1, "\u05D3"], [8505, 1, "i"], [8506, 2], [8507, 1, "fax"], [8508, 1, "\u03C0"], [[8509, 8510], 1, "\u03B3"], [8511, 1, "\u03C0"], [8512, 1, "\u2211"], [[8513, 8516], 2], [[8517, 8518], 1, "d"], [8519, 1, "e"], [8520, 1, "i"], [8521, 1, "j"], [[8522, 8523], 2], [8524, 2], [8525, 2], [8526, 2], [8527, 2], [8528, 1, "1\u20447"], [8529, 1, "1\u20449"], [8530, 1, "1\u204410"], [8531, 1, "1\u20443"], [8532, 1, "2\u20443"], [8533, 1, "1\u20445"], [8534, 1, "2\u20445"], [8535, 1, "3\u20445"], [8536, 1, "4\u20445"], [8537, 1, "1\u20446"], [8538, 1, "5\u20446"], [8539, 1, "1\u20448"], [8540, 1, "3\u20448"], [8541, 1, "5\u20448"], [8542, 1, "7\u20448"], [8543, 1, "1\u2044"], [8544, 1, "i"], [8545, 1, "ii"], [8546, 1, "iii"], [8547, 1, "iv"], [8548, 1, "v"], [8549, 1, "vi"], [8550, 1, "vii"], [8551, 1, "viii"], [8552, 1, "ix"], [8553, 1, "x"], [8554, 1, "xi"], [8555, 1, "xii"], [8556, 1, "l"], [8557, 1, "c"], [8558, 1, "d"], [8559, 1, "m"], [8560, 1, "i"], [8561, 1, "ii"], [8562, 1, "iii"], [8563, 1, "iv"], [8564, 1, "v"], [8565, 1, "vi"], [8566, 1, "vii"], [8567, 1, "viii"], [8568, 1, "ix"], [8569, 1, "x"], [8570, 1, "xi"], [8571, 1, "xii"], [8572, 1, "l"], [8573, 1, "c"], [8574, 1, "d"], [8575, 1, "m"], [[8576, 8578], 2], [8579, 3], [8580, 2], [[8581, 8584], 2], [8585, 1, "0\u20443"], [[8586, 8587], 2], [[8588, 8591], 3], [[8592, 8682], 2], [[8683, 8691], 2], [[8692, 8703], 2], [[8704, 8747], 2], [8748, 1, "\u222B\u222B"], [8749, 1, "\u222B\u222B\u222B"], [8750, 2], [8751, 1, "\u222E\u222E"], [8752, 1, "\u222E\u222E\u222E"], [[8753, 8945], 2], [[8946, 8959], 2], [8960, 2], [8961, 2], [[8962, 9e3], 2], [9001, 1, "\u3008"], [9002, 1, "\u3009"], [[9003, 9082], 2], [9083, 2], [9084, 2], [[9085, 9114], 2], [[9115, 9166], 2], [[9167, 9168], 2], [[9169, 9179], 2], [[9180, 9191], 2], [9192, 2], [[9193, 9203], 2], [[9204, 9210], 2], [[9211, 9214], 2], [9215, 2], [[9216, 9252], 2], [[9253, 9254], 2], [[9255, 9279], 3], [[9280, 9290], 2], [[9291, 9311], 3], [9312, 1, "1"], [9313, 1, "2"], [9314, 1, "3"], [9315, 1, "4"], [9316, 1, "5"], [9317, 1, "6"], [9318, 1, "7"], [9319, 1, "8"], [9320, 1, "9"], [9321, 1, "10"], [9322, 1, "11"], [9323, 1, "12"], [9324, 1, "13"], [9325, 1, "14"], [9326, 1, "15"], [9327, 1, "16"], [9328, 1, "17"], [9329, 1, "18"], [9330, 1, "19"], [9331, 1, "20"], [9332, 5, "(1)"], [9333, 5, "(2)"], [9334, 5, "(3)"], [9335, 5, "(4)"], [9336, 5, "(5)"], [9337, 5, "(6)"], [9338, 5, "(7)"], [9339, 5, "(8)"], [9340, 5, "(9)"], [9341, 5, "(10)"], [9342, 5, "(11)"], [9343, 5, "(12)"], [9344, 5, "(13)"], [9345, 5, "(14)"], [9346, 5, "(15)"], [9347, 5, "(16)"], [9348, 5, "(17)"], [9349, 5, "(18)"], [9350, 5, "(19)"], [9351, 5, "(20)"], [[9352, 9371], 3], [9372, 5, "(a)"], [9373, 5, "(b)"], [9374, 5, "(c)"], [9375, 5, "(d)"], [9376, 5, "(e)"], [9377, 5, "(f)"], [9378, 5, "(g)"], [9379, 5, "(h)"], [9380, 5, "(i)"], [9381, 5, "(j)"], [9382, 5, "(k)"], [9383, 5, "(l)"], [9384, 5, "(m)"], [9385, 5, "(n)"], [9386, 5, "(o)"], [9387, 5, "(p)"], [9388, 5, "(q)"], [9389, 5, "(r)"], [9390, 5, "(s)"], [9391, 5, "(t)"], [9392, 5, "(u)"], [9393, 5, "(v)"], [9394, 5, "(w)"], [9395, 5, "(x)"], [9396, 5, "(y)"], [9397, 5, "(z)"], [9398, 1, "a"], [9399, 1, "b"], [9400, 1, "c"], [9401, 1, "d"], [9402, 1, "e"], [9403, 1, "f"], [9404, 1, "g"], [9405, 1, "h"], [9406, 1, "i"], [9407, 1, "j"], [9408, 1, "k"], [9409, 1, "l"], [9410, 1, "m"], [9411, 1, "n"], [9412, 1, "o"], [9413, 1, "p"], [9414, 1, "q"], [9415, 1, "r"], [9416, 1, "s"], [9417, 1, "t"], [9418, 1, "u"], [9419, 1, "v"], [9420, 1, "w"], [9421, 1, "x"], [9422, 1, "y"], [9423, 1, "z"], [9424, 1, "a"], [9425, 1, "b"], [9426, 1, "c"], [9427, 1, "d"], [9428, 1, "e"], [9429, 1, "f"], [9430, 1, "g"], [9431, 1, "h"], [9432, 1, "i"], [9433, 1, "j"], [9434, 1, "k"], [9435, 1, "l"], [9436, 1, "m"], [9437, 1, "n"], [9438, 1, "o"], [9439, 1, "p"], [9440, 1, "q"], [9441, 1, "r"], [9442, 1, "s"], [9443, 1, "t"], [9444, 1, "u"], [9445, 1, "v"], [9446, 1, "w"], [9447, 1, "x"], [9448, 1, "y"], [9449, 1, "z"], [9450, 1, "0"], [[9451, 9470], 2], [9471, 2], [[9472, 9621], 2], [[9622, 9631], 2], [[9632, 9711], 2], [[9712, 9719], 2], [[9720, 9727], 2], [[9728, 9747], 2], [[9748, 9749], 2], [[9750, 9751], 2], [9752, 2], [9753, 2], [[9754, 9839], 2], [[9840, 9841], 2], [[9842, 9853], 2], [[9854, 9855], 2], [[9856, 9865], 2], [[9866, 9873], 2], [[9874, 9884], 2], [9885, 2], [[9886, 9887], 2], [[9888, 9889], 2], [[9890, 9905], 2], [9906, 2], [[9907, 9916], 2], [[9917, 9919], 2], [[9920, 9923], 2], [[9924, 9933], 2], [9934, 2], [[9935, 9953], 2], [9954, 2], [9955, 2], [[9956, 9959], 2], [[9960, 9983], 2], [9984, 2], [[9985, 9988], 2], [9989, 2], [[9990, 9993], 2], [[9994, 9995], 2], [[9996, 10023], 2], [10024, 2], [[10025, 10059], 2], [10060, 2], [10061, 2], [10062, 2], [[10063, 10066], 2], [[10067, 10069], 2], [10070, 2], [10071, 2], [[10072, 10078], 2], [[10079, 10080], 2], [[10081, 10087], 2], [[10088, 10101], 2], [[10102, 10132], 2], [[10133, 10135], 2], [[10136, 10159], 2], [10160, 2], [[10161, 10174], 2], [10175, 2], [[10176, 10182], 2], [[10183, 10186], 2], [10187, 2], [10188, 2], [10189, 2], [[10190, 10191], 2], [[10192, 10219], 2], [[10220, 10223], 2], [[10224, 10239], 2], [[10240, 10495], 2], [[10496, 10763], 2], [10764, 1, "\u222B\u222B\u222B\u222B"], [[10765, 10867], 2], [10868, 5, "::="], [10869, 5, "=="], [10870, 5, "==="], [[10871, 10971], 2], [10972, 1, "\u2ADD\u0338"], [[10973, 11007], 2], [[11008, 11021], 2], [[11022, 11027], 2], [[11028, 11034], 2], [[11035, 11039], 2], [[11040, 11043], 2], [[11044, 11084], 2], [[11085, 11087], 2], [[11088, 11092], 2], [[11093, 11097], 2], [[11098, 11123], 2], [[11124, 11125], 3], [[11126, 11157], 2], [11158, 3], [11159, 2], [[11160, 11193], 2], [[11194, 11196], 2], [[11197, 11208], 2], [11209, 2], [[11210, 11217], 2], [11218, 2], [[11219, 11243], 2], [[11244, 11247], 2], [[11248, 11262], 2], [11263, 2], [11264, 1, "\u2C30"], [11265, 1, "\u2C31"], [11266, 1, "\u2C32"], [11267, 1, "\u2C33"], [11268, 1, "\u2C34"], [11269, 1, "\u2C35"], [11270, 1, "\u2C36"], [11271, 1, "\u2C37"], [11272, 1, "\u2C38"], [11273, 1, "\u2C39"], [11274, 1, "\u2C3A"], [11275, 1, "\u2C3B"], [11276, 1, "\u2C3C"], [11277, 1, "\u2C3D"], [11278, 1, "\u2C3E"], [11279, 1, "\u2C3F"], [11280, 1, "\u2C40"], [11281, 1, "\u2C41"], [11282, 1, "\u2C42"], [11283, 1, "\u2C43"], [11284, 1, "\u2C44"], [11285, 1, "\u2C45"], [11286, 1, "\u2C46"], [11287, 1, "\u2C47"], [11288, 1, "\u2C48"], [11289, 1, "\u2C49"], [11290, 1, "\u2C4A"], [11291, 1, "\u2C4B"], [11292, 1, "\u2C4C"], [11293, 1, "\u2C4D"], [11294, 1, "\u2C4E"], [11295, 1, "\u2C4F"], [11296, 1, "\u2C50"], [11297, 1, "\u2C51"], [11298, 1, "\u2C52"], [11299, 1, "\u2C53"], [11300, 1, "\u2C54"], [11301, 1, "\u2C55"], [11302, 1, "\u2C56"], [11303, 1, "\u2C57"], [11304, 1, "\u2C58"], [11305, 1, "\u2C59"], [11306, 1, "\u2C5A"], [11307, 1, "\u2C5B"], [11308, 1, "\u2C5C"], [11309, 1, "\u2C5D"], [11310, 1, "\u2C5E"], [11311, 1, "\u2C5F"], [[11312, 11358], 2], [11359, 2], [11360, 1, "\u2C61"], [11361, 2], [11362, 1, "\u026B"], [11363, 1, "\u1D7D"], [11364, 1, "\u027D"], [[11365, 11366], 2], [11367, 1, "\u2C68"], [11368, 2], [11369, 1, "\u2C6A"], [11370, 2], [11371, 1, "\u2C6C"], [11372, 2], [11373, 1, "\u0251"], [11374, 1, "\u0271"], [11375, 1, "\u0250"], [11376, 1, "\u0252"], [11377, 2], [11378, 1, "\u2C73"], [11379, 2], [11380, 2], [11381, 1, "\u2C76"], [[11382, 11383], 2], [[11384, 11387], 2], [11388, 1, "j"], [11389, 1, "v"], [11390, 1, "\u023F"], [11391, 1, "\u0240"], [11392, 1, "\u2C81"], [11393, 2], [11394, 1, "\u2C83"], [11395, 2], [11396, 1, "\u2C85"], [11397, 2], [11398, 1, "\u2C87"], [11399, 2], [11400, 1, "\u2C89"], [11401, 2], [11402, 1, "\u2C8B"], [11403, 2], [11404, 1, "\u2C8D"], [11405, 2], [11406, 1, "\u2C8F"], [11407, 2], [11408, 1, "\u2C91"], [11409, 2], [11410, 1, "\u2C93"], [11411, 2], [11412, 1, "\u2C95"], [11413, 2], [11414, 1, "\u2C97"], [11415, 2], [11416, 1, "\u2C99"], [11417, 2], [11418, 1, "\u2C9B"], [11419, 2], [11420, 1, "\u2C9D"], [11421, 2], [11422, 1, "\u2C9F"], [11423, 2], [11424, 1, "\u2CA1"], [11425, 2], [11426, 1, "\u2CA3"], [11427, 2], [11428, 1, "\u2CA5"], [11429, 2], [11430, 1, "\u2CA7"], [11431, 2], [11432, 1, "\u2CA9"], [11433, 2], [11434, 1, "\u2CAB"], [11435, 2], [11436, 1, "\u2CAD"], [11437, 2], [11438, 1, "\u2CAF"], [11439, 2], [11440, 1, "\u2CB1"], [11441, 2], [11442, 1, "\u2CB3"], [11443, 2], [11444, 1, "\u2CB5"], [11445, 2], [11446, 1, "\u2CB7"], [11447, 2], [11448, 1, "\u2CB9"], [11449, 2], [11450, 1, "\u2CBB"], [11451, 2], [11452, 1, "\u2CBD"], [11453, 2], [11454, 1, "\u2CBF"], [11455, 2], [11456, 1, "\u2CC1"], [11457, 2], [11458, 1, "\u2CC3"], [11459, 2], [11460, 1, "\u2CC5"], [11461, 2], [11462, 1, "\u2CC7"], [11463, 2], [11464, 1, "\u2CC9"], [11465, 2], [11466, 1, "\u2CCB"], [11467, 2], [11468, 1, "\u2CCD"], [11469, 2], [11470, 1, "\u2CCF"], [11471, 2], [11472, 1, "\u2CD1"], [11473, 2], [11474, 1, "\u2CD3"], [11475, 2], [11476, 1, "\u2CD5"], [11477, 2], [11478, 1, "\u2CD7"], [11479, 2], [11480, 1, "\u2CD9"], [11481, 2], [11482, 1, "\u2CDB"], [11483, 2], [11484, 1, "\u2CDD"], [11485, 2], [11486, 1, "\u2CDF"], [11487, 2], [11488, 1, "\u2CE1"], [11489, 2], [11490, 1, "\u2CE3"], [[11491, 11492], 2], [[11493, 11498], 2], [11499, 1, "\u2CEC"], [11500, 2], [11501, 1, "\u2CEE"], [[11502, 11505], 2], [11506, 1, "\u2CF3"], [11507, 2], [[11508, 11512], 3], [[11513, 11519], 2], [[11520, 11557], 2], [11558, 3], [11559, 2], [[11560, 11564], 3], [11565, 2], [[11566, 11567], 3], [[11568, 11621], 2], [[11622, 11623], 2], [[11624, 11630], 3], [11631, 1, "\u2D61"], [11632, 2], [[11633, 11646], 3], [11647, 2], [[11648, 11670], 2], [[11671, 11679], 3], [[11680, 11686], 2], [11687, 3], [[11688, 11694], 2], [11695, 3], [[11696, 11702], 2], [11703, 3], [[11704, 11710], 2], [11711, 3], [[11712, 11718], 2], [11719, 3], [[11720, 11726], 2], [11727, 3], [[11728, 11734], 2], [11735, 3], [[11736, 11742], 2], [11743, 3], [[11744, 11775], 2], [[11776, 11799], 2], [[11800, 11803], 2], [[11804, 11805], 2], [[11806, 11822], 2], [11823, 2], [11824, 2], [11825, 2], [[11826, 11835], 2], [[11836, 11842], 2], [[11843, 11844], 2], [[11845, 11849], 2], [[11850, 11854], 2], [11855, 2], [[11856, 11858], 2], [[11859, 11869], 2], [[11870, 11903], 3], [[11904, 11929], 2], [11930, 3], [[11931, 11934], 2], [11935, 1, "\u6BCD"], [[11936, 12018], 2], [12019, 1, "\u9F9F"], [[12020, 12031], 3], [12032, 1, "\u4E00"], [12033, 1, "\u4E28"], [12034, 1, "\u4E36"], [12035, 1, "\u4E3F"], [12036, 1, "\u4E59"], [12037, 1, "\u4E85"], [12038, 1, "\u4E8C"], [12039, 1, "\u4EA0"], [12040, 1, "\u4EBA"], [12041, 1, "\u513F"], [12042, 1, "\u5165"], [12043, 1, "\u516B"], [12044, 1, "\u5182"], [12045, 1, "\u5196"], [12046, 1, "\u51AB"], [12047, 1, "\u51E0"], [12048, 1, "\u51F5"], [12049, 1, "\u5200"], [12050, 1, "\u529B"], [12051, 1, "\u52F9"], [12052, 1, "\u5315"], [12053, 1, "\u531A"], [12054, 1, "\u5338"], [12055, 1, "\u5341"], [12056, 1, "\u535C"], [12057, 1, "\u5369"], [12058, 1, "\u5382"], [12059, 1, "\u53B6"], [12060, 1, "\u53C8"], [12061, 1, "\u53E3"], [12062, 1, "\u56D7"], [12063, 1, "\u571F"], [12064, 1, "\u58EB"], [12065, 1, "\u5902"], [12066, 1, "\u590A"], [12067, 1, "\u5915"], [12068, 1, "\u5927"], [12069, 1, "\u5973"], [12070, 1, "\u5B50"], [12071, 1, "\u5B80"], [12072, 1, "\u5BF8"], [12073, 1, "\u5C0F"], [12074, 1, "\u5C22"], [12075, 1, "\u5C38"], [12076, 1, "\u5C6E"], [12077, 1, "\u5C71"], [12078, 1, "\u5DDB"], [12079, 1, "\u5DE5"], [12080, 1, "\u5DF1"], [12081, 1, "\u5DFE"], [12082, 1, "\u5E72"], [12083, 1, "\u5E7A"], [12084, 1, "\u5E7F"], [12085, 1, "\u5EF4"], [12086, 1, "\u5EFE"], [12087, 1, "\u5F0B"], [12088, 1, "\u5F13"], [12089, 1, "\u5F50"], [12090, 1, "\u5F61"], [12091, 1, "\u5F73"], [12092, 1, "\u5FC3"], [12093, 1, "\u6208"], [12094, 1, "\u6236"], [12095, 1, "\u624B"], [12096, 1, "\u652F"], [12097, 1, "\u6534"], [12098, 1, "\u6587"], [12099, 1, "\u6597"], [12100, 1, "\u65A4"], [12101, 1, "\u65B9"], [12102, 1, "\u65E0"], [12103, 1, "\u65E5"], [12104, 1, "\u66F0"], [12105, 1, "\u6708"], [12106, 1, "\u6728"], [12107, 1, "\u6B20"], [12108, 1, "\u6B62"], [12109, 1, "\u6B79"], [12110, 1, "\u6BB3"], [12111, 1, "\u6BCB"], [12112, 1, "\u6BD4"], [12113, 1, "\u6BDB"], [12114, 1, "\u6C0F"], [12115, 1, "\u6C14"], [12116, 1, "\u6C34"], [12117, 1, "\u706B"], [12118, 1, "\u722A"], [12119, 1, "\u7236"], [12120, 1, "\u723B"], [12121, 1, "\u723F"], [12122, 1, "\u7247"], [12123, 1, "\u7259"], [12124, 1, "\u725B"], [12125, 1, "\u72AC"], [12126, 1, "\u7384"], [12127, 1, "\u7389"], [12128, 1, "\u74DC"], [12129, 1, "\u74E6"], [12130, 1, "\u7518"], [12131, 1, "\u751F"], [12132, 1, "\u7528"], [12133, 1, "\u7530"], [12134, 1, "\u758B"], [12135, 1, "\u7592"], [12136, 1, "\u7676"], [12137, 1, "\u767D"], [12138, 1, "\u76AE"], [12139, 1, "\u76BF"], [12140, 1, "\u76EE"], [12141, 1, "\u77DB"], [12142, 1, "\u77E2"], [12143, 1, "\u77F3"], [12144, 1, "\u793A"], [12145, 1, "\u79B8"], [12146, 1, "\u79BE"], [12147, 1, "\u7A74"], [12148, 1, "\u7ACB"], [12149, 1, "\u7AF9"], [12150, 1, "\u7C73"], [12151, 1, "\u7CF8"], [12152, 1, "\u7F36"], [12153, 1, "\u7F51"], [12154, 1, "\u7F8A"], [12155, 1, "\u7FBD"], [12156, 1, "\u8001"], [12157, 1, "\u800C"], [12158, 1, "\u8012"], [12159, 1, "\u8033"], [12160, 1, "\u807F"], [12161, 1, "\u8089"], [12162, 1, "\u81E3"], [12163, 1, "\u81EA"], [12164, 1, "\u81F3"], [12165, 1, "\u81FC"], [12166, 1, "\u820C"], [12167, 1, "\u821B"], [12168, 1, "\u821F"], [12169, 1, "\u826E"], [12170, 1, "\u8272"], [12171, 1, "\u8278"], [12172, 1, "\u864D"], [12173, 1, "\u866B"], [12174, 1, "\u8840"], [12175, 1, "\u884C"], [12176, 1, "\u8863"], [12177, 1, "\u897E"], [12178, 1, "\u898B"], [12179, 1, "\u89D2"], [12180, 1, "\u8A00"], [12181, 1, "\u8C37"], [12182, 1, "\u8C46"], [12183, 1, "\u8C55"], [12184, 1, "\u8C78"], [12185, 1, "\u8C9D"], [12186, 1, "\u8D64"], [12187, 1, "\u8D70"], [12188, 1, "\u8DB3"], [12189, 1, "\u8EAB"], [12190, 1, "\u8ECA"], [12191, 1, "\u8F9B"], [12192, 1, "\u8FB0"], [12193, 1, "\u8FB5"], [12194, 1, "\u9091"], [12195, 1, "\u9149"], [12196, 1, "\u91C6"], [12197, 1, "\u91CC"], [12198, 1, "\u91D1"], [12199, 1, "\u9577"], [12200, 1, "\u9580"], [12201, 1, "\u961C"], [12202, 1, "\u96B6"], [12203, 1, "\u96B9"], [12204, 1, "\u96E8"], [12205, 1, "\u9751"], [12206, 1, "\u975E"], [12207, 1, "\u9762"], [12208, 1, "\u9769"], [12209, 1, "\u97CB"], [12210, 1, "\u97ED"], [12211, 1, "\u97F3"], [12212, 1, "\u9801"], [12213, 1, "\u98A8"], [12214, 1, "\u98DB"], [12215, 1, "\u98DF"], [12216, 1, "\u9996"], [12217, 1, "\u9999"], [12218, 1, "\u99AC"], [12219, 1, "\u9AA8"], [12220, 1, "\u9AD8"], [12221, 1, "\u9ADF"], [12222, 1, "\u9B25"], [12223, 1, "\u9B2F"], [12224, 1, "\u9B32"], [12225, 1, "\u9B3C"], [12226, 1, "\u9B5A"], [12227, 1, "\u9CE5"], [12228, 1, "\u9E75"], [12229, 1, "\u9E7F"], [12230, 1, "\u9EA5"], [12231, 1, "\u9EBB"], [12232, 1, "\u9EC3"], [12233, 1, "\u9ECD"], [12234, 1, "\u9ED1"], [12235, 1, "\u9EF9"], [12236, 1, "\u9EFD"], [12237, 1, "\u9F0E"], [12238, 1, "\u9F13"], [12239, 1, "\u9F20"], [12240, 1, "\u9F3B"], [12241, 1, "\u9F4A"], [12242, 1, "\u9F52"], [12243, 1, "\u9F8D"], [12244, 1, "\u9F9C"], [12245, 1, "\u9FA0"], [[12246, 12271], 3], [[12272, 12283], 3], [[12284, 12287], 3], [12288, 5, " "], [12289, 2], [12290, 1, "."], [[12291, 12292], 2], [[12293, 12295], 2], [[12296, 12329], 2], [[12330, 12333], 2], [[12334, 12341], 2], [12342, 1, "\u3012"], [12343, 2], [12344, 1, "\u5341"], [12345, 1, "\u5344"], [12346, 1, "\u5345"], [12347, 2], [12348, 2], [12349, 2], [12350, 2], [12351, 2], [12352, 3], [[12353, 12436], 2], [[12437, 12438], 2], [[12439, 12440], 3], [[12441, 12442], 2], [12443, 5, " \u3099"], [12444, 5, " \u309A"], [[12445, 12446], 2], [12447, 1, "\u3088\u308A"], [12448, 2], [[12449, 12542], 2], [12543, 1, "\u30B3\u30C8"], [[12544, 12548], 3], [[12549, 12588], 2], [12589, 2], [12590, 2], [12591, 2], [12592, 3], [12593, 1, "\u1100"], [12594, 1, "\u1101"], [12595, 1, "\u11AA"], [12596, 1, "\u1102"], [12597, 1, "\u11AC"], [12598, 1, "\u11AD"], [12599, 1, "\u1103"], [12600, 1, "\u1104"], [12601, 1, "\u1105"], [12602, 1, "\u11B0"], [12603, 1, "\u11B1"], [12604, 1, "\u11B2"], [12605, 1, "\u11B3"], [12606, 1, "\u11B4"], [12607, 1, "\u11B5"], [12608, 1, "\u111A"], [12609, 1, "\u1106"], [12610, 1, "\u1107"], [12611, 1, "\u1108"], [12612, 1, "\u1121"], [12613, 1, "\u1109"], [12614, 1, "\u110A"], [12615, 1, "\u110B"], [12616, 1, "\u110C"], [12617, 1, "\u110D"], [12618, 1, "\u110E"], [12619, 1, "\u110F"], [12620, 1, "\u1110"], [12621, 1, "\u1111"], [12622, 1, "\u1112"], [12623, 1, "\u1161"], [12624, 1, "\u1162"], [12625, 1, "\u1163"], [12626, 1, "\u1164"], [12627, 1, "\u1165"], [12628, 1, "\u1166"], [12629, 1, "\u1167"], [12630, 1, "\u1168"], [12631, 1, "\u1169"], [12632, 1, "\u116A"], [12633, 1, "\u116B"], [12634, 1, "\u116C"], [12635, 1, "\u116D"], [12636, 1, "\u116E"], [12637, 1, "\u116F"], [12638, 1, "\u1170"], [12639, 1, "\u1171"], [12640, 1, "\u1172"], [12641, 1, "\u1173"], [12642, 1, "\u1174"], [12643, 1, "\u1175"], [12644, 3], [12645, 1, "\u1114"], [12646, 1, "\u1115"], [12647, 1, "\u11C7"], [12648, 1, "\u11C8"], [12649, 1, "\u11CC"], [12650, 1, "\u11CE"], [12651, 1, "\u11D3"], [12652, 1, "\u11D7"], [12653, 1, "\u11D9"], [12654, 1, "\u111C"], [12655, 1, "\u11DD"], [12656, 1, "\u11DF"], [12657, 1, "\u111D"], [12658, 1, "\u111E"], [12659, 1, "\u1120"], [12660, 1, "\u1122"], [12661, 1, "\u1123"], [12662, 1, "\u1127"], [12663, 1, "\u1129"], [12664, 1, "\u112B"], [12665, 1, "\u112C"], [12666, 1, "\u112D"], [12667, 1, "\u112E"], [12668, 1, "\u112F"], [12669, 1, "\u1132"], [12670, 1, "\u1136"], [12671, 1, "\u1140"], [12672, 1, "\u1147"], [12673, 1, "\u114C"], [12674, 1, "\u11F1"], [12675, 1, "\u11F2"], [12676, 1, "\u1157"], [12677, 1, "\u1158"], [12678, 1, "\u1159"], [12679, 1, "\u1184"], [12680, 1, "\u1185"], [12681, 1, "\u1188"], [12682, 1, "\u1191"], [12683, 1, "\u1192"], [12684, 1, "\u1194"], [12685, 1, "\u119E"], [12686, 1, "\u11A1"], [12687, 3], [[12688, 12689], 2], [12690, 1, "\u4E00"], [12691, 1, "\u4E8C"], [12692, 1, "\u4E09"], [12693, 1, "\u56DB"], [12694, 1, "\u4E0A"], [12695, 1, "\u4E2D"], [12696, 1, "\u4E0B"], [12697, 1, "\u7532"], [12698, 1, "\u4E59"], [12699, 1, "\u4E19"], [12700, 1, "\u4E01"], [12701, 1, "\u5929"], [12702, 1, "\u5730"], [12703, 1, "\u4EBA"], [[12704, 12727], 2], [[12728, 12730], 2], [[12731, 12735], 2], [[12736, 12751], 2], [[12752, 12771], 2], [[12772, 12782], 3], [12783, 3], [[12784, 12799], 2], [12800, 5, "(\u1100)"], [12801, 5, "(\u1102)"], [12802, 5, "(\u1103)"], [12803, 5, "(\u1105)"], [12804, 5, "(\u1106)"], [12805, 5, "(\u1107)"], [12806, 5, "(\u1109)"], [12807, 5, "(\u110B)"], [12808, 5, "(\u110C)"], [12809, 5, "(\u110E)"], [12810, 5, "(\u110F)"], [12811, 5, "(\u1110)"], [12812, 5, "(\u1111)"], [12813, 5, "(\u1112)"], [12814, 5, "(\uAC00)"], [12815, 5, "(\uB098)"], [12816, 5, "(\uB2E4)"], [12817, 5, "(\uB77C)"], [12818, 5, "(\uB9C8)"], [12819, 5, "(\uBC14)"], [12820, 5, "(\uC0AC)"], [12821, 5, "(\uC544)"], [12822, 5, "(\uC790)"], [12823, 5, "(\uCC28)"], [12824, 5, "(\uCE74)"], [12825, 5, "(\uD0C0)"], [12826, 5, "(\uD30C)"], [12827, 5, "(\uD558)"], [12828, 5, "(\uC8FC)"], [12829, 5, "(\uC624\uC804)"], [12830, 5, "(\uC624\uD6C4)"], [12831, 3], [12832, 5, "(\u4E00)"], [12833, 5, "(\u4E8C)"], [12834, 5, "(\u4E09)"], [12835, 5, "(\u56DB)"], [12836, 5, "(\u4E94)"], [12837, 5, "(\u516D)"], [12838, 5, "(\u4E03)"], [12839, 5, "(\u516B)"], [12840, 5, "(\u4E5D)"], [12841, 5, "(\u5341)"], [12842, 5, "(\u6708)"], [12843, 5, "(\u706B)"], [12844, 5, "(\u6C34)"], [12845, 5, "(\u6728)"], [12846, 5, "(\u91D1)"], [12847, 5, "(\u571F)"], [12848, 5, "(\u65E5)"], [12849, 5, "(\u682A)"], [12850, 5, "(\u6709)"], [12851, 5, "(\u793E)"], [12852, 5, "(\u540D)"], [12853, 5, "(\u7279)"], [12854, 5, "(\u8CA1)"], [12855, 5, "(\u795D)"], [12856, 5, "(\u52B4)"], [12857, 5, "(\u4EE3)"], [12858, 5, "(\u547C)"], [12859, 5, "(\u5B66)"], [12860, 5, "(\u76E3)"], [12861, 5, "(\u4F01)"], [12862, 5, "(\u8CC7)"], [12863, 5, "(\u5354)"], [12864, 5, "(\u796D)"], [12865, 5, "(\u4F11)"], [12866, 5, "(\u81EA)"], [12867, 5, "(\u81F3)"], [12868, 1, "\u554F"], [12869, 1, "\u5E7C"], [12870, 1, "\u6587"], [12871, 1, "\u7B8F"], [[12872, 12879], 2], [12880, 1, "pte"], [12881, 1, "21"], [12882, 1, "22"], [12883, 1, "23"], [12884, 1, "24"], [12885, 1, "25"], [12886, 1, "26"], [12887, 1, "27"], [12888, 1, "28"], [12889, 1, "29"], [12890, 1, "30"], [12891, 1, "31"], [12892, 1, "32"], [12893, 1, "33"], [12894, 1, "34"], [12895, 1, "35"], [12896, 1, "\u1100"], [12897, 1, "\u1102"], [12898, 1, "\u1103"], [12899, 1, "\u1105"], [12900, 1, "\u1106"], [12901, 1, "\u1107"], [12902, 1, "\u1109"], [12903, 1, "\u110B"], [12904, 1, "\u110C"], [12905, 1, "\u110E"], [12906, 1, "\u110F"], [12907, 1, "\u1110"], [12908, 1, "\u1111"], [12909, 1, "\u1112"], [12910, 1, "\uAC00"], [12911, 1, "\uB098"], [12912, 1, "\uB2E4"], [12913, 1, "\uB77C"], [12914, 1, "\uB9C8"], [12915, 1, "\uBC14"], [12916, 1, "\uC0AC"], [12917, 1, "\uC544"], [12918, 1, "\uC790"], [12919, 1, "\uCC28"], [12920, 1, "\uCE74"], [12921, 1, "\uD0C0"], [12922, 1, "\uD30C"], [12923, 1, "\uD558"], [12924, 1, "\uCC38\uACE0"], [12925, 1, "\uC8FC\uC758"], [12926, 1, "\uC6B0"], [12927, 2], [12928, 1, "\u4E00"], [12929, 1, "\u4E8C"], [12930, 1, "\u4E09"], [12931, 1, "\u56DB"], [12932, 1, "\u4E94"], [12933, 1, "\u516D"], [12934, 1, "\u4E03"], [12935, 1, "\u516B"], [12936, 1, "\u4E5D"], [12937, 1, "\u5341"], [12938, 1, "\u6708"], [12939, 1, "\u706B"], [12940, 1, "\u6C34"], [12941, 1, "\u6728"], [12942, 1, "\u91D1"], [12943, 1, "\u571F"], [12944, 1, "\u65E5"], [12945, 1, "\u682A"], [12946, 1, "\u6709"], [12947, 1, "\u793E"], [12948, 1, "\u540D"], [12949, 1, "\u7279"], [12950, 1, "\u8CA1"], [12951, 1, "\u795D"], [12952, 1, "\u52B4"], [12953, 1, "\u79D8"], [12954, 1, "\u7537"], [12955, 1, "\u5973"], [12956, 1, "\u9069"], [12957, 1, "\u512A"], [12958, 1, "\u5370"], [12959, 1, "\u6CE8"], [12960, 1, "\u9805"], [12961, 1, "\u4F11"], [12962, 1, "\u5199"], [12963, 1, "\u6B63"], [12964, 1, "\u4E0A"], [12965, 1, "\u4E2D"], [12966, 1, "\u4E0B"], [12967, 1, "\u5DE6"], [12968, 1, "\u53F3"], [12969, 1, "\u533B"], [12970, 1, "\u5B97"], [12971, 1, "\u5B66"], [12972, 1, "\u76E3"], [12973, 1, "\u4F01"], [12974, 1, "\u8CC7"], [12975, 1, "\u5354"], [12976, 1, "\u591C"], [12977, 1, "36"], [12978, 1, "37"], [12979, 1, "38"], [12980, 1, "39"], [12981, 1, "40"], [12982, 1, "41"], [12983, 1, "42"], [12984, 1, "43"], [12985, 1, "44"], [12986, 1, "45"], [12987, 1, "46"], [12988, 1, "47"], [12989, 1, "48"], [12990, 1, "49"], [12991, 1, "50"], [12992, 1, "1\u6708"], [12993, 1, "2\u6708"], [12994, 1, "3\u6708"], [12995, 1, "4\u6708"], [12996, 1, "5\u6708"], [12997, 1, "6\u6708"], [12998, 1, "7\u6708"], [12999, 1, "8\u6708"], [13e3, 1, "9\u6708"], [13001, 1, "10\u6708"], [13002, 1, "11\u6708"], [13003, 1, "12\u6708"], [13004, 1, "hg"], [13005, 1, "erg"], [13006, 1, "ev"], [13007, 1, "ltd"], [13008, 1, "\u30A2"], [13009, 1, "\u30A4"], [13010, 1, "\u30A6"], [13011, 1, "\u30A8"], [13012, 1, "\u30AA"], [13013, 1, "\u30AB"], [13014, 1, "\u30AD"], [13015, 1, "\u30AF"], [13016, 1, "\u30B1"], [13017, 1, "\u30B3"], [13018, 1, "\u30B5"], [13019, 1, "\u30B7"], [13020, 1, "\u30B9"], [13021, 1, "\u30BB"], [13022, 1, "\u30BD"], [13023, 1, "\u30BF"], [13024, 1, "\u30C1"], [13025, 1, "\u30C4"], [13026, 1, "\u30C6"], [13027, 1, "\u30C8"], [13028, 1, "\u30CA"], [13029, 1, "\u30CB"], [13030, 1, "\u30CC"], [13031, 1, "\u30CD"], [13032, 1, "\u30CE"], [13033, 1, "\u30CF"], [13034, 1, "\u30D2"], [13035, 1, "\u30D5"], [13036, 1, "\u30D8"], [13037, 1, "\u30DB"], [13038, 1, "\u30DE"], [13039, 1, "\u30DF"], [13040, 1, "\u30E0"], [13041, 1, "\u30E1"], [13042, 1, "\u30E2"], [13043, 1, "\u30E4"], [13044, 1, "\u30E6"], [13045, 1, "\u30E8"], [13046, 1, "\u30E9"], [13047, 1, "\u30EA"], [13048, 1, "\u30EB"], [13049, 1, "\u30EC"], [13050, 1, "\u30ED"], [13051, 1, "\u30EF"], [13052, 1, "\u30F0"], [13053, 1, "\u30F1"], [13054, 1, "\u30F2"], [13055, 1, "\u4EE4\u548C"], [13056, 1, "\u30A2\u30D1\u30FC\u30C8"], [13057, 1, "\u30A2\u30EB\u30D5\u30A1"], [13058, 1, "\u30A2\u30F3\u30DA\u30A2"], [13059, 1, "\u30A2\u30FC\u30EB"], [13060, 1, "\u30A4\u30CB\u30F3\u30B0"], [13061, 1, "\u30A4\u30F3\u30C1"], [13062, 1, "\u30A6\u30A9\u30F3"], [13063, 1, "\u30A8\u30B9\u30AF\u30FC\u30C9"], [13064, 1, "\u30A8\u30FC\u30AB\u30FC"], [13065, 1, "\u30AA\u30F3\u30B9"], [13066, 1, "\u30AA\u30FC\u30E0"], [13067, 1, "\u30AB\u30A4\u30EA"], [13068, 1, "\u30AB\u30E9\u30C3\u30C8"], [13069, 1, "\u30AB\u30ED\u30EA\u30FC"], [13070, 1, "\u30AC\u30ED\u30F3"], [13071, 1, "\u30AC\u30F3\u30DE"], [13072, 1, "\u30AE\u30AC"], [13073, 1, "\u30AE\u30CB\u30FC"], [13074, 1, "\u30AD\u30E5\u30EA\u30FC"], [13075, 1, "\u30AE\u30EB\u30C0\u30FC"], [13076, 1, "\u30AD\u30ED"], [13077, 1, "\u30AD\u30ED\u30B0\u30E9\u30E0"], [13078, 1, "\u30AD\u30ED\u30E1\u30FC\u30C8\u30EB"], [13079, 1, "\u30AD\u30ED\u30EF\u30C3\u30C8"], [13080, 1, "\u30B0\u30E9\u30E0"], [13081, 1, "\u30B0\u30E9\u30E0\u30C8\u30F3"], [13082, 1, "\u30AF\u30EB\u30BC\u30A4\u30ED"], [13083, 1, "\u30AF\u30ED\u30FC\u30CD"], [13084, 1, "\u30B1\u30FC\u30B9"], [13085, 1, "\u30B3\u30EB\u30CA"], [13086, 1, "\u30B3\u30FC\u30DD"], [13087, 1, "\u30B5\u30A4\u30AF\u30EB"], [13088, 1, "\u30B5\u30F3\u30C1\u30FC\u30E0"], [13089, 1, "\u30B7\u30EA\u30F3\u30B0"], [13090, 1, "\u30BB\u30F3\u30C1"], [13091, 1, "\u30BB\u30F3\u30C8"], [13092, 1, "\u30C0\u30FC\u30B9"], [13093, 1, "\u30C7\u30B7"], [13094, 1, "\u30C9\u30EB"], [13095, 1, "\u30C8\u30F3"], [13096, 1, "\u30CA\u30CE"], [13097, 1, "\u30CE\u30C3\u30C8"], [13098, 1, "\u30CF\u30A4\u30C4"], [13099, 1, "\u30D1\u30FC\u30BB\u30F3\u30C8"], [13100, 1, "\u30D1\u30FC\u30C4"], [13101, 1, "\u30D0\u30FC\u30EC\u30EB"], [13102, 1, "\u30D4\u30A2\u30B9\u30C8\u30EB"], [13103, 1, "\u30D4\u30AF\u30EB"], [13104, 1, "\u30D4\u30B3"], [13105, 1, "\u30D3\u30EB"], [13106, 1, "\u30D5\u30A1\u30E9\u30C3\u30C9"], [13107, 1, "\u30D5\u30A3\u30FC\u30C8"], [13108, 1, "\u30D6\u30C3\u30B7\u30A7\u30EB"], [13109, 1, "\u30D5\u30E9\u30F3"], [13110, 1, "\u30D8\u30AF\u30BF\u30FC\u30EB"], [13111, 1, "\u30DA\u30BD"], [13112, 1, "\u30DA\u30CB\u30D2"], [13113, 1, "\u30D8\u30EB\u30C4"], [13114, 1, "\u30DA\u30F3\u30B9"], [13115, 1, "\u30DA\u30FC\u30B8"], [13116, 1, "\u30D9\u30FC\u30BF"], [13117, 1, "\u30DD\u30A4\u30F3\u30C8"], [13118, 1, "\u30DC\u30EB\u30C8"], [13119, 1, "\u30DB\u30F3"], [13120, 1, "\u30DD\u30F3\u30C9"], [13121, 1, "\u30DB\u30FC\u30EB"], [13122, 1, "\u30DB\u30FC\u30F3"], [13123, 1, "\u30DE\u30A4\u30AF\u30ED"], [13124, 1, "\u30DE\u30A4\u30EB"], [13125, 1, "\u30DE\u30C3\u30CF"], [13126, 1, "\u30DE\u30EB\u30AF"], [13127, 1, "\u30DE\u30F3\u30B7\u30E7\u30F3"], [13128, 1, "\u30DF\u30AF\u30ED\u30F3"], [13129, 1, "\u30DF\u30EA"], [13130, 1, "\u30DF\u30EA\u30D0\u30FC\u30EB"], [13131, 1, "\u30E1\u30AC"], [13132, 1, "\u30E1\u30AC\u30C8\u30F3"], [13133, 1, "\u30E1\u30FC\u30C8\u30EB"], [13134, 1, "\u30E4\u30FC\u30C9"], [13135, 1, "\u30E4\u30FC\u30EB"], [13136, 1, "\u30E6\u30A2\u30F3"], [13137, 1, "\u30EA\u30C3\u30C8\u30EB"], [13138, 1, "\u30EA\u30E9"], [13139, 1, "\u30EB\u30D4\u30FC"], [13140, 1, "\u30EB\u30FC\u30D6\u30EB"], [13141, 1, "\u30EC\u30E0"], [13142, 1, "\u30EC\u30F3\u30C8\u30B2\u30F3"], [13143, 1, "\u30EF\u30C3\u30C8"], [13144, 1, "0\u70B9"], [13145, 1, "1\u70B9"], [13146, 1, "2\u70B9"], [13147, 1, "3\u70B9"], [13148, 1, "4\u70B9"], [13149, 1, "5\u70B9"], [13150, 1, "6\u70B9"], [13151, 1, "7\u70B9"], [13152, 1, "8\u70B9"], [13153, 1, "9\u70B9"], [13154, 1, "10\u70B9"], [13155, 1, "11\u70B9"], [13156, 1, "12\u70B9"], [13157, 1, "13\u70B9"], [13158, 1, "14\u70B9"], [13159, 1, "15\u70B9"], [13160, 1, "16\u70B9"], [13161, 1, "17\u70B9"], [13162, 1, "18\u70B9"], [13163, 1, "19\u70B9"], [13164, 1, "20\u70B9"], [13165, 1, "21\u70B9"], [13166, 1, "22\u70B9"], [13167, 1, "23\u70B9"], [13168, 1, "24\u70B9"], [13169, 1, "hpa"], [13170, 1, "da"], [13171, 1, "au"], [13172, 1, "bar"], [13173, 1, "ov"], [13174, 1, "pc"], [13175, 1, "dm"], [13176, 1, "dm2"], [13177, 1, "dm3"], [13178, 1, "iu"], [13179, 1, "\u5E73\u6210"], [13180, 1, "\u662D\u548C"], [13181, 1, "\u5927\u6B63"], [13182, 1, "\u660E\u6CBB"], [13183, 1, "\u682A\u5F0F\u4F1A\u793E"], [13184, 1, "pa"], [13185, 1, "na"], [13186, 1, "\u03BCa"], [13187, 1, "ma"], [13188, 1, "ka"], [13189, 1, "kb"], [13190, 1, "mb"], [13191, 1, "gb"], [13192, 1, "cal"], [13193, 1, "kcal"], [13194, 1, "pf"], [13195, 1, "nf"], [13196, 1, "\u03BCf"], [13197, 1, "\u03BCg"], [13198, 1, "mg"], [13199, 1, "kg"], [13200, 1, "hz"], [13201, 1, "khz"], [13202, 1, "mhz"], [13203, 1, "ghz"], [13204, 1, "thz"], [13205, 1, "\u03BCl"], [13206, 1, "ml"], [13207, 1, "dl"], [13208, 1, "kl"], [13209, 1, "fm"], [13210, 1, "nm"], [13211, 1, "\u03BCm"], [13212, 1, "mm"], [13213, 1, "cm"], [13214, 1, "km"], [13215, 1, "mm2"], [13216, 1, "cm2"], [13217, 1, "m2"], [13218, 1, "km2"], [13219, 1, "mm3"], [13220, 1, "cm3"], [13221, 1, "m3"], [13222, 1, "km3"], [13223, 1, "m\u2215s"], [13224, 1, "m\u2215s2"], [13225, 1, "pa"], [13226, 1, "kpa"], [13227, 1, "mpa"], [13228, 1, "gpa"], [13229, 1, "rad"], [13230, 1, "rad\u2215s"], [13231, 1, "rad\u2215s2"], [13232, 1, "ps"], [13233, 1, "ns"], [13234, 1, "\u03BCs"], [13235, 1, "ms"], [13236, 1, "pv"], [13237, 1, "nv"], [13238, 1, "\u03BCv"], [13239, 1, "mv"], [13240, 1, "kv"], [13241, 1, "mv"], [13242, 1, "pw"], [13243, 1, "nw"], [13244, 1, "\u03BCw"], [13245, 1, "mw"], [13246, 1, "kw"], [13247, 1, "mw"], [13248, 1, "k\u03C9"], [13249, 1, "m\u03C9"], [13250, 3], [13251, 1, "bq"], [13252, 1, "cc"], [13253, 1, "cd"], [13254, 1, "c\u2215kg"], [13255, 3], [13256, 1, "db"], [13257, 1, "gy"], [13258, 1, "ha"], [13259, 1, "hp"], [13260, 1, "in"], [13261, 1, "kk"], [13262, 1, "km"], [13263, 1, "kt"], [13264, 1, "lm"], [13265, 1, "ln"], [13266, 1, "log"], [13267, 1, "lx"], [13268, 1, "mb"], [13269, 1, "mil"], [13270, 1, "mol"], [13271, 1, "ph"], [13272, 3], [13273, 1, "ppm"], [13274, 1, "pr"], [13275, 1, "sr"], [13276, 1, "sv"], [13277, 1, "wb"], [13278, 1, "v\u2215m"], [13279, 1, "a\u2215m"], [13280, 1, "1\u65E5"], [13281, 1, "2\u65E5"], [13282, 1, "3\u65E5"], [13283, 1, "4\u65E5"], [13284, 1, "5\u65E5"], [13285, 1, "6\u65E5"], [13286, 1, "7\u65E5"], [13287, 1, "8\u65E5"], [13288, 1, "9\u65E5"], [13289, 1, "10\u65E5"], [13290, 1, "11\u65E5"], [13291, 1, "12\u65E5"], [13292, 1, "13\u65E5"], [13293, 1, "14\u65E5"], [13294, 1, "15\u65E5"], [13295, 1, "16\u65E5"], [13296, 1, "17\u65E5"], [13297, 1, "18\u65E5"], [13298, 1, "19\u65E5"], [13299, 1, "20\u65E5"], [13300, 1, "21\u65E5"], [13301, 1, "22\u65E5"], [13302, 1, "23\u65E5"], [13303, 1, "24\u65E5"], [13304, 1, "25\u65E5"], [13305, 1, "26\u65E5"], [13306, 1, "27\u65E5"], [13307, 1, "28\u65E5"], [13308, 1, "29\u65E5"], [13309, 1, "30\u65E5"], [13310, 1, "31\u65E5"], [13311, 1, "gal"], [[13312, 19893], 2], [[19894, 19903], 2], [[19904, 19967], 2], [[19968, 40869], 2], [[40870, 40891], 2], [[40892, 40899], 2], [[40900, 40907], 2], [40908, 2], [[40909, 40917], 2], [[40918, 40938], 2], [[40939, 40943], 2], [[40944, 40956], 2], [[40957, 40959], 2], [[40960, 42124], 2], [[42125, 42127], 3], [[42128, 42145], 2], [[42146, 42147], 2], [[42148, 42163], 2], [42164, 2], [[42165, 42176], 2], [42177, 2], [[42178, 42180], 2], [42181, 2], [42182, 2], [[42183, 42191], 3], [[42192, 42237], 2], [[42238, 42239], 2], [[42240, 42508], 2], [[42509, 42511], 2], [[42512, 42539], 2], [[42540, 42559], 3], [42560, 1, "\uA641"], [42561, 2], [42562, 1, "\uA643"], [42563, 2], [42564, 1, "\uA645"], [42565, 2], [42566, 1, "\uA647"], [42567, 2], [42568, 1, "\uA649"], [42569, 2], [42570, 1, "\uA64B"], [42571, 2], [42572, 1, "\uA64D"], [42573, 2], [42574, 1, "\uA64F"], [42575, 2], [42576, 1, "\uA651"], [42577, 2], [42578, 1, "\uA653"], [42579, 2], [42580, 1, "\uA655"], [42581, 2], [42582, 1, "\uA657"], [42583, 2], [42584, 1, "\uA659"], [42585, 2], [42586, 1, "\uA65B"], [42587, 2], [42588, 1, "\uA65D"], [42589, 2], [42590, 1, "\uA65F"], [42591, 2], [42592, 1, "\uA661"], [42593, 2], [42594, 1, "\uA663"], [42595, 2], [42596, 1, "\uA665"], [42597, 2], [42598, 1, "\uA667"], [42599, 2], [42600, 1, "\uA669"], [42601, 2], [42602, 1, "\uA66B"], [42603, 2], [42604, 1, "\uA66D"], [[42605, 42607], 2], [[42608, 42611], 2], [[42612, 42619], 2], [[42620, 42621], 2], [42622, 2], [42623, 2], [42624, 1, "\uA681"], [42625, 2], [42626, 1, "\uA683"], [42627, 2], [42628, 1, "\uA685"], [42629, 2], [42630, 1, "\uA687"], [42631, 2], [42632, 1, "\uA689"], [42633, 2], [42634, 1, "\uA68B"], [42635, 2], [42636, 1, "\uA68D"], [42637, 2], [42638, 1, "\uA68F"], [42639, 2], [42640, 1, "\uA691"], [42641, 2], [42642, 1, "\uA693"], [42643, 2], [42644, 1, "\uA695"], [42645, 2], [42646, 1, "\uA697"], [42647, 2], [42648, 1, "\uA699"], [42649, 2], [42650, 1, "\uA69B"], [42651, 2], [42652, 1, "\u044A"], [42653, 1, "\u044C"], [42654, 2], [42655, 2], [[42656, 42725], 2], [[42726, 42735], 2], [[42736, 42737], 2], [[42738, 42743], 2], [[42744, 42751], 3], [[42752, 42774], 2], [[42775, 42778], 2], [[42779, 42783], 2], [[42784, 42785], 2], [42786, 1, "\uA723"], [42787, 2], [42788, 1, "\uA725"], [42789, 2], [42790, 1, "\uA727"], [42791, 2], [42792, 1, "\uA729"], [42793, 2], [42794, 1, "\uA72B"], [42795, 2], [42796, 1, "\uA72D"], [42797, 2], [42798, 1, "\uA72F"], [[42799, 42801], 2], [42802, 1, "\uA733"], [42803, 2], [42804, 1, "\uA735"], [42805, 2], [42806, 1, "\uA737"], [42807, 2], [42808, 1, "\uA739"], [42809, 2], [42810, 1, "\uA73B"], [42811, 2], [42812, 1, "\uA73D"], [42813, 2], [42814, 1, "\uA73F"], [42815, 2], [42816, 1, "\uA741"], [42817, 2], [42818, 1, "\uA743"], [42819, 2], [42820, 1, "\uA745"], [42821, 2], [42822, 1, "\uA747"], [42823, 2], [42824, 1, "\uA749"], [42825, 2], [42826, 1, "\uA74B"], [42827, 2], [42828, 1, "\uA74D"], [42829, 2], [42830, 1, "\uA74F"], [42831, 2], [42832, 1, "\uA751"], [42833, 2], [42834, 1, "\uA753"], [42835, 2], [42836, 1, "\uA755"], [42837, 2], [42838, 1, "\uA757"], [42839, 2], [42840, 1, "\uA759"], [42841, 2], [42842, 1, "\uA75B"], [42843, 2], [42844, 1, "\uA75D"], [42845, 2], [42846, 1, "\uA75F"], [42847, 2], [42848, 1, "\uA761"], [42849, 2], [42850, 1, "\uA763"], [42851, 2], [42852, 1, "\uA765"], [42853, 2], [42854, 1, "\uA767"], [42855, 2], [42856, 1, "\uA769"], [42857, 2], [42858, 1, "\uA76B"], [42859, 2], [42860, 1, "\uA76D"], [42861, 2], [42862, 1, "\uA76F"], [42863, 2], [42864, 1, "\uA76F"], [[42865, 42872], 2], [42873, 1, "\uA77A"], [42874, 2], [42875, 1, "\uA77C"], [42876, 2], [42877, 1, "\u1D79"], [42878, 1, "\uA77F"], [42879, 2], [42880, 1, "\uA781"], [42881, 2], [42882, 1, "\uA783"], [42883, 2], [42884, 1, "\uA785"], [42885, 2], [42886, 1, "\uA787"], [[42887, 42888], 2], [[42889, 42890], 2], [42891, 1, "\uA78C"], [42892, 2], [42893, 1, "\u0265"], [42894, 2], [42895, 2], [42896, 1, "\uA791"], [42897, 2], [42898, 1, "\uA793"], [42899, 2], [[42900, 42901], 2], [42902, 1, "\uA797"], [42903, 2], [42904, 1, "\uA799"], [42905, 2], [42906, 1, "\uA79B"], [42907, 2], [42908, 1, "\uA79D"], [42909, 2], [42910, 1, "\uA79F"], [42911, 2], [42912, 1, "\uA7A1"], [42913, 2], [42914, 1, "\uA7A3"], [42915, 2], [42916, 1, "\uA7A5"], [42917, 2], [42918, 1, "\uA7A7"], [42919, 2], [42920, 1, "\uA7A9"], [42921, 2], [42922, 1, "\u0266"], [42923, 1, "\u025C"], [42924, 1, "\u0261"], [42925, 1, "\u026C"], [42926, 1, "\u026A"], [42927, 2], [42928, 1, "\u029E"], [42929, 1, "\u0287"], [42930, 1, "\u029D"], [42931, 1, "\uAB53"], [42932, 1, "\uA7B5"], [42933, 2], [42934, 1, "\uA7B7"], [42935, 2], [42936, 1, "\uA7B9"], [42937, 2], [42938, 1, "\uA7BB"], [42939, 2], [42940, 1, "\uA7BD"], [42941, 2], [42942, 1, "\uA7BF"], [42943, 2], [42944, 1, "\uA7C1"], [42945, 2], [42946, 1, "\uA7C3"], [42947, 2], [42948, 1, "\uA794"], [42949, 1, "\u0282"], [42950, 1, "\u1D8E"], [42951, 1, "\uA7C8"], [42952, 2], [42953, 1, "\uA7CA"], [42954, 2], [[42955, 42959], 3], [42960, 1, "\uA7D1"], [42961, 2], [42962, 3], [42963, 2], [42964, 3], [42965, 2], [42966, 1, "\uA7D7"], [42967, 2], [42968, 1, "\uA7D9"], [42969, 2], [[42970, 42993], 3], [42994, 1, "c"], [42995, 1, "f"], [42996, 1, "q"], [42997, 1, "\uA7F6"], [42998, 2], [42999, 2], [43e3, 1, "\u0127"], [43001, 1, "\u0153"], [43002, 2], [[43003, 43007], 2], [[43008, 43047], 2], [[43048, 43051], 2], [43052, 2], [[43053, 43055], 3], [[43056, 43065], 2], [[43066, 43071], 3], [[43072, 43123], 2], [[43124, 43127], 2], [[43128, 43135], 3], [[43136, 43204], 2], [43205, 2], [[43206, 43213], 3], [[43214, 43215], 2], [[43216, 43225], 2], [[43226, 43231], 3], [[43232, 43255], 2], [[43256, 43258], 2], [43259, 2], [43260, 2], [43261, 2], [[43262, 43263], 2], [[43264, 43309], 2], [[43310, 43311], 2], [[43312, 43347], 2], [[43348, 43358], 3], [43359, 2], [[43360, 43388], 2], [[43389, 43391], 3], [[43392, 43456], 2], [[43457, 43469], 2], [43470, 3], [[43471, 43481], 2], [[43482, 43485], 3], [[43486, 43487], 2], [[43488, 43518], 2], [43519, 3], [[43520, 43574], 2], [[43575, 43583], 3], [[43584, 43597], 2], [[43598, 43599], 3], [[43600, 43609], 2], [[43610, 43611], 3], [[43612, 43615], 2], [[43616, 43638], 2], [[43639, 43641], 2], [[43642, 43643], 2], [[43644, 43647], 2], [[43648, 43714], 2], [[43715, 43738], 3], [[43739, 43741], 2], [[43742, 43743], 2], [[43744, 43759], 2], [[43760, 43761], 2], [[43762, 43766], 2], [[43767, 43776], 3], [[43777, 43782], 2], [[43783, 43784], 3], [[43785, 43790], 2], [[43791, 43792], 3], [[43793, 43798], 2], [[43799, 43807], 3], [[43808, 43814], 2], [43815, 3], [[43816, 43822], 2], [43823, 3], [[43824, 43866], 2], [43867, 2], [43868, 1, "\uA727"], [43869, 1, "\uAB37"], [43870, 1, "\u026B"], [43871, 1, "\uAB52"], [[43872, 43875], 2], [[43876, 43877], 2], [[43878, 43879], 2], [43880, 2], [43881, 1, "\u028D"], [[43882, 43883], 2], [[43884, 43887], 3], [43888, 1, "\u13A0"], [43889, 1, "\u13A1"], [43890, 1, "\u13A2"], [43891, 1, "\u13A3"], [43892, 1, "\u13A4"], [43893, 1, "\u13A5"], [43894, 1, "\u13A6"], [43895, 1, "\u13A7"], [43896, 1, "\u13A8"], [43897, 1, "\u13A9"], [43898, 1, "\u13AA"], [43899, 1, "\u13AB"], [43900, 1, "\u13AC"], [43901, 1, "\u13AD"], [43902, 1, "\u13AE"], [43903, 1, "\u13AF"], [43904, 1, "\u13B0"], [43905, 1, "\u13B1"], [43906, 1, "\u13B2"], [43907, 1, "\u13B3"], [43908, 1, "\u13B4"], [43909, 1, "\u13B5"], [43910, 1, "\u13B6"], [43911, 1, "\u13B7"], [43912, 1, "\u13B8"], [43913, 1, "\u13B9"], [43914, 1, "\u13BA"], [43915, 1, "\u13BB"], [43916, 1, "\u13BC"], [43917, 1, "\u13BD"], [43918, 1, "\u13BE"], [43919, 1, "\u13BF"], [43920, 1, "\u13C0"], [43921, 1, "\u13C1"], [43922, 1, "\u13C2"], [43923, 1, "\u13C3"], [43924, 1, "\u13C4"], [43925, 1, "\u13C5"], [43926, 1, "\u13C6"], [43927, 1, "\u13C7"], [43928, 1, "\u13C8"], [43929, 1, "\u13C9"], [43930, 1, "\u13CA"], [43931, 1, "\u13CB"], [43932, 1, "\u13CC"], [43933, 1, "\u13CD"], [43934, 1, "\u13CE"], [43935, 1, "\u13CF"], [43936, 1, "\u13D0"], [43937, 1, "\u13D1"], [43938, 1, "\u13D2"], [43939, 1, "\u13D3"], [43940, 1, "\u13D4"], [43941, 1, "\u13D5"], [43942, 1, "\u13D6"], [43943, 1, "\u13D7"], [43944, 1, "\u13D8"], [43945, 1, "\u13D9"], [43946, 1, "\u13DA"], [43947, 1, "\u13DB"], [43948, 1, "\u13DC"], [43949, 1, "\u13DD"], [43950, 1, "\u13DE"], [43951, 1, "\u13DF"], [43952, 1, "\u13E0"], [43953, 1, "\u13E1"], [43954, 1, "\u13E2"], [43955, 1, "\u13E3"], [43956, 1, "\u13E4"], [43957, 1, "\u13E5"], [43958, 1, "\u13E6"], [43959, 1, "\u13E7"], [43960, 1, "\u13E8"], [43961, 1, "\u13E9"], [43962, 1, "\u13EA"], [43963, 1, "\u13EB"], [43964, 1, "\u13EC"], [43965, 1, "\u13ED"], [43966, 1, "\u13EE"], [43967, 1, "\u13EF"], [[43968, 44010], 2], [44011, 2], [[44012, 44013], 2], [[44014, 44015], 3], [[44016, 44025], 2], [[44026, 44031], 3], [[44032, 55203], 2], [[55204, 55215], 3], [[55216, 55238], 2], [[55239, 55242], 3], [[55243, 55291], 2], [[55292, 55295], 3], [[55296, 57343], 3], [[57344, 63743], 3], [63744, 1, "\u8C48"], [63745, 1, "\u66F4"], [63746, 1, "\u8ECA"], [63747, 1, "\u8CC8"], [63748, 1, "\u6ED1"], [63749, 1, "\u4E32"], [63750, 1, "\u53E5"], [[63751, 63752], 1, "\u9F9C"], [63753, 1, "\u5951"], [63754, 1, "\u91D1"], [63755, 1, "\u5587"], [63756, 1, "\u5948"], [63757, 1, "\u61F6"], [63758, 1, "\u7669"], [63759, 1, "\u7F85"], [63760, 1, "\u863F"], [63761, 1, "\u87BA"], [63762, 1, "\u88F8"], [63763, 1, "\u908F"], [63764, 1, "\u6A02"], [63765, 1, "\u6D1B"], [63766, 1, "\u70D9"], [63767, 1, "\u73DE"], [63768, 1, "\u843D"], [63769, 1, "\u916A"], [63770, 1, "\u99F1"], [63771, 1, "\u4E82"], [63772, 1, "\u5375"], [63773, 1, "\u6B04"], [63774, 1, "\u721B"], [63775, 1, "\u862D"], [63776, 1, "\u9E1E"], [63777, 1, "\u5D50"], [63778, 1, "\u6FEB"], [63779, 1, "\u85CD"], [63780, 1, "\u8964"], [63781, 1, "\u62C9"], [63782, 1, "\u81D8"], [63783, 1, "\u881F"], [63784, 1, "\u5ECA"], [63785, 1, "\u6717"], [63786, 1, "\u6D6A"], [63787, 1, "\u72FC"], [63788, 1, "\u90CE"], [63789, 1, "\u4F86"], [63790, 1, "\u51B7"], [63791, 1, "\u52DE"], [63792, 1, "\u64C4"], [63793, 1, "\u6AD3"], [63794, 1, "\u7210"], [63795, 1, "\u76E7"], [63796, 1, "\u8001"], [63797, 1, "\u8606"], [63798, 1, "\u865C"], [63799, 1, "\u8DEF"], [63800, 1, "\u9732"], [63801, 1, "\u9B6F"], [63802, 1, "\u9DFA"], [63803, 1, "\u788C"], [63804, 1, "\u797F"], [63805, 1, "\u7DA0"], [63806, 1, "\u83C9"], [63807, 1, "\u9304"], [63808, 1, "\u9E7F"], [63809, 1, "\u8AD6"], [63810, 1, "\u58DF"], [63811, 1, "\u5F04"], [63812, 1, "\u7C60"], [63813, 1, "\u807E"], [63814, 1, "\u7262"], [63815, 1, "\u78CA"], [63816, 1, "\u8CC2"], [63817, 1, "\u96F7"], [63818, 1, "\u58D8"], [63819, 1, "\u5C62"], [63820, 1, "\u6A13"], [63821, 1, "\u6DDA"], [63822, 1, "\u6F0F"], [63823, 1, "\u7D2F"], [63824, 1, "\u7E37"], [63825, 1, "\u964B"], [63826, 1, "\u52D2"], [63827, 1, "\u808B"], [63828, 1, "\u51DC"], [63829, 1, "\u51CC"], [63830, 1, "\u7A1C"], [63831, 1, "\u7DBE"], [63832, 1, "\u83F1"], [63833, 1, "\u9675"], [63834, 1, "\u8B80"], [63835, 1, "\u62CF"], [63836, 1, "\u6A02"], [63837, 1, "\u8AFE"], [63838, 1, "\u4E39"], [63839, 1, "\u5BE7"], [63840, 1, "\u6012"], [63841, 1, "\u7387"], [63842, 1, "\u7570"], [63843, 1, "\u5317"], [63844, 1, "\u78FB"], [63845, 1, "\u4FBF"], [63846, 1, "\u5FA9"], [63847, 1, "\u4E0D"], [63848, 1, "\u6CCC"], [63849, 1, "\u6578"], [63850, 1, "\u7D22"], [63851, 1, "\u53C3"], [63852, 1, "\u585E"], [63853, 1, "\u7701"], [63854, 1, "\u8449"], [63855, 1, "\u8AAA"], [63856, 1, "\u6BBA"], [63857, 1, "\u8FB0"], [63858, 1, "\u6C88"], [63859, 1, "\u62FE"], [63860, 1, "\u82E5"], [63861, 1, "\u63A0"], [63862, 1, "\u7565"], [63863, 1, "\u4EAE"], [63864, 1, "\u5169"], [63865, 1, "\u51C9"], [63866, 1, "\u6881"], [63867, 1, "\u7CE7"], [63868, 1, "\u826F"], [63869, 1, "\u8AD2"], [63870, 1, "\u91CF"], [63871, 1, "\u52F5"], [63872, 1, "\u5442"], [63873, 1, "\u5973"], [63874, 1, "\u5EEC"], [63875, 1, "\u65C5"], [63876, 1, "\u6FFE"], [63877, 1, "\u792A"], [63878, 1, "\u95AD"], [63879, 1, "\u9A6A"], [63880, 1, "\u9E97"], [63881, 1, "\u9ECE"], [63882, 1, "\u529B"], [63883, 1, "\u66C6"], [63884, 1, "\u6B77"], [63885, 1, "\u8F62"], [63886, 1, "\u5E74"], [63887, 1, "\u6190"], [63888, 1, "\u6200"], [63889, 1, "\u649A"], [63890, 1, "\u6F23"], [63891, 1, "\u7149"], [63892, 1, "\u7489"], [63893, 1, "\u79CA"], [63894, 1, "\u7DF4"], [63895, 1, "\u806F"], [63896, 1, "\u8F26"], [63897, 1, "\u84EE"], [63898, 1, "\u9023"], [63899, 1, "\u934A"], [63900, 1, "\u5217"], [63901, 1, "\u52A3"], [63902, 1, "\u54BD"], [63903, 1, "\u70C8"], [63904, 1, "\u88C2"], [63905, 1, "\u8AAA"], [63906, 1, "\u5EC9"], [63907, 1, "\u5FF5"], [63908, 1, "\u637B"], [63909, 1, "\u6BAE"], [63910, 1, "\u7C3E"], [63911, 1, "\u7375"], [63912, 1, "\u4EE4"], [63913, 1, "\u56F9"], [63914, 1, "\u5BE7"], [63915, 1, "\u5DBA"], [63916, 1, "\u601C"], [63917, 1, "\u73B2"], [63918, 1, "\u7469"], [63919, 1, "\u7F9A"], [63920, 1, "\u8046"], [63921, 1, "\u9234"], [63922, 1, "\u96F6"], [63923, 1, "\u9748"], [63924, 1, "\u9818"], [63925, 1, "\u4F8B"], [63926, 1, "\u79AE"], [63927, 1, "\u91B4"], [63928, 1, "\u96B8"], [63929, 1, "\u60E1"], [63930, 1, "\u4E86"], [63931, 1, "\u50DA"], [63932, 1, "\u5BEE"], [63933, 1, "\u5C3F"], [63934, 1, "\u6599"], [63935, 1, "\u6A02"], [63936, 1, "\u71CE"], [63937, 1, "\u7642"], [63938, 1, "\u84FC"], [63939, 1, "\u907C"], [63940, 1, "\u9F8D"], [63941, 1, "\u6688"], [63942, 1, "\u962E"], [63943, 1, "\u5289"], [63944, 1, "\u677B"], [63945, 1, "\u67F3"], [63946, 1, "\u6D41"], [63947, 1, "\u6E9C"], [63948, 1, "\u7409"], [63949, 1, "\u7559"], [63950, 1, "\u786B"], [63951, 1, "\u7D10"], [63952, 1, "\u985E"], [63953, 1, "\u516D"], [63954, 1, "\u622E"], [63955, 1, "\u9678"], [63956, 1, "\u502B"], [63957, 1, "\u5D19"], [63958, 1, "\u6DEA"], [63959, 1, "\u8F2A"], [63960, 1, "\u5F8B"], [63961, 1, "\u6144"], [63962, 1, "\u6817"], [63963, 1, "\u7387"], [63964, 1, "\u9686"], [63965, 1, "\u5229"], [63966, 1, "\u540F"], [63967, 1, "\u5C65"], [63968, 1, "\u6613"], [63969, 1, "\u674E"], [63970, 1, "\u68A8"], [63971, 1, "\u6CE5"], [63972, 1, "\u7406"], [63973, 1, "\u75E2"], [63974, 1, "\u7F79"], [63975, 1, "\u88CF"], [63976, 1, "\u88E1"], [63977, 1, "\u91CC"], [63978, 1, "\u96E2"], [63979, 1, "\u533F"], [63980, 1, "\u6EBA"], [63981, 1, "\u541D"], [63982, 1, "\u71D0"], [63983, 1, "\u7498"], [63984, 1, "\u85FA"], [63985, 1, "\u96A3"], [63986, 1, "\u9C57"], [63987, 1, "\u9E9F"], [63988, 1, "\u6797"], [63989, 1, "\u6DCB"], [63990, 1, "\u81E8"], [63991, 1, "\u7ACB"], [63992, 1, "\u7B20"], [63993, 1, "\u7C92"], [63994, 1, "\u72C0"], [63995, 1, "\u7099"], [63996, 1, "\u8B58"], [63997, 1, "\u4EC0"], [63998, 1, "\u8336"], [63999, 1, "\u523A"], [64e3, 1, "\u5207"], [64001, 1, "\u5EA6"], [64002, 1, "\u62D3"], [64003, 1, "\u7CD6"], [64004, 1, "\u5B85"], [64005, 1, "\u6D1E"], [64006, 1, "\u66B4"], [64007, 1, "\u8F3B"], [64008, 1, "\u884C"], [64009, 1, "\u964D"], [64010, 1, "\u898B"], [64011, 1, "\u5ED3"], [64012, 1, "\u5140"], [64013, 1, "\u55C0"], [[64014, 64015], 2], [64016, 1, "\u585A"], [64017, 2], [64018, 1, "\u6674"], [[64019, 64020], 2], [64021, 1, "\u51DE"], [64022, 1, "\u732A"], [64023, 1, "\u76CA"], [64024, 1, "\u793C"], [64025, 1, "\u795E"], [64026, 1, "\u7965"], [64027, 1, "\u798F"], [64028, 1, "\u9756"], [64029, 1, "\u7CBE"], [64030, 1, "\u7FBD"], [64031, 2], [64032, 1, "\u8612"], [64033, 2], [64034, 1, "\u8AF8"], [[64035, 64036], 2], [64037, 1, "\u9038"], [64038, 1, "\u90FD"], [[64039, 64041], 2], [64042, 1, "\u98EF"], [64043, 1, "\u98FC"], [64044, 1, "\u9928"], [64045, 1, "\u9DB4"], [64046, 1, "\u90DE"], [64047, 1, "\u96B7"], [64048, 1, "\u4FAE"], [64049, 1, "\u50E7"], [64050, 1, "\u514D"], [64051, 1, "\u52C9"], [64052, 1, "\u52E4"], [64053, 1, "\u5351"], [64054, 1, "\u559D"], [64055, 1, "\u5606"], [64056, 1, "\u5668"], [64057, 1, "\u5840"], [64058, 1, "\u58A8"], [64059, 1, "\u5C64"], [64060, 1, "\u5C6E"], [64061, 1, "\u6094"], [64062, 1, "\u6168"], [64063, 1, "\u618E"], [64064, 1, "\u61F2"], [64065, 1, "\u654F"], [64066, 1, "\u65E2"], [64067, 1, "\u6691"], [64068, 1, "\u6885"], [64069, 1, "\u6D77"], [64070, 1, "\u6E1A"], [64071, 1, "\u6F22"], [64072, 1, "\u716E"], [64073, 1, "\u722B"], [64074, 1, "\u7422"], [64075, 1, "\u7891"], [64076, 1, "\u793E"], [64077, 1, "\u7949"], [64078, 1, "\u7948"], [64079, 1, "\u7950"], [64080, 1, "\u7956"], [64081, 1, "\u795D"], [64082, 1, "\u798D"], [64083, 1, "\u798E"], [64084, 1, "\u7A40"], [64085, 1, "\u7A81"], [64086, 1, "\u7BC0"], [64087, 1, "\u7DF4"], [64088, 1, "\u7E09"], [64089, 1, "\u7E41"], [64090, 1, "\u7F72"], [64091, 1, "\u8005"], [64092, 1, "\u81ED"], [[64093, 64094], 1, "\u8279"], [64095, 1, "\u8457"], [64096, 1, "\u8910"], [64097, 1, "\u8996"], [64098, 1, "\u8B01"], [64099, 1, "\u8B39"], [64100, 1, "\u8CD3"], [64101, 1, "\u8D08"], [64102, 1, "\u8FB6"], [64103, 1, "\u9038"], [64104, 1, "\u96E3"], [64105, 1, "\u97FF"], [64106, 1, "\u983B"], [64107, 1, "\u6075"], [64108, 1, "\u{242EE}"], [64109, 1, "\u8218"], [[64110, 64111], 3], [64112, 1, "\u4E26"], [64113, 1, "\u51B5"], [64114, 1, "\u5168"], [64115, 1, "\u4F80"], [64116, 1, "\u5145"], [64117, 1, "\u5180"], [64118, 1, "\u52C7"], [64119, 1, "\u52FA"], [64120, 1, "\u559D"], [64121, 1, "\u5555"], [64122, 1, "\u5599"], [64123, 1, "\u55E2"], [64124, 1, "\u585A"], [64125, 1, "\u58B3"], [64126, 1, "\u5944"], [64127, 1, "\u5954"], [64128, 1, "\u5A62"], [64129, 1, "\u5B28"], [64130, 1, "\u5ED2"], [64131, 1, "\u5ED9"], [64132, 1, "\u5F69"], [64133, 1, "\u5FAD"], [64134, 1, "\u60D8"], [64135, 1, "\u614E"], [64136, 1, "\u6108"], [64137, 1, "\u618E"], [64138, 1, "\u6160"], [64139, 1, "\u61F2"], [64140, 1, "\u6234"], [64141, 1, "\u63C4"], [64142, 1, "\u641C"], [64143, 1, "\u6452"], [64144, 1, "\u6556"], [64145, 1, "\u6674"], [64146, 1, "\u6717"], [64147, 1, "\u671B"], [64148, 1, "\u6756"], [64149, 1, "\u6B79"], [64150, 1, "\u6BBA"], [64151, 1, "\u6D41"], [64152, 1, "\u6EDB"], [64153, 1, "\u6ECB"], [64154, 1, "\u6F22"], [64155, 1, "\u701E"], [64156, 1, "\u716E"], [64157, 1, "\u77A7"], [64158, 1, "\u7235"], [64159, 1, "\u72AF"], [64160, 1, "\u732A"], [64161, 1, "\u7471"], [64162, 1, "\u7506"], [64163, 1, "\u753B"], [64164, 1, "\u761D"], [64165, 1, "\u761F"], [64166, 1, "\u76CA"], [64167, 1, "\u76DB"], [64168, 1, "\u76F4"], [64169, 1, "\u774A"], [64170, 1, "\u7740"], [64171, 1, "\u78CC"], [64172, 1, "\u7AB1"], [64173, 1, "\u7BC0"], [64174, 1, "\u7C7B"], [64175, 1, "\u7D5B"], [64176, 1, "\u7DF4"], [64177, 1, "\u7F3E"], [64178, 1, "\u8005"], [64179, 1, "\u8352"], [64180, 1, "\u83EF"], [64181, 1, "\u8779"], [64182, 1, "\u8941"], [64183, 1, "\u8986"], [64184, 1, "\u8996"], [64185, 1, "\u8ABF"], [64186, 1, "\u8AF8"], [64187, 1, "\u8ACB"], [64188, 1, "\u8B01"], [64189, 1, "\u8AFE"], [64190, 1, "\u8AED"], [64191, 1, "\u8B39"], [64192, 1, "\u8B8A"], [64193, 1, "\u8D08"], [64194, 1, "\u8F38"], [64195, 1, "\u9072"], [64196, 1, "\u9199"], [64197, 1, "\u9276"], [64198, 1, "\u967C"], [64199, 1, "\u96E3"], [64200, 1, "\u9756"], [64201, 1, "\u97DB"], [64202, 1, "\u97FF"], [64203, 1, "\u980B"], [64204, 1, "\u983B"], [64205, 1, "\u9B12"], [64206, 1, "\u9F9C"], [64207, 1, "\u{2284A}"], [64208, 1, "\u{22844}"], [64209, 1, "\u{233D5}"], [64210, 1, "\u3B9D"], [64211, 1, "\u4018"], [64212, 1, "\u4039"], [64213, 1, "\u{25249}"], [64214, 1, "\u{25CD0}"], [64215, 1, "\u{27ED3}"], [64216, 1, "\u9F43"], [64217, 1, "\u9F8E"], [[64218, 64255], 3], [64256, 1, "ff"], [64257, 1, "fi"], [64258, 1, "fl"], [64259, 1, "ffi"], [64260, 1, "ffl"], [[64261, 64262], 1, "st"], [[64263, 64274], 3], [64275, 1, "\u0574\u0576"], [64276, 1, "\u0574\u0565"], [64277, 1, "\u0574\u056B"], [64278, 1, "\u057E\u0576"], [64279, 1, "\u0574\u056D"], [[64280, 64284], 3], [64285, 1, "\u05D9\u05B4"], [64286, 2], [64287, 1, "\u05F2\u05B7"], [64288, 1, "\u05E2"], [64289, 1, "\u05D0"], [64290, 1, "\u05D3"], [64291, 1, "\u05D4"], [64292, 1, "\u05DB"], [64293, 1, "\u05DC"], [64294, 1, "\u05DD"], [64295, 1, "\u05E8"], [64296, 1, "\u05EA"], [64297, 5, "+"], [64298, 1, "\u05E9\u05C1"], [64299, 1, "\u05E9\u05C2"], [64300, 1, "\u05E9\u05BC\u05C1"], [64301, 1, "\u05E9\u05BC\u05C2"], [64302, 1, "\u05D0\u05B7"], [64303, 1, "\u05D0\u05B8"], [64304, 1, "\u05D0\u05BC"], [64305, 1, "\u05D1\u05BC"], [64306, 1, "\u05D2\u05BC"], [64307, 1, "\u05D3\u05BC"], [64308, 1, "\u05D4\u05BC"], [64309, 1, "\u05D5\u05BC"], [64310, 1, "\u05D6\u05BC"], [64311, 3], [64312, 1, "\u05D8\u05BC"], [64313, 1, "\u05D9\u05BC"], [64314, 1, "\u05DA\u05BC"], [64315, 1, "\u05DB\u05BC"], [64316, 1, "\u05DC\u05BC"], [64317, 3], [64318, 1, "\u05DE\u05BC"], [64319, 3], [64320, 1, "\u05E0\u05BC"], [64321, 1, "\u05E1\u05BC"], [64322, 3], [64323, 1, "\u05E3\u05BC"], [64324, 1, "\u05E4\u05BC"], [64325, 3], [64326, 1, "\u05E6\u05BC"], [64327, 1, "\u05E7\u05BC"], [64328, 1, "\u05E8\u05BC"], [64329, 1, "\u05E9\u05BC"], [64330, 1, "\u05EA\u05BC"], [64331, 1, "\u05D5\u05B9"], [64332, 1, "\u05D1\u05BF"], [64333, 1, "\u05DB\u05BF"], [64334, 1, "\u05E4\u05BF"], [64335, 1, "\u05D0\u05DC"], [[64336, 64337], 1, "\u0671"], [[64338, 64341], 1, "\u067B"], [[64342, 64345], 1, "\u067E"], [[64346, 64349], 1, "\u0680"], [[64350, 64353], 1, "\u067A"], [[64354, 64357], 1, "\u067F"], [[64358, 64361], 1, "\u0679"], [[64362, 64365], 1, "\u06A4"], [[64366, 64369], 1, "\u06A6"], [[64370, 64373], 1, "\u0684"], [[64374, 64377], 1, "\u0683"], [[64378, 64381], 1, "\u0686"], [[64382, 64385], 1, "\u0687"], [[64386, 64387], 1, "\u068D"], [[64388, 64389], 1, "\u068C"], [[64390, 64391], 1, "\u068E"], [[64392, 64393], 1, "\u0688"], [[64394, 64395], 1, "\u0698"], [[64396, 64397], 1, "\u0691"], [[64398, 64401], 1, "\u06A9"], [[64402, 64405], 1, "\u06AF"], [[64406, 64409], 1, "\u06B3"], [[64410, 64413], 1, "\u06B1"], [[64414, 64415], 1, "\u06BA"], [[64416, 64419], 1, "\u06BB"], [[64420, 64421], 1, "\u06C0"], [[64422, 64425], 1, "\u06C1"], [[64426, 64429], 1, "\u06BE"], [[64430, 64431], 1, "\u06D2"], [[64432, 64433], 1, "\u06D3"], [[64434, 64449], 2], [64450, 2], [[64451, 64466], 3], [[64467, 64470], 1, "\u06AD"], [[64471, 64472], 1, "\u06C7"], [[64473, 64474], 1, "\u06C6"], [[64475, 64476], 1, "\u06C8"], [64477, 1, "\u06C7\u0674"], [[64478, 64479], 1, "\u06CB"], [[64480, 64481], 1, "\u06C5"], [[64482, 64483], 1, "\u06C9"], [[64484, 64487], 1, "\u06D0"], [[64488, 64489], 1, "\u0649"], [[64490, 64491], 1, "\u0626\u0627"], [[64492, 64493], 1, "\u0626\u06D5"], [[64494, 64495], 1, "\u0626\u0648"], [[64496, 64497], 1, "\u0626\u06C7"], [[64498, 64499], 1, "\u0626\u06C6"], [[64500, 64501], 1, "\u0626\u06C8"], [[64502, 64504], 1, "\u0626\u06D0"], [[64505, 64507], 1, "\u0626\u0649"], [[64508, 64511], 1, "\u06CC"], [64512, 1, "\u0626\u062C"], [64513, 1, "\u0626\u062D"], [64514, 1, "\u0626\u0645"], [64515, 1, "\u0626\u0649"], [64516, 1, "\u0626\u064A"], [64517, 1, "\u0628\u062C"], [64518, 1, "\u0628\u062D"], [64519, 1, "\u0628\u062E"], [64520, 1, "\u0628\u0645"], [64521, 1, "\u0628\u0649"], [64522, 1, "\u0628\u064A"], [64523, 1, "\u062A\u062C"], [64524, 1, "\u062A\u062D"], [64525, 1, "\u062A\u062E"], [64526, 1, "\u062A\u0645"], [64527, 1, "\u062A\u0649"], [64528, 1, "\u062A\u064A"], [64529, 1, "\u062B\u062C"], [64530, 1, "\u062B\u0645"], [64531, 1, "\u062B\u0649"], [64532, 1, "\u062B\u064A"], [64533, 1, "\u062C\u062D"], [64534, 1, "\u062C\u0645"], [64535, 1, "\u062D\u062C"], [64536, 1, "\u062D\u0645"], [64537, 1, "\u062E\u062C"], [64538, 1, "\u062E\u062D"], [64539, 1, "\u062E\u0645"], [64540, 1, "\u0633\u062C"], [64541, 1, "\u0633\u062D"], [64542, 1, "\u0633\u062E"], [64543, 1, "\u0633\u0645"], [64544, 1, "\u0635\u062D"], [64545, 1, "\u0635\u0645"], [64546, 1, "\u0636\u062C"], [64547, 1, "\u0636\u062D"], [64548, 1, "\u0636\u062E"], [64549, 1, "\u0636\u0645"], [64550, 1, "\u0637\u062D"], [64551, 1, "\u0637\u0645"], [64552, 1, "\u0638\u0645"], [64553, 1, "\u0639\u062C"], [64554, 1, "\u0639\u0645"], [64555, 1, "\u063A\u062C"], [64556, 1, "\u063A\u0645"], [64557, 1, "\u0641\u062C"], [64558, 1, "\u0641\u062D"], [64559, 1, "\u0641\u062E"], [64560, 1, "\u0641\u0645"], [64561, 1, "\u0641\u0649"], [64562, 1, "\u0641\u064A"], [64563, 1, "\u0642\u062D"], [64564, 1, "\u0642\u0645"], [64565, 1, "\u0642\u0649"], [64566, 1, "\u0642\u064A"], [64567, 1, "\u0643\u0627"], [64568, 1, "\u0643\u062C"], [64569, 1, "\u0643\u062D"], [64570, 1, "\u0643\u062E"], [64571, 1, "\u0643\u0644"], [64572, 1, "\u0643\u0645"], [64573, 1, "\u0643\u0649"], [64574, 1, "\u0643\u064A"], [64575, 1, "\u0644\u062C"], [64576, 1, "\u0644\u062D"], [64577, 1, "\u0644\u062E"], [64578, 1, "\u0644\u0645"], [64579, 1, "\u0644\u0649"], [64580, 1, "\u0644\u064A"], [64581, 1, "\u0645\u062C"], [64582, 1, "\u0645\u062D"], [64583, 1, "\u0645\u062E"], [64584, 1, "\u0645\u0645"], [64585, 1, "\u0645\u0649"], [64586, 1, "\u0645\u064A"], [64587, 1, "\u0646\u062C"], [64588, 1, "\u0646\u062D"], [64589, 1, "\u0646\u062E"], [64590, 1, "\u0646\u0645"], [64591, 1, "\u0646\u0649"], [64592, 1, "\u0646\u064A"], [64593, 1, "\u0647\u062C"], [64594, 1, "\u0647\u0645"], [64595, 1, "\u0647\u0649"], [64596, 1, "\u0647\u064A"], [64597, 1, "\u064A\u062C"], [64598, 1, "\u064A\u062D"], [64599, 1, "\u064A\u062E"], [64600, 1, "\u064A\u0645"], [64601, 1, "\u064A\u0649"], [64602, 1, "\u064A\u064A"], [64603, 1, "\u0630\u0670"], [64604, 1, "\u0631\u0670"], [64605, 1, "\u0649\u0670"], [64606, 5, " \u064C\u0651"], [64607, 5, " \u064D\u0651"], [64608, 5, " \u064E\u0651"], [64609, 5, " \u064F\u0651"], [64610, 5, " \u0650\u0651"], [64611, 5, " \u0651\u0670"], [64612, 1, "\u0626\u0631"], [64613, 1, "\u0626\u0632"], [64614, 1, "\u0626\u0645"], [64615, 1, "\u0626\u0646"], [64616, 1, "\u0626\u0649"], [64617, 1, "\u0626\u064A"], [64618, 1, "\u0628\u0631"], [64619, 1, "\u0628\u0632"], [64620, 1, "\u0628\u0645"], [64621, 1, "\u0628\u0646"], [64622, 1, "\u0628\u0649"], [64623, 1, "\u0628\u064A"], [64624, 1, "\u062A\u0631"], [64625, 1, "\u062A\u0632"], [64626, 1, "\u062A\u0645"], [64627, 1, "\u062A\u0646"], [64628, 1, "\u062A\u0649"], [64629, 1, "\u062A\u064A"], [64630, 1, "\u062B\u0631"], [64631, 1, "\u062B\u0632"], [64632, 1, "\u062B\u0645"], [64633, 1, "\u062B\u0646"], [64634, 1, "\u062B\u0649"], [64635, 1, "\u062B\u064A"], [64636, 1, "\u0641\u0649"], [64637, 1, "\u0641\u064A"], [64638, 1, "\u0642\u0649"], [64639, 1, "\u0642\u064A"], [64640, 1, "\u0643\u0627"], [64641, 1, "\u0643\u0644"], [64642, 1, "\u0643\u0645"], [64643, 1, "\u0643\u0649"], [64644, 1, "\u0643\u064A"], [64645, 1, "\u0644\u0645"], [64646, 1, "\u0644\u0649"], [64647, 1, "\u0644\u064A"], [64648, 1, "\u0645\u0627"], [64649, 1, "\u0645\u0645"], [64650, 1, "\u0646\u0631"], [64651, 1, "\u0646\u0632"], [64652, 1, "\u0646\u0645"], [64653, 1, "\u0646\u0646"], [64654, 1, "\u0646\u0649"], [64655, 1, "\u0646\u064A"], [64656, 1, "\u0649\u0670"], [64657, 1, "\u064A\u0631"], [64658, 1, "\u064A\u0632"], [64659, 1, "\u064A\u0645"], [64660, 1, "\u064A\u0646"], [64661, 1, "\u064A\u0649"], [64662, 1, "\u064A\u064A"], [64663, 1, "\u0626\u062C"], [64664, 1, "\u0626\u062D"], [64665, 1, "\u0626\u062E"], [64666, 1, "\u0626\u0645"], [64667, 1, "\u0626\u0647"], [64668, 1, "\u0628\u062C"], [64669, 1, "\u0628\u062D"], [64670, 1, "\u0628\u062E"], [64671, 1, "\u0628\u0645"], [64672, 1, "\u0628\u0647"], [64673, 1, "\u062A\u062C"], [64674, 1, "\u062A\u062D"], [64675, 1, "\u062A\u062E"], [64676, 1, "\u062A\u0645"], [64677, 1, "\u062A\u0647"], [64678, 1, "\u062B\u0645"], [64679, 1, "\u062C\u062D"], [64680, 1, "\u062C\u0645"], [64681, 1, "\u062D\u062C"], [64682, 1, "\u062D\u0645"], [64683, 1, "\u062E\u062C"], [64684, 1, "\u062E\u0645"], [64685, 1, "\u0633\u062C"], [64686, 1, "\u0633\u062D"], [64687, 1, "\u0633\u062E"], [64688, 1, "\u0633\u0645"], [64689, 1, "\u0635\u062D"], [64690, 1, "\u0635\u062E"], [64691, 1, "\u0635\u0645"], [64692, 1, "\u0636\u062C"], [64693, 1, "\u0636\u062D"], [64694, 1, "\u0636\u062E"], [64695, 1, "\u0636\u0645"], [64696, 1, "\u0637\u062D"], [64697, 1, "\u0638\u0645"], [64698, 1, "\u0639\u062C"], [64699, 1, "\u0639\u0645"], [64700, 1, "\u063A\u062C"], [64701, 1, "\u063A\u0645"], [64702, 1, "\u0641\u062C"], [64703, 1, "\u0641\u062D"], [64704, 1, "\u0641\u062E"], [64705, 1, "\u0641\u0645"], [64706, 1, "\u0642\u062D"], [64707, 1, "\u0642\u0645"], [64708, 1, "\u0643\u062C"], [64709, 1, "\u0643\u062D"], [64710, 1, "\u0643\u062E"], [64711, 1, "\u0643\u0644"], [64712, 1, "\u0643\u0645"], [64713, 1, "\u0644\u062C"], [64714, 1, "\u0644\u062D"], [64715, 1, "\u0644\u062E"], [64716, 1, "\u0644\u0645"], [64717, 1, "\u0644\u0647"], [64718, 1, "\u0645\u062C"], [64719, 1, "\u0645\u062D"], [64720, 1, "\u0645\u062E"], [64721, 1, "\u0645\u0645"], [64722, 1, "\u0646\u062C"], [64723, 1, "\u0646\u062D"], [64724, 1, "\u0646\u062E"], [64725, 1, "\u0646\u0645"], [64726, 1, "\u0646\u0647"], [64727, 1, "\u0647\u062C"], [64728, 1, "\u0647\u0645"], [64729, 1, "\u0647\u0670"], [64730, 1, "\u064A\u062C"], [64731, 1, "\u064A\u062D"], [64732, 1, "\u064A\u062E"], [64733, 1, "\u064A\u0645"], [64734, 1, "\u064A\u0647"], [64735, 1, "\u0626\u0645"], [64736, 1, "\u0626\u0647"], [64737, 1, "\u0628\u0645"], [64738, 1, "\u0628\u0647"], [64739, 1, "\u062A\u0645"], [64740, 1, "\u062A\u0647"], [64741, 1, "\u062B\u0645"], [64742, 1, "\u062B\u0647"], [64743, 1, "\u0633\u0645"], [64744, 1, "\u0633\u0647"], [64745, 1, "\u0634\u0645"], [64746, 1, "\u0634\u0647"], [64747, 1, "\u0643\u0644"], [64748, 1, "\u0643\u0645"], [64749, 1, "\u0644\u0645"], [64750, 1, "\u0646\u0645"], [64751, 1, "\u0646\u0647"], [64752, 1, "\u064A\u0645"], [64753, 1, "\u064A\u0647"], [64754, 1, "\u0640\u064E\u0651"], [64755, 1, "\u0640\u064F\u0651"], [64756, 1, "\u0640\u0650\u0651"], [64757, 1, "\u0637\u0649"], [64758, 1, "\u0637\u064A"], [64759, 1, "\u0639\u0649"], [64760, 1, "\u0639\u064A"], [64761, 1, "\u063A\u0649"], [64762, 1, "\u063A\u064A"], [64763, 1, "\u0633\u0649"], [64764, 1, "\u0633\u064A"], [64765, 1, "\u0634\u0649"], [64766, 1, "\u0634\u064A"], [64767, 1, "\u062D\u0649"], [64768, 1, "\u062D\u064A"], [64769, 1, "\u062C\u0649"], [64770, 1, "\u062C\u064A"], [64771, 1, "\u062E\u0649"], [64772, 1, "\u062E\u064A"], [64773, 1, "\u0635\u0649"], [64774, 1, "\u0635\u064A"], [64775, 1, "\u0636\u0649"], [64776, 1, "\u0636\u064A"], [64777, 1, "\u0634\u062C"], [64778, 1, "\u0634\u062D"], [64779, 1, "\u0634\u062E"], [64780, 1, "\u0634\u0645"], [64781, 1, "\u0634\u0631"], [64782, 1, "\u0633\u0631"], [64783, 1, "\u0635\u0631"], [64784, 1, "\u0636\u0631"], [64785, 1, "\u0637\u0649"], [64786, 1, "\u0637\u064A"], [64787, 1, "\u0639\u0649"], [64788, 1, "\u0639\u064A"], [64789, 1, "\u063A\u0649"], [64790, 1, "\u063A\u064A"], [64791, 1, "\u0633\u0649"], [64792, 1, "\u0633\u064A"], [64793, 1, "\u0634\u0649"], [64794, 1, "\u0634\u064A"], [64795, 1, "\u062D\u0649"], [64796, 1, "\u062D\u064A"], [64797, 1, "\u062C\u0649"], [64798, 1, "\u062C\u064A"], [64799, 1, "\u062E\u0649"], [64800, 1, "\u062E\u064A"], [64801, 1, "\u0635\u0649"], [64802, 1, "\u0635\u064A"], [64803, 1, "\u0636\u0649"], [64804, 1, "\u0636\u064A"], [64805, 1, "\u0634\u062C"], [64806, 1, "\u0634\u062D"], [64807, 1, "\u0634\u062E"], [64808, 1, "\u0634\u0645"], [64809, 1, "\u0634\u0631"], [64810, 1, "\u0633\u0631"], [64811, 1, "\u0635\u0631"], [64812, 1, "\u0636\u0631"], [64813, 1, "\u0634\u062C"], [64814, 1, "\u0634\u062D"], [64815, 1, "\u0634\u062E"], [64816, 1, "\u0634\u0645"], [64817, 1, "\u0633\u0647"], [64818, 1, "\u0634\u0647"], [64819, 1, "\u0637\u0645"], [64820, 1, "\u0633\u062C"], [64821, 1, "\u0633\u062D"], [64822, 1, "\u0633\u062E"], [64823, 1, "\u0634\u062C"], [64824, 1, "\u0634\u062D"], [64825, 1, "\u0634\u062E"], [64826, 1, "\u0637\u0645"], [64827, 1, "\u0638\u0645"], [[64828, 64829], 1, "\u0627\u064B"], [[64830, 64831], 2], [[64832, 64847], 2], [64848, 1, "\u062A\u062C\u0645"], [[64849, 64850], 1, "\u062A\u062D\u062C"], [64851, 1, "\u062A\u062D\u0645"], [64852, 1, "\u062A\u062E\u0645"], [64853, 1, "\u062A\u0645\u062C"], [64854, 1, "\u062A\u0645\u062D"], [64855, 1, "\u062A\u0645\u062E"], [[64856, 64857], 1, "\u062C\u0645\u062D"], [64858, 1, "\u062D\u0645\u064A"], [64859, 1, "\u062D\u0645\u0649"], [64860, 1, "\u0633\u062D\u062C"], [64861, 1, "\u0633\u062C\u062D"], [64862, 1, "\u0633\u062C\u0649"], [[64863, 64864], 1, "\u0633\u0645\u062D"], [64865, 1, "\u0633\u0645\u062C"], [[64866, 64867], 1, "\u0633\u0645\u0645"], [[64868, 64869], 1, "\u0635\u062D\u062D"], [64870, 1, "\u0635\u0645\u0645"], [[64871, 64872], 1, "\u0634\u062D\u0645"], [64873, 1, "\u0634\u062C\u064A"], [[64874, 64875], 1, "\u0634\u0645\u062E"], [[64876, 64877], 1, "\u0634\u0645\u0645"], [64878, 1, "\u0636\u062D\u0649"], [[64879, 64880], 1, "\u0636\u062E\u0645"], [[64881, 64882], 1, "\u0637\u0645\u062D"], [64883, 1, "\u0637\u0645\u0645"], [64884, 1, "\u0637\u0645\u064A"], [64885, 1, "\u0639\u062C\u0645"], [[64886, 64887], 1, "\u0639\u0645\u0645"], [64888, 1, "\u0639\u0645\u0649"], [64889, 1, "\u063A\u0645\u0645"], [64890, 1, "\u063A\u0645\u064A"], [64891, 1, "\u063A\u0645\u0649"], [[64892, 64893], 1, "\u0641\u062E\u0645"], [64894, 1, "\u0642\u0645\u062D"], [64895, 1, "\u0642\u0645\u0645"], [64896, 1, "\u0644\u062D\u0645"], [64897, 1, "\u0644\u062D\u064A"], [64898, 1, "\u0644\u062D\u0649"], [[64899, 64900], 1, "\u0644\u062C\u062C"], [[64901, 64902], 1, "\u0644\u062E\u0645"], [[64903, 64904], 1, "\u0644\u0645\u062D"], [64905, 1, "\u0645\u062D\u062C"], [64906, 1, "\u0645\u062D\u0645"], [64907, 1, "\u0645\u062D\u064A"], [64908, 1, "\u0645\u062C\u062D"], [64909, 1, "\u0645\u062C\u0645"], [64910, 1, "\u0645\u062E\u062C"], [64911, 1, "\u0645\u062E\u0645"], [[64912, 64913], 3], [64914, 1, "\u0645\u062C\u062E"], [64915, 1, "\u0647\u0645\u062C"], [64916, 1, "\u0647\u0645\u0645"], [64917, 1, "\u0646\u062D\u0645"], [64918, 1, "\u0646\u062D\u0649"], [[64919, 64920], 1, "\u0646\u062C\u0645"], [64921, 1, "\u0646\u062C\u0649"], [64922, 1, "\u0646\u0645\u064A"], [64923, 1, "\u0646\u0645\u0649"], [[64924, 64925], 1, "\u064A\u0645\u0645"], [64926, 1, "\u0628\u062E\u064A"], [64927, 1, "\u062A\u062C\u064A"], [64928, 1, "\u062A\u062C\u0649"], [64929, 1, "\u062A\u062E\u064A"], [64930, 1, "\u062A\u062E\u0649"], [64931, 1, "\u062A\u0645\u064A"], [64932, 1, "\u062A\u0645\u0649"], [64933, 1, "\u062C\u0645\u064A"], [64934, 1, "\u062C\u062D\u0649"], [64935, 1, "\u062C\u0645\u0649"], [64936, 1, "\u0633\u062E\u0649"], [64937, 1, "\u0635\u062D\u064A"], [64938, 1, "\u0634\u062D\u064A"], [64939, 1, "\u0636\u062D\u064A"], [64940, 1, "\u0644\u062C\u064A"], [64941, 1, "\u0644\u0645\u064A"], [64942, 1, "\u064A\u062D\u064A"], [64943, 1, "\u064A\u062C\u064A"], [64944, 1, "\u064A\u0645\u064A"], [64945, 1, "\u0645\u0645\u064A"], [64946, 1, "\u0642\u0645\u064A"], [64947, 1, "\u0646\u062D\u064A"], [64948, 1, "\u0642\u0645\u062D"], [64949, 1, "\u0644\u062D\u0645"], [64950, 1, "\u0639\u0645\u064A"], [64951, 1, "\u0643\u0645\u064A"], [64952, 1, "\u0646\u062C\u062D"], [64953, 1, "\u0645\u062E\u064A"], [64954, 1, "\u0644\u062C\u0645"], [64955, 1, "\u0643\u0645\u0645"], [64956, 1, "\u0644\u062C\u0645"], [64957, 1, "\u0646\u062C\u062D"], [64958, 1, "\u062C\u062D\u064A"], [64959, 1, "\u062D\u062C\u064A"], [64960, 1, "\u0645\u062C\u064A"], [64961, 1, "\u0641\u0645\u064A"], [64962, 1, "\u0628\u062D\u064A"], [64963, 1, "\u0643\u0645\u0645"], [64964, 1, "\u0639\u062C\u0645"], [64965, 1, "\u0635\u0645\u0645"], [64966, 1, "\u0633\u062E\u064A"], [64967, 1, "\u0646\u062C\u064A"], [[64968, 64974], 3], [64975, 2], [[64976, 65007], 3], [65008, 1, "\u0635\u0644\u06D2"], [65009, 1, "\u0642\u0644\u06D2"], [65010, 1, "\u0627\u0644\u0644\u0647"], [65011, 1, "\u0627\u0643\u0628\u0631"], [65012, 1, "\u0645\u062D\u0645\u062F"], [65013, 1, "\u0635\u0644\u0639\u0645"], [65014, 1, "\u0631\u0633\u0648\u0644"], [65015, 1, "\u0639\u0644\u064A\u0647"], [65016, 1, "\u0648\u0633\u0644\u0645"], [65017, 1, "\u0635\u0644\u0649"], [65018, 5, "\u0635\u0644\u0649 \u0627\u0644\u0644\u0647 \u0639\u0644\u064A\u0647 \u0648\u0633\u0644\u0645"], [65019, 5, "\u062C\u0644 \u062C\u0644\u0627\u0644\u0647"], [65020, 1, "\u0631\u06CC\u0627\u0644"], [65021, 2], [[65022, 65023], 2], [[65024, 65039], 7], [65040, 5, ","], [65041, 1, "\u3001"], [65042, 3], [65043, 5, ":"], [65044, 5, ";"], [65045, 5, "!"], [65046, 5, "?"], [65047, 1, "\u3016"], [65048, 1, "\u3017"], [65049, 3], [[65050, 65055], 3], [[65056, 65059], 2], [[65060, 65062], 2], [[65063, 65069], 2], [[65070, 65071], 2], [65072, 3], [65073, 1, "\u2014"], [65074, 1, "\u2013"], [[65075, 65076], 5, "_"], [65077, 5, "("], [65078, 5, ")"], [65079, 5, "{"], [65080, 5, "}"], [65081, 1, "\u3014"], [65082, 1, "\u3015"], [65083, 1, "\u3010"], [65084, 1, "\u3011"], [65085, 1, "\u300A"], [65086, 1, "\u300B"], [65087, 1, "\u3008"], [65088, 1, "\u3009"], [65089, 1, "\u300C"], [65090, 1, "\u300D"], [65091, 1, "\u300E"], [65092, 1, "\u300F"], [[65093, 65094], 2], [65095, 5, "["], [65096, 5, "]"], [[65097, 65100], 5, " \u0305"], [[65101, 65103], 5, "_"], [65104, 5, ","], [65105, 1, "\u3001"], [65106, 3], [65107, 3], [65108, 5, ";"], [65109, 5, ":"], [65110, 5, "?"], [65111, 5, "!"], [65112, 1, "\u2014"], [65113, 5, "("], [65114, 5, ")"], [65115, 5, "{"], [65116, 5, "}"], [65117, 1, "\u3014"], [65118, 1, "\u3015"], [65119, 5, "#"], [65120, 5, "&"], [65121, 5, "*"], [65122, 5, "+"], [65123, 1, "-"], [65124, 5, "<"], [65125, 5, ">"], [65126, 5, "="], [65127, 3], [65128, 5, "\\"], [65129, 5, "$"], [65130, 5, "%"], [65131, 5, "@"], [[65132, 65135], 3], [65136, 5, " \u064B"], [65137, 1, "\u0640\u064B"], [65138, 5, " \u064C"], [65139, 2], [65140, 5, " \u064D"], [65141, 3], [65142, 5, " \u064E"], [65143, 1, "\u0640\u064E"], [65144, 5, " \u064F"], [65145, 1, "\u0640\u064F"], [65146, 5, " \u0650"], [65147, 1, "\u0640\u0650"], [65148, 5, " \u0651"], [65149, 1, "\u0640\u0651"], [65150, 5, " \u0652"], [65151, 1, "\u0640\u0652"], [65152, 1, "\u0621"], [[65153, 65154], 1, "\u0622"], [[65155, 65156], 1, "\u0623"], [[65157, 65158], 1, "\u0624"], [[65159, 65160], 1, "\u0625"], [[65161, 65164], 1, "\u0626"], [[65165, 65166], 1, "\u0627"], [[65167, 65170], 1, "\u0628"], [[65171, 65172], 1, "\u0629"], [[65173, 65176], 1, "\u062A"], [[65177, 65180], 1, "\u062B"], [[65181, 65184], 1, "\u062C"], [[65185, 65188], 1, "\u062D"], [[65189, 65192], 1, "\u062E"], [[65193, 65194], 1, "\u062F"], [[65195, 65196], 1, "\u0630"], [[65197, 65198], 1, "\u0631"], [[65199, 65200], 1, "\u0632"], [[65201, 65204], 1, "\u0633"], [[65205, 65208], 1, "\u0634"], [[65209, 65212], 1, "\u0635"], [[65213, 65216], 1, "\u0636"], [[65217, 65220], 1, "\u0637"], [[65221, 65224], 1, "\u0638"], [[65225, 65228], 1, "\u0639"], [[65229, 65232], 1, "\u063A"], [[65233, 65236], 1, "\u0641"], [[65237, 65240], 1, "\u0642"], [[65241, 65244], 1, "\u0643"], [[65245, 65248], 1, "\u0644"], [[65249, 65252], 1, "\u0645"], [[65253, 65256], 1, "\u0646"], [[65257, 65260], 1, "\u0647"], [[65261, 65262], 1, "\u0648"], [[65263, 65264], 1, "\u0649"], [[65265, 65268], 1, "\u064A"], [[65269, 65270], 1, "\u0644\u0622"], [[65271, 65272], 1, "\u0644\u0623"], [[65273, 65274], 1, "\u0644\u0625"], [[65275, 65276], 1, "\u0644\u0627"], [[65277, 65278], 3], [65279, 7], [65280, 3], [65281, 5, "!"], [65282, 5, '"'], [65283, 5, "#"], [65284, 5, "$"], [65285, 5, "%"], [65286, 5, "&"], [65287, 5, "'"], [65288, 5, "("], [65289, 5, ")"], [65290, 5, "*"], [65291, 5, "+"], [65292, 5, ","], [65293, 1, "-"], [65294, 1, "."], [65295, 5, "/"], [65296, 1, "0"], [65297, 1, "1"], [65298, 1, "2"], [65299, 1, "3"], [65300, 1, "4"], [65301, 1, "5"], [65302, 1, "6"], [65303, 1, "7"], [65304, 1, "8"], [65305, 1, "9"], [65306, 5, ":"], [65307, 5, ";"], [65308, 5, "<"], [65309, 5, "="], [65310, 5, ">"], [65311, 5, "?"], [65312, 5, "@"], [65313, 1, "a"], [65314, 1, "b"], [65315, 1, "c"], [65316, 1, "d"], [65317, 1, "e"], [65318, 1, "f"], [65319, 1, "g"], [65320, 1, "h"], [65321, 1, "i"], [65322, 1, "j"], [65323, 1, "k"], [65324, 1, "l"], [65325, 1, "m"], [65326, 1, "n"], [65327, 1, "o"], [65328, 1, "p"], [65329, 1, "q"], [65330, 1, "r"], [65331, 1, "s"], [65332, 1, "t"], [65333, 1, "u"], [65334, 1, "v"], [65335, 1, "w"], [65336, 1, "x"], [65337, 1, "y"], [65338, 1, "z"], [65339, 5, "["], [65340, 5, "\\"], [65341, 5, "]"], [65342, 5, "^"], [65343, 5, "_"], [65344, 5, "`"], [65345, 1, "a"], [65346, 1, "b"], [65347, 1, "c"], [65348, 1, "d"], [65349, 1, "e"], [65350, 1, "f"], [65351, 1, "g"], [65352, 1, "h"], [65353, 1, "i"], [65354, 1, "j"], [65355, 1, "k"], [65356, 1, "l"], [65357, 1, "m"], [65358, 1, "n"], [65359, 1, "o"], [65360, 1, "p"], [65361, 1, "q"], [65362, 1, "r"], [65363, 1, "s"], [65364, 1, "t"], [65365, 1, "u"], [65366, 1, "v"], [65367, 1, "w"], [65368, 1, "x"], [65369, 1, "y"], [65370, 1, "z"], [65371, 5, "{"], [65372, 5, "|"], [65373, 5, "}"], [65374, 5, "~"], [65375, 1, "\u2985"], [65376, 1, "\u2986"], [65377, 1, "."], [65378, 1, "\u300C"], [65379, 1, "\u300D"], [65380, 1, "\u3001"], [65381, 1, "\u30FB"], [65382, 1, "\u30F2"], [65383, 1, "\u30A1"], [65384, 1, "\u30A3"], [65385, 1, "\u30A5"], [65386, 1, "\u30A7"], [65387, 1, "\u30A9"], [65388, 1, "\u30E3"], [65389, 1, "\u30E5"], [65390, 1, "\u30E7"], [65391, 1, "\u30C3"], [65392, 1, "\u30FC"], [65393, 1, "\u30A2"], [65394, 1, "\u30A4"], [65395, 1, "\u30A6"], [65396, 1, "\u30A8"], [65397, 1, "\u30AA"], [65398, 1, "\u30AB"], [65399, 1, "\u30AD"], [65400, 1, "\u30AF"], [65401, 1, "\u30B1"], [65402, 1, "\u30B3"], [65403, 1, "\u30B5"], [65404, 1, "\u30B7"], [65405, 1, "\u30B9"], [65406, 1, "\u30BB"], [65407, 1, "\u30BD"], [65408, 1, "\u30BF"], [65409, 1, "\u30C1"], [65410, 1, "\u30C4"], [65411, 1, "\u30C6"], [65412, 1, "\u30C8"], [65413, 1, "\u30CA"], [65414, 1, "\u30CB"], [65415, 1, "\u30CC"], [65416, 1, "\u30CD"], [65417, 1, "\u30CE"], [65418, 1, "\u30CF"], [65419, 1, "\u30D2"], [65420, 1, "\u30D5"], [65421, 1, "\u30D8"], [65422, 1, "\u30DB"], [65423, 1, "\u30DE"], [65424, 1, "\u30DF"], [65425, 1, "\u30E0"], [65426, 1, "\u30E1"], [65427, 1, "\u30E2"], [65428, 1, "\u30E4"], [65429, 1, "\u30E6"], [65430, 1, "\u30E8"], [65431, 1, "\u30E9"], [65432, 1, "\u30EA"], [65433, 1, "\u30EB"], [65434, 1, "\u30EC"], [65435, 1, "\u30ED"], [65436, 1, "\u30EF"], [65437, 1, "\u30F3"], [65438, 1, "\u3099"], [65439, 1, "\u309A"], [65440, 3], [65441, 1, "\u1100"], [65442, 1, "\u1101"], [65443, 1, "\u11AA"], [65444, 1, "\u1102"], [65445, 1, "\u11AC"], [65446, 1, "\u11AD"], [65447, 1, "\u1103"], [65448, 1, "\u1104"], [65449, 1, "\u1105"], [65450, 1, "\u11B0"], [65451, 1, "\u11B1"], [65452, 1, "\u11B2"], [65453, 1, "\u11B3"], [65454, 1, "\u11B4"], [65455, 1, "\u11B5"], [65456, 1, "\u111A"], [65457, 1, "\u1106"], [65458, 1, "\u1107"], [65459, 1, "\u1108"], [65460, 1, "\u1121"], [65461, 1, "\u1109"], [65462, 1, "\u110A"], [65463, 1, "\u110B"], [65464, 1, "\u110C"], [65465, 1, "\u110D"], [65466, 1, "\u110E"], [65467, 1, "\u110F"], [65468, 1, "\u1110"], [65469, 1, "\u1111"], [65470, 1, "\u1112"], [[65471, 65473], 3], [65474, 1, "\u1161"], [65475, 1, "\u1162"], [65476, 1, "\u1163"], [65477, 1, "\u1164"], [65478, 1, "\u1165"], [65479, 1, "\u1166"], [[65480, 65481], 3], [65482, 1, "\u1167"], [65483, 1, "\u1168"], [65484, 1, "\u1169"], [65485, 1, "\u116A"], [65486, 1, "\u116B"], [65487, 1, "\u116C"], [[65488, 65489], 3], [65490, 1, "\u116D"], [65491, 1, "\u116E"], [65492, 1, "\u116F"], [65493, 1, "\u1170"], [65494, 1, "\u1171"], [65495, 1, "\u1172"], [[65496, 65497], 3], [65498, 1, "\u1173"], [65499, 1, "\u1174"], [65500, 1, "\u1175"], [[65501, 65503], 3], [65504, 1, "\xA2"], [65505, 1, "\xA3"], [65506, 1, "\xAC"], [65507, 5, " \u0304"], [65508, 1, "\xA6"], [65509, 1, "\xA5"], [65510, 1, "\u20A9"], [65511, 3], [65512, 1, "\u2502"], [65513, 1, "\u2190"], [65514, 1, "\u2191"], [65515, 1, "\u2192"], [65516, 1, "\u2193"], [65517, 1, "\u25A0"], [65518, 1, "\u25CB"], [[65519, 65528], 3], [[65529, 65531], 3], [65532, 3], [65533, 3], [[65534, 65535], 3], [[65536, 65547], 2], [65548, 3], [[65549, 65574], 2], [65575, 3], [[65576, 65594], 2], [65595, 3], [[65596, 65597], 2], [65598, 3], [[65599, 65613], 2], [[65614, 65615], 3], [[65616, 65629], 2], [[65630, 65663], 3], [[65664, 65786], 2], [[65787, 65791], 3], [[65792, 65794], 2], [[65795, 65798], 3], [[65799, 65843], 2], [[65844, 65846], 3], [[65847, 65855], 2], [[65856, 65930], 2], [[65931, 65932], 2], [[65933, 65934], 2], [65935, 3], [[65936, 65947], 2], [65948, 2], [[65949, 65951], 3], [65952, 2], [[65953, 65999], 3], [[66e3, 66044], 2], [66045, 2], [[66046, 66175], 3], [[66176, 66204], 2], [[66205, 66207], 3], [[66208, 66256], 2], [[66257, 66271], 3], [66272, 2], [[66273, 66299], 2], [[66300, 66303], 3], [[66304, 66334], 2], [66335, 2], [[66336, 66339], 2], [[66340, 66348], 3], [[66349, 66351], 2], [[66352, 66368], 2], [66369, 2], [[66370, 66377], 2], [66378, 2], [[66379, 66383], 3], [[66384, 66426], 2], [[66427, 66431], 3], [[66432, 66461], 2], [66462, 3], [66463, 2], [[66464, 66499], 2], [[66500, 66503], 3], [[66504, 66511], 2], [[66512, 66517], 2], [[66518, 66559], 3], [66560, 1, "\u{10428}"], [66561, 1, "\u{10429}"], [66562, 1, "\u{1042A}"], [66563, 1, "\u{1042B}"], [66564, 1, "\u{1042C}"], [66565, 1, "\u{1042D}"], [66566, 1, "\u{1042E}"], [66567, 1, "\u{1042F}"], [66568, 1, "\u{10430}"], [66569, 1, "\u{10431}"], [66570, 1, "\u{10432}"], [66571, 1, "\u{10433}"], [66572, 1, "\u{10434}"], [66573, 1, "\u{10435}"], [66574, 1, "\u{10436}"], [66575, 1, "\u{10437}"], [66576, 1, "\u{10438}"], [66577, 1, "\u{10439}"], [66578, 1, "\u{1043A}"], [66579, 1, "\u{1043B}"], [66580, 1, "\u{1043C}"], [66581, 1, "\u{1043D}"], [66582, 1, "\u{1043E}"], [66583, 1, "\u{1043F}"], [66584, 1, "\u{10440}"], [66585, 1, "\u{10441}"], [66586, 1, "\u{10442}"], [66587, 1, "\u{10443}"], [66588, 1, "\u{10444}"], [66589, 1, "\u{10445}"], [66590, 1, "\u{10446}"], [66591, 1, "\u{10447}"], [66592, 1, "\u{10448}"], [66593, 1, "\u{10449}"], [66594, 1, "\u{1044A}"], [66595, 1, "\u{1044B}"], [66596, 1, "\u{1044C}"], [66597, 1, "\u{1044D}"], [66598, 1, "\u{1044E}"], [66599, 1, "\u{1044F}"], [[66600, 66637], 2], [[66638, 66717], 2], [[66718, 66719], 3], [[66720, 66729], 2], [[66730, 66735], 3], [66736, 1, "\u{104D8}"], [66737, 1, "\u{104D9}"], [66738, 1, "\u{104DA}"], [66739, 1, "\u{104DB}"], [66740, 1, "\u{104DC}"], [66741, 1, "\u{104DD}"], [66742, 1, "\u{104DE}"], [66743, 1, "\u{104DF}"], [66744, 1, "\u{104E0}"], [66745, 1, "\u{104E1}"], [66746, 1, "\u{104E2}"], [66747, 1, "\u{104E3}"], [66748, 1, "\u{104E4}"], [66749, 1, "\u{104E5}"], [66750, 1, "\u{104E6}"], [66751, 1, "\u{104E7}"], [66752, 1, "\u{104E8}"], [66753, 1, "\u{104E9}"], [66754, 1, "\u{104EA}"], [66755, 1, "\u{104EB}"], [66756, 1, "\u{104EC}"], [66757, 1, "\u{104ED}"], [66758, 1, "\u{104EE}"], [66759, 1, "\u{104EF}"], [66760, 1, "\u{104F0}"], [66761, 1, "\u{104F1}"], [66762, 1, "\u{104F2}"], [66763, 1, "\u{104F3}"], [66764, 1, "\u{104F4}"], [66765, 1, "\u{104F5}"], [66766, 1, "\u{104F6}"], [66767, 1, "\u{104F7}"], [66768, 1, "\u{104F8}"], [66769, 1, "\u{104F9}"], [66770, 1, "\u{104FA}"], [66771, 1, "\u{104FB}"], [[66772, 66775], 3], [[66776, 66811], 2], [[66812, 66815], 3], [[66816, 66855], 2], [[66856, 66863], 3], [[66864, 66915], 2], [[66916, 66926], 3], [66927, 2], [66928, 1, "\u{10597}"], [66929, 1, "\u{10598}"], [66930, 1, "\u{10599}"], [66931, 1, "\u{1059A}"], [66932, 1, "\u{1059B}"], [66933, 1, "\u{1059C}"], [66934, 1, "\u{1059D}"], [66935, 1, "\u{1059E}"], [66936, 1, "\u{1059F}"], [66937, 1, "\u{105A0}"], [66938, 1, "\u{105A1}"], [66939, 3], [66940, 1, "\u{105A3}"], [66941, 1, "\u{105A4}"], [66942, 1, "\u{105A5}"], [66943, 1, "\u{105A6}"], [66944, 1, "\u{105A7}"], [66945, 1, "\u{105A8}"], [66946, 1, "\u{105A9}"], [66947, 1, "\u{105AA}"], [66948, 1, "\u{105AB}"], [66949, 1, "\u{105AC}"], [66950, 1, "\u{105AD}"], [66951, 1, "\u{105AE}"], [66952, 1, "\u{105AF}"], [66953, 1, "\u{105B0}"], [66954, 1, "\u{105B1}"], [66955, 3], [66956, 1, "\u{105B3}"], [66957, 1, "\u{105B4}"], [66958, 1, "\u{105B5}"], [66959, 1, "\u{105B6}"], [66960, 1, "\u{105B7}"], [66961, 1, "\u{105B8}"], [66962, 1, "\u{105B9}"], [66963, 3], [66964, 1, "\u{105BB}"], [66965, 1, "\u{105BC}"], [66966, 3], [[66967, 66977], 2], [66978, 3], [[66979, 66993], 2], [66994, 3], [[66995, 67001], 2], [67002, 3], [[67003, 67004], 2], [[67005, 67071], 3], [[67072, 67382], 2], [[67383, 67391], 3], [[67392, 67413], 2], [[67414, 67423], 3], [[67424, 67431], 2], [[67432, 67455], 3], [67456, 2], [67457, 1, "\u02D0"], [67458, 1, "\u02D1"], [67459, 1, "\xE6"], [67460, 1, "\u0299"], [67461, 1, "\u0253"], [67462, 3], [67463, 1, "\u02A3"], [67464, 1, "\uAB66"], [67465, 1, "\u02A5"], [67466, 1, "\u02A4"], [67467, 1, "\u0256"], [67468, 1, "\u0257"], [67469, 1, "\u1D91"], [67470, 1, "\u0258"], [67471, 1, "\u025E"], [67472, 1, "\u02A9"], [67473, 1, "\u0264"], [67474, 1, "\u0262"], [67475, 1, "\u0260"], [67476, 1, "\u029B"], [67477, 1, "\u0127"], [67478, 1, "\u029C"], [67479, 1, "\u0267"], [67480, 1, "\u0284"], [67481, 1, "\u02AA"], [67482, 1, "\u02AB"], [67483, 1, "\u026C"], [67484, 1, "\u{1DF04}"], [67485, 1, "\uA78E"], [67486, 1, "\u026E"], [67487, 1, "\u{1DF05}"], [67488, 1, "\u028E"], [67489, 1, "\u{1DF06}"], [67490, 1, "\xF8"], [67491, 1, "\u0276"], [67492, 1, "\u0277"], [67493, 1, "q"], [67494, 1, "\u027A"], [67495, 1, "\u{1DF08}"], [67496, 1, "\u027D"], [67497, 1, "\u027E"], [67498, 1, "\u0280"], [67499, 1, "\u02A8"], [67500, 1, "\u02A6"], [67501, 1, "\uAB67"], [67502, 1, "\u02A7"], [67503, 1, "\u0288"], [67504, 1, "\u2C71"], [67505, 3], [67506, 1, "\u028F"], [67507, 1, "\u02A1"], [67508, 1, "\u02A2"], [67509, 1, "\u0298"], [67510, 1, "\u01C0"], [67511, 1, "\u01C1"], [67512, 1, "\u01C2"], [67513, 1, "\u{1DF0A}"], [67514, 1, "\u{1DF1E}"], [[67515, 67583], 3], [[67584, 67589], 2], [[67590, 67591], 3], [67592, 2], [67593, 3], [[67594, 67637], 2], [67638, 3], [[67639, 67640], 2], [[67641, 67643], 3], [67644, 2], [[67645, 67646], 3], [67647, 2], [[67648, 67669], 2], [67670, 3], [[67671, 67679], 2], [[67680, 67702], 2], [[67703, 67711], 2], [[67712, 67742], 2], [[67743, 67750], 3], [[67751, 67759], 2], [[67760, 67807], 3], [[67808, 67826], 2], [67827, 3], [[67828, 67829], 2], [[67830, 67834], 3], [[67835, 67839], 2], [[67840, 67861], 2], [[67862, 67865], 2], [[67866, 67867], 2], [[67868, 67870], 3], [67871, 2], [[67872, 67897], 2], [[67898, 67902], 3], [67903, 2], [[67904, 67967], 3], [[67968, 68023], 2], [[68024, 68027], 3], [[68028, 68029], 2], [[68030, 68031], 2], [[68032, 68047], 2], [[68048, 68049], 3], [[68050, 68095], 2], [[68096, 68099], 2], [68100, 3], [[68101, 68102], 2], [[68103, 68107], 3], [[68108, 68115], 2], [68116, 3], [[68117, 68119], 2], [68120, 3], [[68121, 68147], 2], [[68148, 68149], 2], [[68150, 68151], 3], [[68152, 68154], 2], [[68155, 68158], 3], [68159, 2], [[68160, 68167], 2], [68168, 2], [[68169, 68175], 3], [[68176, 68184], 2], [[68185, 68191], 3], [[68192, 68220], 2], [[68221, 68223], 2], [[68224, 68252], 2], [[68253, 68255], 2], [[68256, 68287], 3], [[68288, 68295], 2], [68296, 2], [[68297, 68326], 2], [[68327, 68330], 3], [[68331, 68342], 2], [[68343, 68351], 3], [[68352, 68405], 2], [[68406, 68408], 3], [[68409, 68415], 2], [[68416, 68437], 2], [[68438, 68439], 3], [[68440, 68447], 2], [[68448, 68466], 2], [[68467, 68471], 3], [[68472, 68479], 2], [[68480, 68497], 2], [[68498, 68504], 3], [[68505, 68508], 2], [[68509, 68520], 3], [[68521, 68527], 2], [[68528, 68607], 3], [[68608, 68680], 2], [[68681, 68735], 3], [68736, 1, "\u{10CC0}"], [68737, 1, "\u{10CC1}"], [68738, 1, "\u{10CC2}"], [68739, 1, "\u{10CC3}"], [68740, 1, "\u{10CC4}"], [68741, 1, "\u{10CC5}"], [68742, 1, "\u{10CC6}"], [68743, 1, "\u{10CC7}"], [68744, 1, "\u{10CC8}"], [68745, 1, "\u{10CC9}"], [68746, 1, "\u{10CCA}"], [68747, 1, "\u{10CCB}"], [68748, 1, "\u{10CCC}"], [68749, 1, "\u{10CCD}"], [68750, 1, "\u{10CCE}"], [68751, 1, "\u{10CCF}"], [68752, 1, "\u{10CD0}"], [68753, 1, "\u{10CD1}"], [68754, 1, "\u{10CD2}"], [68755, 1, "\u{10CD3}"], [68756, 1, "\u{10CD4}"], [68757, 1, "\u{10CD5}"], [68758, 1, "\u{10CD6}"], [68759, 1, "\u{10CD7}"], [68760, 1, "\u{10CD8}"], [68761, 1, "\u{10CD9}"], [68762, 1, "\u{10CDA}"], [68763, 1, "\u{10CDB}"], [68764, 1, "\u{10CDC}"], [68765, 1, "\u{10CDD}"], [68766, 1, "\u{10CDE}"], [68767, 1, "\u{10CDF}"], [68768, 1, "\u{10CE0}"], [68769, 1, "\u{10CE1}"], [68770, 1, "\u{10CE2}"], [68771, 1, "\u{10CE3}"], [68772, 1, "\u{10CE4}"], [68773, 1, "\u{10CE5}"], [68774, 1, "\u{10CE6}"], [68775, 1, "\u{10CE7}"], [68776, 1, "\u{10CE8}"], [68777, 1, "\u{10CE9}"], [68778, 1, "\u{10CEA}"], [68779, 1, "\u{10CEB}"], [68780, 1, "\u{10CEC}"], [68781, 1, "\u{10CED}"], [68782, 1, "\u{10CEE}"], [68783, 1, "\u{10CEF}"], [68784, 1, "\u{10CF0}"], [68785, 1, "\u{10CF1}"], [68786, 1, "\u{10CF2}"], [[68787, 68799], 3], [[68800, 68850], 2], [[68851, 68857], 3], [[68858, 68863], 2], [[68864, 68903], 2], [[68904, 68911], 3], [[68912, 68921], 2], [[68922, 69215], 3], [[69216, 69246], 2], [69247, 3], [[69248, 69289], 2], [69290, 3], [[69291, 69292], 2], [69293, 2], [[69294, 69295], 3], [[69296, 69297], 2], [[69298, 69372], 3], [[69373, 69375], 2], [[69376, 69404], 2], [[69405, 69414], 2], [69415, 2], [[69416, 69423], 3], [[69424, 69456], 2], [[69457, 69465], 2], [[69466, 69487], 3], [[69488, 69509], 2], [[69510, 69513], 2], [[69514, 69551], 3], [[69552, 69572], 2], [[69573, 69579], 2], [[69580, 69599], 3], [[69600, 69622], 2], [[69623, 69631], 3], [[69632, 69702], 2], [[69703, 69709], 2], [[69710, 69713], 3], [[69714, 69733], 2], [[69734, 69743], 2], [[69744, 69749], 2], [[69750, 69758], 3], [69759, 2], [[69760, 69818], 2], [[69819, 69820], 2], [69821, 3], [[69822, 69825], 2], [69826, 2], [[69827, 69836], 3], [69837, 3], [[69838, 69839], 3], [[69840, 69864], 2], [[69865, 69871], 3], [[69872, 69881], 2], [[69882, 69887], 3], [[69888, 69940], 2], [69941, 3], [[69942, 69951], 2], [[69952, 69955], 2], [[69956, 69958], 2], [69959, 2], [[69960, 69967], 3], [[69968, 70003], 2], [[70004, 70005], 2], [70006, 2], [[70007, 70015], 3], [[70016, 70084], 2], [[70085, 70088], 2], [[70089, 70092], 2], [70093, 2], [[70094, 70095], 2], [[70096, 70105], 2], [70106, 2], [70107, 2], [70108, 2], [[70109, 70111], 2], [70112, 3], [[70113, 70132], 2], [[70133, 70143], 3], [[70144, 70161], 2], [70162, 3], [[70163, 70199], 2], [[70200, 70205], 2], [70206, 2], [[70207, 70209], 2], [[70210, 70271], 3], [[70272, 70278], 2], [70279, 3], [70280, 2], [70281, 3], [[70282, 70285], 2], [70286, 3], [[70287, 70301], 2], [70302, 3], [[70303, 70312], 2], [70313, 2], [[70314, 70319], 3], [[70320, 70378], 2], [[70379, 70383], 3], [[70384, 70393], 2], [[70394, 70399], 3], [70400, 2], [[70401, 70403], 2], [70404, 3], [[70405, 70412], 2], [[70413, 70414], 3], [[70415, 70416], 2], [[70417, 70418], 3], [[70419, 70440], 2], [70441, 3], [[70442, 70448], 2], [70449, 3], [[70450, 70451], 2], [70452, 3], [[70453, 70457], 2], [70458, 3], [70459, 2], [[70460, 70468], 2], [[70469, 70470], 3], [[70471, 70472], 2], [[70473, 70474], 3], [[70475, 70477], 2], [[70478, 70479], 3], [70480, 2], [[70481, 70486], 3], [70487, 2], [[70488, 70492], 3], [[70493, 70499], 2], [[70500, 70501], 3], [[70502, 70508], 2], [[70509, 70511], 3], [[70512, 70516], 2], [[70517, 70655], 3], [[70656, 70730], 2], [[70731, 70735], 2], [[70736, 70745], 2], [70746, 2], [70747, 2], [70748, 3], [70749, 2], [70750, 2], [70751, 2], [[70752, 70753], 2], [[70754, 70783], 3], [[70784, 70853], 2], [70854, 2], [70855, 2], [[70856, 70863], 3], [[70864, 70873], 2], [[70874, 71039], 3], [[71040, 71093], 2], [[71094, 71095], 3], [[71096, 71104], 2], [[71105, 71113], 2], [[71114, 71127], 2], [[71128, 71133], 2], [[71134, 71167], 3], [[71168, 71232], 2], [[71233, 71235], 2], [71236, 2], [[71237, 71247], 3], [[71248, 71257], 2], [[71258, 71263], 3], [[71264, 71276], 2], [[71277, 71295], 3], [[71296, 71351], 2], [71352, 2], [71353, 2], [[71354, 71359], 3], [[71360, 71369], 2], [[71370, 71423], 3], [[71424, 71449], 2], [71450, 2], [[71451, 71452], 3], [[71453, 71467], 2], [[71468, 71471], 3], [[71472, 71481], 2], [[71482, 71487], 2], [[71488, 71494], 2], [[71495, 71679], 3], [[71680, 71738], 2], [71739, 2], [[71740, 71839], 3], [71840, 1, "\u{118C0}"], [71841, 1, "\u{118C1}"], [71842, 1, "\u{118C2}"], [71843, 1, "\u{118C3}"], [71844, 1, "\u{118C4}"], [71845, 1, "\u{118C5}"], [71846, 1, "\u{118C6}"], [71847, 1, "\u{118C7}"], [71848, 1, "\u{118C8}"], [71849, 1, "\u{118C9}"], [71850, 1, "\u{118CA}"], [71851, 1, "\u{118CB}"], [71852, 1, "\u{118CC}"], [71853, 1, "\u{118CD}"], [71854, 1, "\u{118CE}"], [71855, 1, "\u{118CF}"], [71856, 1, "\u{118D0}"], [71857, 1, "\u{118D1}"], [71858, 1, "\u{118D2}"], [71859, 1, "\u{118D3}"], [71860, 1, "\u{118D4}"], [71861, 1, "\u{118D5}"], [71862, 1, "\u{118D6}"], [71863, 1, "\u{118D7}"], [71864, 1, "\u{118D8}"], [71865, 1, "\u{118D9}"], [71866, 1, "\u{118DA}"], [71867, 1, "\u{118DB}"], [71868, 1, "\u{118DC}"], [71869, 1, "\u{118DD}"], [71870, 1, "\u{118DE}"], [71871, 1, "\u{118DF}"], [[71872, 71913], 2], [[71914, 71922], 2], [[71923, 71934], 3], [71935, 2], [[71936, 71942], 2], [[71943, 71944], 3], [71945, 2], [[71946, 71947], 3], [[71948, 71955], 2], [71956, 3], [[71957, 71958], 2], [71959, 3], [[71960, 71989], 2], [71990, 3], [[71991, 71992], 2], [[71993, 71994], 3], [[71995, 72003], 2], [[72004, 72006], 2], [[72007, 72015], 3], [[72016, 72025], 2], [[72026, 72095], 3], [[72096, 72103], 2], [[72104, 72105], 3], [[72106, 72151], 2], [[72152, 72153], 3], [[72154, 72161], 2], [72162, 2], [[72163, 72164], 2], [[72165, 72191], 3], [[72192, 72254], 2], [[72255, 72262], 2], [72263, 2], [[72264, 72271], 3], [[72272, 72323], 2], [[72324, 72325], 2], [[72326, 72345], 2], [[72346, 72348], 2], [72349, 2], [[72350, 72354], 2], [[72355, 72367], 3], [[72368, 72383], 2], [[72384, 72440], 2], [[72441, 72447], 3], [[72448, 72457], 2], [[72458, 72703], 3], [[72704, 72712], 2], [72713, 3], [[72714, 72758], 2], [72759, 3], [[72760, 72768], 2], [[72769, 72773], 2], [[72774, 72783], 3], [[72784, 72793], 2], [[72794, 72812], 2], [[72813, 72815], 3], [[72816, 72817], 2], [[72818, 72847], 2], [[72848, 72849], 3], [[72850, 72871], 2], [72872, 3], [[72873, 72886], 2], [[72887, 72959], 3], [[72960, 72966], 2], [72967, 3], [[72968, 72969], 2], [72970, 3], [[72971, 73014], 2], [[73015, 73017], 3], [73018, 2], [73019, 3], [[73020, 73021], 2], [73022, 3], [[73023, 73031], 2], [[73032, 73039], 3], [[73040, 73049], 2], [[73050, 73055], 3], [[73056, 73061], 2], [73062, 3], [[73063, 73064], 2], [73065, 3], [[73066, 73102], 2], [73103, 3], [[73104, 73105], 2], [73106, 3], [[73107, 73112], 2], [[73113, 73119], 3], [[73120, 73129], 2], [[73130, 73439], 3], [[73440, 73462], 2], [[73463, 73464], 2], [[73465, 73471], 3], [[73472, 73488], 2], [73489, 3], [[73490, 73530], 2], [[73531, 73533], 3], [[73534, 73538], 2], [[73539, 73551], 2], [[73552, 73561], 2], [[73562, 73647], 3], [73648, 2], [[73649, 73663], 3], [[73664, 73713], 2], [[73714, 73726], 3], [73727, 2], [[73728, 74606], 2], [[74607, 74648], 2], [74649, 2], [[74650, 74751], 3], [[74752, 74850], 2], [[74851, 74862], 2], [74863, 3], [[74864, 74867], 2], [74868, 2], [[74869, 74879], 3], [[74880, 75075], 2], [[75076, 77711], 3], [[77712, 77808], 2], [[77809, 77810], 2], [[77811, 77823], 3], [[77824, 78894], 2], [78895, 2], [[78896, 78904], 3], [[78905, 78911], 3], [[78912, 78933], 2], [[78934, 82943], 3], [[82944, 83526], 2], [[83527, 92159], 3], [[92160, 92728], 2], [[92729, 92735], 3], [[92736, 92766], 2], [92767, 3], [[92768, 92777], 2], [[92778, 92781], 3], [[92782, 92783], 2], [[92784, 92862], 2], [92863, 3], [[92864, 92873], 2], [[92874, 92879], 3], [[92880, 92909], 2], [[92910, 92911], 3], [[92912, 92916], 2], [92917, 2], [[92918, 92927], 3], [[92928, 92982], 2], [[92983, 92991], 2], [[92992, 92995], 2], [[92996, 92997], 2], [[92998, 93007], 3], [[93008, 93017], 2], [93018, 3], [[93019, 93025], 2], [93026, 3], [[93027, 93047], 2], [[93048, 93052], 3], [[93053, 93071], 2], [[93072, 93759], 3], [93760, 1, "\u{16E60}"], [93761, 1, "\u{16E61}"], [93762, 1, "\u{16E62}"], [93763, 1, "\u{16E63}"], [93764, 1, "\u{16E64}"], [93765, 1, "\u{16E65}"], [93766, 1, "\u{16E66}"], [93767, 1, "\u{16E67}"], [93768, 1, "\u{16E68}"], [93769, 1, "\u{16E69}"], [93770, 1, "\u{16E6A}"], [93771, 1, "\u{16E6B}"], [93772, 1, "\u{16E6C}"], [93773, 1, "\u{16E6D}"], [93774, 1, "\u{16E6E}"], [93775, 1, "\u{16E6F}"], [93776, 1, "\u{16E70}"], [93777, 1, "\u{16E71}"], [93778, 1, "\u{16E72}"], [93779, 1, "\u{16E73}"], [93780, 1, "\u{16E74}"], [93781, 1, "\u{16E75}"], [93782, 1, "\u{16E76}"], [93783, 1, "\u{16E77}"], [93784, 1, "\u{16E78}"], [93785, 1, "\u{16E79}"], [93786, 1, "\u{16E7A}"], [93787, 1, "\u{16E7B}"], [93788, 1, "\u{16E7C}"], [93789, 1, "\u{16E7D}"], [93790, 1, "\u{16E7E}"], [93791, 1, "\u{16E7F}"], [[93792, 93823], 2], [[93824, 93850], 2], [[93851, 93951], 3], [[93952, 94020], 2], [[94021, 94026], 2], [[94027, 94030], 3], [94031, 2], [[94032, 94078], 2], [[94079, 94087], 2], [[94088, 94094], 3], [[94095, 94111], 2], [[94112, 94175], 3], [94176, 2], [94177, 2], [94178, 2], [94179, 2], [94180, 2], [[94181, 94191], 3], [[94192, 94193], 2], [[94194, 94207], 3], [[94208, 100332], 2], [[100333, 100337], 2], [[100338, 100343], 2], [[100344, 100351], 3], [[100352, 101106], 2], [[101107, 101589], 2], [[101590, 101631], 3], [[101632, 101640], 2], [[101641, 110575], 3], [[110576, 110579], 2], [110580, 3], [[110581, 110587], 2], [110588, 3], [[110589, 110590], 2], [110591, 3], [[110592, 110593], 2], [[110594, 110878], 2], [[110879, 110882], 2], [[110883, 110897], 3], [110898, 2], [[110899, 110927], 3], [[110928, 110930], 2], [[110931, 110932], 3], [110933, 2], [[110934, 110947], 3], [[110948, 110951], 2], [[110952, 110959], 3], [[110960, 111355], 2], [[111356, 113663], 3], [[113664, 113770], 2], [[113771, 113775], 3], [[113776, 113788], 2], [[113789, 113791], 3], [[113792, 113800], 2], [[113801, 113807], 3], [[113808, 113817], 2], [[113818, 113819], 3], [113820, 2], [[113821, 113822], 2], [113823, 2], [[113824, 113827], 7], [[113828, 118527], 3], [[118528, 118573], 2], [[118574, 118575], 3], [[118576, 118598], 2], [[118599, 118607], 3], [[118608, 118723], 2], [[118724, 118783], 3], [[118784, 119029], 2], [[119030, 119039], 3], [[119040, 119078], 2], [[119079, 119080], 3], [119081, 2], [[119082, 119133], 2], [119134, 1, "\u{1D157}\u{1D165}"], [119135, 1, "\u{1D158}\u{1D165}"], [119136, 1, "\u{1D158}\u{1D165}\u{1D16E}"], [119137, 1, "\u{1D158}\u{1D165}\u{1D16F}"], [119138, 1, "\u{1D158}\u{1D165}\u{1D170}"], [119139, 1, "\u{1D158}\u{1D165}\u{1D171}"], [119140, 1, "\u{1D158}\u{1D165}\u{1D172}"], [[119141, 119154], 2], [[119155, 119162], 3], [[119163, 119226], 2], [119227, 1, "\u{1D1B9}\u{1D165}"], [119228, 1, "\u{1D1BA}\u{1D165}"], [119229, 1, "\u{1D1B9}\u{1D165}\u{1D16E}"], [119230, 1, "\u{1D1BA}\u{1D165}\u{1D16E}"], [119231, 1, "\u{1D1B9}\u{1D165}\u{1D16F}"], [119232, 1, "\u{1D1BA}\u{1D165}\u{1D16F}"], [[119233, 119261], 2], [[119262, 119272], 2], [[119273, 119274], 2], [[119275, 119295], 3], [[119296, 119365], 2], [[119366, 119487], 3], [[119488, 119507], 2], [[119508, 119519], 3], [[119520, 119539], 2], [[119540, 119551], 3], [[119552, 119638], 2], [[119639, 119647], 3], [[119648, 119665], 2], [[119666, 119672], 2], [[119673, 119807], 3], [119808, 1, "a"], [119809, 1, "b"], [119810, 1, "c"], [119811, 1, "d"], [119812, 1, "e"], [119813, 1, "f"], [119814, 1, "g"], [119815, 1, "h"], [119816, 1, "i"], [119817, 1, "j"], [119818, 1, "k"], [119819, 1, "l"], [119820, 1, "m"], [119821, 1, "n"], [119822, 1, "o"], [119823, 1, "p"], [119824, 1, "q"], [119825, 1, "r"], [119826, 1, "s"], [119827, 1, "t"], [119828, 1, "u"], [119829, 1, "v"], [119830, 1, "w"], [119831, 1, "x"], [119832, 1, "y"], [119833, 1, "z"], [119834, 1, "a"], [119835, 1, "b"], [119836, 1, "c"], [119837, 1, "d"], [119838, 1, "e"], [119839, 1, "f"], [119840, 1, "g"], [119841, 1, "h"], [119842, 1, "i"], [119843, 1, "j"], [119844, 1, "k"], [119845, 1, "l"], [119846, 1, "m"], [119847, 1, "n"], [119848, 1, "o"], [119849, 1, "p"], [119850, 1, "q"], [119851, 1, "r"], [119852, 1, "s"], [119853, 1, "t"], [119854, 1, "u"], [119855, 1, "v"], [119856, 1, "w"], [119857, 1, "x"], [119858, 1, "y"], [119859, 1, "z"], [119860, 1, "a"], [119861, 1, "b"], [119862, 1, "c"], [119863, 1, "d"], [119864, 1, "e"], [119865, 1, "f"], [119866, 1, "g"], [119867, 1, "h"], [119868, 1, "i"], [119869, 1, "j"], [119870, 1, "k"], [119871, 1, "l"], [119872, 1, "m"], [119873, 1, "n"], [119874, 1, "o"], [119875, 1, "p"], [119876, 1, "q"], [119877, 1, "r"], [119878, 1, "s"], [119879, 1, "t"], [119880, 1, "u"], [119881, 1, "v"], [119882, 1, "w"], [119883, 1, "x"], [119884, 1, "y"], [119885, 1, "z"], [119886, 1, "a"], [119887, 1, "b"], [119888, 1, "c"], [119889, 1, "d"], [119890, 1, "e"], [119891, 1, "f"], [119892, 1, "g"], [119893, 3], [119894, 1, "i"], [119895, 1, "j"], [119896, 1, "k"], [119897, 1, "l"], [119898, 1, "m"], [119899, 1, "n"], [119900, 1, "o"], [119901, 1, "p"], [119902, 1, "q"], [119903, 1, "r"], [119904, 1, "s"], [119905, 1, "t"], [119906, 1, "u"], [119907, 1, "v"], [119908, 1, "w"], [119909, 1, "x"], [119910, 1, "y"], [119911, 1, "z"], [119912, 1, "a"], [119913, 1, "b"], [119914, 1, "c"], [119915, 1, "d"], [119916, 1, "e"], [119917, 1, "f"], [119918, 1, "g"], [119919, 1, "h"], [119920, 1, "i"], [119921, 1, "j"], [119922, 1, "k"], [119923, 1, "l"], [119924, 1, "m"], [119925, 1, "n"], [119926, 1, "o"], [119927, 1, "p"], [119928, 1, "q"], [119929, 1, "r"], [119930, 1, "s"], [119931, 1, "t"], [119932, 1, "u"], [119933, 1, "v"], [119934, 1, "w"], [119935, 1, "x"], [119936, 1, "y"], [119937, 1, "z"], [119938, 1, "a"], [119939, 1, "b"], [119940, 1, "c"], [119941, 1, "d"], [119942, 1, "e"], [119943, 1, "f"], [119944, 1, "g"], [119945, 1, "h"], [119946, 1, "i"], [119947, 1, "j"], [119948, 1, "k"], [119949, 1, "l"], [119950, 1, "m"], [119951, 1, "n"], [119952, 1, "o"], [119953, 1, "p"], [119954, 1, "q"], [119955, 1, "r"], [119956, 1, "s"], [119957, 1, "t"], [119958, 1, "u"], [119959, 1, "v"], [119960, 1, "w"], [119961, 1, "x"], [119962, 1, "y"], [119963, 1, "z"], [119964, 1, "a"], [119965, 3], [119966, 1, "c"], [119967, 1, "d"], [[119968, 119969], 3], [119970, 1, "g"], [[119971, 119972], 3], [119973, 1, "j"], [119974, 1, "k"], [[119975, 119976], 3], [119977, 1, "n"], [119978, 1, "o"], [119979, 1, "p"], [119980, 1, "q"], [119981, 3], [119982, 1, "s"], [119983, 1, "t"], [119984, 1, "u"], [119985, 1, "v"], [119986, 1, "w"], [119987, 1, "x"], [119988, 1, "y"], [119989, 1, "z"], [119990, 1, "a"], [119991, 1, "b"], [119992, 1, "c"], [119993, 1, "d"], [119994, 3], [119995, 1, "f"], [119996, 3], [119997, 1, "h"], [119998, 1, "i"], [119999, 1, "j"], [12e4, 1, "k"], [120001, 1, "l"], [120002, 1, "m"], [120003, 1, "n"], [120004, 3], [120005, 1, "p"], [120006, 1, "q"], [120007, 1, "r"], [120008, 1, "s"], [120009, 1, "t"], [120010, 1, "u"], [120011, 1, "v"], [120012, 1, "w"], [120013, 1, "x"], [120014, 1, "y"], [120015, 1, "z"], [120016, 1, "a"], [120017, 1, "b"], [120018, 1, "c"], [120019, 1, "d"], [120020, 1, "e"], [120021, 1, "f"], [120022, 1, "g"], [120023, 1, "h"], [120024, 1, "i"], [120025, 1, "j"], [120026, 1, "k"], [120027, 1, "l"], [120028, 1, "m"], [120029, 1, "n"], [120030, 1, "o"], [120031, 1, "p"], [120032, 1, "q"], [120033, 1, "r"], [120034, 1, "s"], [120035, 1, "t"], [120036, 1, "u"], [120037, 1, "v"], [120038, 1, "w"], [120039, 1, "x"], [120040, 1, "y"], [120041, 1, "z"], [120042, 1, "a"], [120043, 1, "b"], [120044, 1, "c"], [120045, 1, "d"], [120046, 1, "e"], [120047, 1, "f"], [120048, 1, "g"], [120049, 1, "h"], [120050, 1, "i"], [120051, 1, "j"], [120052, 1, "k"], [120053, 1, "l"], [120054, 1, "m"], [120055, 1, "n"], [120056, 1, "o"], [120057, 1, "p"], [120058, 1, "q"], [120059, 1, "r"], [120060, 1, "s"], [120061, 1, "t"], [120062, 1, "u"], [120063, 1, "v"], [120064, 1, "w"], [120065, 1, "x"], [120066, 1, "y"], [120067, 1, "z"], [120068, 1, "a"], [120069, 1, "b"], [120070, 3], [120071, 1, "d"], [120072, 1, "e"], [120073, 1, "f"], [120074, 1, "g"], [[120075, 120076], 3], [120077, 1, "j"], [120078, 1, "k"], [120079, 1, "l"], [120080, 1, "m"], [120081, 1, "n"], [120082, 1, "o"], [120083, 1, "p"], [120084, 1, "q"], [120085, 3], [120086, 1, "s"], [120087, 1, "t"], [120088, 1, "u"], [120089, 1, "v"], [120090, 1, "w"], [120091, 1, "x"], [120092, 1, "y"], [120093, 3], [120094, 1, "a"], [120095, 1, "b"], [120096, 1, "c"], [120097, 1, "d"], [120098, 1, "e"], [120099, 1, "f"], [120100, 1, "g"], [120101, 1, "h"], [120102, 1, "i"], [120103, 1, "j"], [120104, 1, "k"], [120105, 1, "l"], [120106, 1, "m"], [120107, 1, "n"], [120108, 1, "o"], [120109, 1, "p"], [120110, 1, "q"], [120111, 1, "r"], [120112, 1, "s"], [120113, 1, "t"], [120114, 1, "u"], [120115, 1, "v"], [120116, 1, "w"], [120117, 1, "x"], [120118, 1, "y"], [120119, 1, "z"], [120120, 1, "a"], [120121, 1, "b"], [120122, 3], [120123, 1, "d"], [120124, 1, "e"], [120125, 1, "f"], [120126, 1, "g"], [120127, 3], [120128, 1, "i"], [120129, 1, "j"], [120130, 1, "k"], [120131, 1, "l"], [120132, 1, "m"], [120133, 3], [120134, 1, "o"], [[120135, 120137], 3], [120138, 1, "s"], [120139, 1, "t"], [120140, 1, "u"], [120141, 1, "v"], [120142, 1, "w"], [120143, 1, "x"], [120144, 1, "y"], [120145, 3], [120146, 1, "a"], [120147, 1, "b"], [120148, 1, "c"], [120149, 1, "d"], [120150, 1, "e"], [120151, 1, "f"], [120152, 1, "g"], [120153, 1, "h"], [120154, 1, "i"], [120155, 1, "j"], [120156, 1, "k"], [120157, 1, "l"], [120158, 1, "m"], [120159, 1, "n"], [120160, 1, "o"], [120161, 1, "p"], [120162, 1, "q"], [120163, 1, "r"], [120164, 1, "s"], [120165, 1, "t"], [120166, 1, "u"], [120167, 1, "v"], [120168, 1, "w"], [120169, 1, "x"], [120170, 1, "y"], [120171, 1, "z"], [120172, 1, "a"], [120173, 1, "b"], [120174, 1, "c"], [120175, 1, "d"], [120176, 1, "e"], [120177, 1, "f"], [120178, 1, "g"], [120179, 1, "h"], [120180, 1, "i"], [120181, 1, "j"], [120182, 1, "k"], [120183, 1, "l"], [120184, 1, "m"], [120185, 1, "n"], [120186, 1, "o"], [120187, 1, "p"], [120188, 1, "q"], [120189, 1, "r"], [120190, 1, "s"], [120191, 1, "t"], [120192, 1, "u"], [120193, 1, "v"], [120194, 1, "w"], [120195, 1, "x"], [120196, 1, "y"], [120197, 1, "z"], [120198, 1, "a"], [120199, 1, "b"], [120200, 1, "c"], [120201, 1, "d"], [120202, 1, "e"], [120203, 1, "f"], [120204, 1, "g"], [120205, 1, "h"], [120206, 1, "i"], [120207, 1, "j"], [120208, 1, "k"], [120209, 1, "l"], [120210, 1, "m"], [120211, 1, "n"], [120212, 1, "o"], [120213, 1, "p"], [120214, 1, "q"], [120215, 1, "r"], [120216, 1, "s"], [120217, 1, "t"], [120218, 1, "u"], [120219, 1, "v"], [120220, 1, "w"], [120221, 1, "x"], [120222, 1, "y"], [120223, 1, "z"], [120224, 1, "a"], [120225, 1, "b"], [120226, 1, "c"], [120227, 1, "d"], [120228, 1, "e"], [120229, 1, "f"], [120230, 1, "g"], [120231, 1, "h"], [120232, 1, "i"], [120233, 1, "j"], [120234, 1, "k"], [120235, 1, "l"], [120236, 1, "m"], [120237, 1, "n"], [120238, 1, "o"], [120239, 1, "p"], [120240, 1, "q"], [120241, 1, "r"], [120242, 1, "s"], [120243, 1, "t"], [120244, 1, "u"], [120245, 1, "v"], [120246, 1, "w"], [120247, 1, "x"], [120248, 1, "y"], [120249, 1, "z"], [120250, 1, "a"], [120251, 1, "b"], [120252, 1, "c"], [120253, 1, "d"], [120254, 1, "e"], [120255, 1, "f"], [120256, 1, "g"], [120257, 1, "h"], [120258, 1, "i"], [120259, 1, "j"], [120260, 1, "k"], [120261, 1, "l"], [120262, 1, "m"], [120263, 1, "n"], [120264, 1, "o"], [120265, 1, "p"], [120266, 1, "q"], [120267, 1, "r"], [120268, 1, "s"], [120269, 1, "t"], [120270, 1, "u"], [120271, 1, "v"], [120272, 1, "w"], [120273, 1, "x"], [120274, 1, "y"], [120275, 1, "z"], [120276, 1, "a"], [120277, 1, "b"], [120278, 1, "c"], [120279, 1, "d"], [120280, 1, "e"], [120281, 1, "f"], [120282, 1, "g"], [120283, 1, "h"], [120284, 1, "i"], [120285, 1, "j"], [120286, 1, "k"], [120287, 1, "l"], [120288, 1, "m"], [120289, 1, "n"], [120290, 1, "o"], [120291, 1, "p"], [120292, 1, "q"], [120293, 1, "r"], [120294, 1, "s"], [120295, 1, "t"], [120296, 1, "u"], [120297, 1, "v"], [120298, 1, "w"], [120299, 1, "x"], [120300, 1, "y"], [120301, 1, "z"], [120302, 1, "a"], [120303, 1, "b"], [120304, 1, "c"], [120305, 1, "d"], [120306, 1, "e"], [120307, 1, "f"], [120308, 1, "g"], [120309, 1, "h"], [120310, 1, "i"], [120311, 1, "j"], [120312, 1, "k"], [120313, 1, "l"], [120314, 1, "m"], [120315, 1, "n"], [120316, 1, "o"], [120317, 1, "p"], [120318, 1, "q"], [120319, 1, "r"], [120320, 1, "s"], [120321, 1, "t"], [120322, 1, "u"], [120323, 1, "v"], [120324, 1, "w"], [120325, 1, "x"], [120326, 1, "y"], [120327, 1, "z"], [120328, 1, "a"], [120329, 1, "b"], [120330, 1, "c"], [120331, 1, "d"], [120332, 1, "e"], [120333, 1, "f"], [120334, 1, "g"], [120335, 1, "h"], [120336, 1, "i"], [120337, 1, "j"], [120338, 1, "k"], [120339, 1, "l"], [120340, 1, "m"], [120341, 1, "n"], [120342, 1, "o"], [120343, 1, "p"], [120344, 1, "q"], [120345, 1, "r"], [120346, 1, "s"], [120347, 1, "t"], [120348, 1, "u"], [120349, 1, "v"], [120350, 1, "w"], [120351, 1, "x"], [120352, 1, "y"], [120353, 1, "z"], [120354, 1, "a"], [120355, 1, "b"], [120356, 1, "c"], [120357, 1, "d"], [120358, 1, "e"], [120359, 1, "f"], [120360, 1, "g"], [120361, 1, "h"], [120362, 1, "i"], [120363, 1, "j"], [120364, 1, "k"], [120365, 1, "l"], [120366, 1, "m"], [120367, 1, "n"], [120368, 1, "o"], [120369, 1, "p"], [120370, 1, "q"], [120371, 1, "r"], [120372, 1, "s"], [120373, 1, "t"], [120374, 1, "u"], [120375, 1, "v"], [120376, 1, "w"], [120377, 1, "x"], [120378, 1, "y"], [120379, 1, "z"], [120380, 1, "a"], [120381, 1, "b"], [120382, 1, "c"], [120383, 1, "d"], [120384, 1, "e"], [120385, 1, "f"], [120386, 1, "g"], [120387, 1, "h"], [120388, 1, "i"], [120389, 1, "j"], [120390, 1, "k"], [120391, 1, "l"], [120392, 1, "m"], [120393, 1, "n"], [120394, 1, "o"], [120395, 1, "p"], [120396, 1, "q"], [120397, 1, "r"], [120398, 1, "s"], [120399, 1, "t"], [120400, 1, "u"], [120401, 1, "v"], [120402, 1, "w"], [120403, 1, "x"], [120404, 1, "y"], [120405, 1, "z"], [120406, 1, "a"], [120407, 1, "b"], [120408, 1, "c"], [120409, 1, "d"], [120410, 1, "e"], [120411, 1, "f"], [120412, 1, "g"], [120413, 1, "h"], [120414, 1, "i"], [120415, 1, "j"], [120416, 1, "k"], [120417, 1, "l"], [120418, 1, "m"], [120419, 1, "n"], [120420, 1, "o"], [120421, 1, "p"], [120422, 1, "q"], [120423, 1, "r"], [120424, 1, "s"], [120425, 1, "t"], [120426, 1, "u"], [120427, 1, "v"], [120428, 1, "w"], [120429, 1, "x"], [120430, 1, "y"], [120431, 1, "z"], [120432, 1, "a"], [120433, 1, "b"], [120434, 1, "c"], [120435, 1, "d"], [120436, 1, "e"], [120437, 1, "f"], [120438, 1, "g"], [120439, 1, "h"], [120440, 1, "i"], [120441, 1, "j"], [120442, 1, "k"], [120443, 1, "l"], [120444, 1, "m"], [120445, 1, "n"], [120446, 1, "o"], [120447, 1, "p"], [120448, 1, "q"], [120449, 1, "r"], [120450, 1, "s"], [120451, 1, "t"], [120452, 1, "u"], [120453, 1, "v"], [120454, 1, "w"], [120455, 1, "x"], [120456, 1, "y"], [120457, 1, "z"], [120458, 1, "a"], [120459, 1, "b"], [120460, 1, "c"], [120461, 1, "d"], [120462, 1, "e"], [120463, 1, "f"], [120464, 1, "g"], [120465, 1, "h"], [120466, 1, "i"], [120467, 1, "j"], [120468, 1, "k"], [120469, 1, "l"], [120470, 1, "m"], [120471, 1, "n"], [120472, 1, "o"], [120473, 1, "p"], [120474, 1, "q"], [120475, 1, "r"], [120476, 1, "s"], [120477, 1, "t"], [120478, 1, "u"], [120479, 1, "v"], [120480, 1, "w"], [120481, 1, "x"], [120482, 1, "y"], [120483, 1, "z"], [120484, 1, "\u0131"], [120485, 1, "\u0237"], [[120486, 120487], 3], [120488, 1, "\u03B1"], [120489, 1, "\u03B2"], [120490, 1, "\u03B3"], [120491, 1, "\u03B4"], [120492, 1, "\u03B5"], [120493, 1, "\u03B6"], [120494, 1, "\u03B7"], [120495, 1, "\u03B8"], [120496, 1, "\u03B9"], [120497, 1, "\u03BA"], [120498, 1, "\u03BB"], [120499, 1, "\u03BC"], [120500, 1, "\u03BD"], [120501, 1, "\u03BE"], [120502, 1, "\u03BF"], [120503, 1, "\u03C0"], [120504, 1, "\u03C1"], [120505, 1, "\u03B8"], [120506, 1, "\u03C3"], [120507, 1, "\u03C4"], [120508, 1, "\u03C5"], [120509, 1, "\u03C6"], [120510, 1, "\u03C7"], [120511, 1, "\u03C8"], [120512, 1, "\u03C9"], [120513, 1, "\u2207"], [120514, 1, "\u03B1"], [120515, 1, "\u03B2"], [120516, 1, "\u03B3"], [120517, 1, "\u03B4"], [120518, 1, "\u03B5"], [120519, 1, "\u03B6"], [120520, 1, "\u03B7"], [120521, 1, "\u03B8"], [120522, 1, "\u03B9"], [120523, 1, "\u03BA"], [120524, 1, "\u03BB"], [120525, 1, "\u03BC"], [120526, 1, "\u03BD"], [120527, 1, "\u03BE"], [120528, 1, "\u03BF"], [120529, 1, "\u03C0"], [120530, 1, "\u03C1"], [[120531, 120532], 1, "\u03C3"], [120533, 1, "\u03C4"], [120534, 1, "\u03C5"], [120535, 1, "\u03C6"], [120536, 1, "\u03C7"], [120537, 1, "\u03C8"], [120538, 1, "\u03C9"], [120539, 1, "\u2202"], [120540, 1, "\u03B5"], [120541, 1, "\u03B8"], [120542, 1, "\u03BA"], [120543, 1, "\u03C6"], [120544, 1, "\u03C1"], [120545, 1, "\u03C0"], [120546, 1, "\u03B1"], [120547, 1, "\u03B2"], [120548, 1, "\u03B3"], [120549, 1, "\u03B4"], [120550, 1, "\u03B5"], [120551, 1, "\u03B6"], [120552, 1, "\u03B7"], [120553, 1, "\u03B8"], [120554, 1, "\u03B9"], [120555, 1, "\u03BA"], [120556, 1, "\u03BB"], [120557, 1, "\u03BC"], [120558, 1, "\u03BD"], [120559, 1, "\u03BE"], [120560, 1, "\u03BF"], [120561, 1, "\u03C0"], [120562, 1, "\u03C1"], [120563, 1, "\u03B8"], [120564, 1, "\u03C3"], [120565, 1, "\u03C4"], [120566, 1, "\u03C5"], [120567, 1, "\u03C6"], [120568, 1, "\u03C7"], [120569, 1, "\u03C8"], [120570, 1, "\u03C9"], [120571, 1, "\u2207"], [120572, 1, "\u03B1"], [120573, 1, "\u03B2"], [120574, 1, "\u03B3"], [120575, 1, "\u03B4"], [120576, 1, "\u03B5"], [120577, 1, "\u03B6"], [120578, 1, "\u03B7"], [120579, 1, "\u03B8"], [120580, 1, "\u03B9"], [120581, 1, "\u03BA"], [120582, 1, "\u03BB"], [120583, 1, "\u03BC"], [120584, 1, "\u03BD"], [120585, 1, "\u03BE"], [120586, 1, "\u03BF"], [120587, 1, "\u03C0"], [120588, 1, "\u03C1"], [[120589, 120590], 1, "\u03C3"], [120591, 1, "\u03C4"], [120592, 1, "\u03C5"], [120593, 1, "\u03C6"], [120594, 1, "\u03C7"], [120595, 1, "\u03C8"], [120596, 1, "\u03C9"], [120597, 1, "\u2202"], [120598, 1, "\u03B5"], [120599, 1, "\u03B8"], [120600, 1, "\u03BA"], [120601, 1, "\u03C6"], [120602, 1, "\u03C1"], [120603, 1, "\u03C0"], [120604, 1, "\u03B1"], [120605, 1, "\u03B2"], [120606, 1, "\u03B3"], [120607, 1, "\u03B4"], [120608, 1, "\u03B5"], [120609, 1, "\u03B6"], [120610, 1, "\u03B7"], [120611, 1, "\u03B8"], [120612, 1, "\u03B9"], [120613, 1, "\u03BA"], [120614, 1, "\u03BB"], [120615, 1, "\u03BC"], [120616, 1, "\u03BD"], [120617, 1, "\u03BE"], [120618, 1, "\u03BF"], [120619, 1, "\u03C0"], [120620, 1, "\u03C1"], [120621, 1, "\u03B8"], [120622, 1, "\u03C3"], [120623, 1, "\u03C4"], [120624, 1, "\u03C5"], [120625, 1, "\u03C6"], [120626, 1, "\u03C7"], [120627, 1, "\u03C8"], [120628, 1, "\u03C9"], [120629, 1, "\u2207"], [120630, 1, "\u03B1"], [120631, 1, "\u03B2"], [120632, 1, "\u03B3"], [120633, 1, "\u03B4"], [120634, 1, "\u03B5"], [120635, 1, "\u03B6"], [120636, 1, "\u03B7"], [120637, 1, "\u03B8"], [120638, 1, "\u03B9"], [120639, 1, "\u03BA"], [120640, 1, "\u03BB"], [120641, 1, "\u03BC"], [120642, 1, "\u03BD"], [120643, 1, "\u03BE"], [120644, 1, "\u03BF"], [120645, 1, "\u03C0"], [120646, 1, "\u03C1"], [[120647, 120648], 1, "\u03C3"], [120649, 1, "\u03C4"], [120650, 1, "\u03C5"], [120651, 1, "\u03C6"], [120652, 1, "\u03C7"], [120653, 1, "\u03C8"], [120654, 1, "\u03C9"], [120655, 1, "\u2202"], [120656, 1, "\u03B5"], [120657, 1, "\u03B8"], [120658, 1, "\u03BA"], [120659, 1, "\u03C6"], [120660, 1, "\u03C1"], [120661, 1, "\u03C0"], [120662, 1, "\u03B1"], [120663, 1, "\u03B2"], [120664, 1, "\u03B3"], [120665, 1, "\u03B4"], [120666, 1, "\u03B5"], [120667, 1, "\u03B6"], [120668, 1, "\u03B7"], [120669, 1, "\u03B8"], [120670, 1, "\u03B9"], [120671, 1, "\u03BA"], [120672, 1, "\u03BB"], [120673, 1, "\u03BC"], [120674, 1, "\u03BD"], [120675, 1, "\u03BE"], [120676, 1, "\u03BF"], [120677, 1, "\u03C0"], [120678, 1, "\u03C1"], [120679, 1, "\u03B8"], [120680, 1, "\u03C3"], [120681, 1, "\u03C4"], [120682, 1, "\u03C5"], [120683, 1, "\u03C6"], [120684, 1, "\u03C7"], [120685, 1, "\u03C8"], [120686, 1, "\u03C9"], [120687, 1, "\u2207"], [120688, 1, "\u03B1"], [120689, 1, "\u03B2"], [120690, 1, "\u03B3"], [120691, 1, "\u03B4"], [120692, 1, "\u03B5"], [120693, 1, "\u03B6"], [120694, 1, "\u03B7"], [120695, 1, "\u03B8"], [120696, 1, "\u03B9"], [120697, 1, "\u03BA"], [120698, 1, "\u03BB"], [120699, 1, "\u03BC"], [120700, 1, "\u03BD"], [120701, 1, "\u03BE"], [120702, 1, "\u03BF"], [120703, 1, "\u03C0"], [120704, 1, "\u03C1"], [[120705, 120706], 1, "\u03C3"], [120707, 1, "\u03C4"], [120708, 1, "\u03C5"], [120709, 1, "\u03C6"], [120710, 1, "\u03C7"], [120711, 1, "\u03C8"], [120712, 1, "\u03C9"], [120713, 1, "\u2202"], [120714, 1, "\u03B5"], [120715, 1, "\u03B8"], [120716, 1, "\u03BA"], [120717, 1, "\u03C6"], [120718, 1, "\u03C1"], [120719, 1, "\u03C0"], [120720, 1, "\u03B1"], [120721, 1, "\u03B2"], [120722, 1, "\u03B3"], [120723, 1, "\u03B4"], [120724, 1, "\u03B5"], [120725, 1, "\u03B6"], [120726, 1, "\u03B7"], [120727, 1, "\u03B8"], [120728, 1, "\u03B9"], [120729, 1, "\u03BA"], [120730, 1, "\u03BB"], [120731, 1, "\u03BC"], [120732, 1, "\u03BD"], [120733, 1, "\u03BE"], [120734, 1, "\u03BF"], [120735, 1, "\u03C0"], [120736, 1, "\u03C1"], [120737, 1, "\u03B8"], [120738, 1, "\u03C3"], [120739, 1, "\u03C4"], [120740, 1, "\u03C5"], [120741, 1, "\u03C6"], [120742, 1, "\u03C7"], [120743, 1, "\u03C8"], [120744, 1, "\u03C9"], [120745, 1, "\u2207"], [120746, 1, "\u03B1"], [120747, 1, "\u03B2"], [120748, 1, "\u03B3"], [120749, 1, "\u03B4"], [120750, 1, "\u03B5"], [120751, 1, "\u03B6"], [120752, 1, "\u03B7"], [120753, 1, "\u03B8"], [120754, 1, "\u03B9"], [120755, 1, "\u03BA"], [120756, 1, "\u03BB"], [120757, 1, "\u03BC"], [120758, 1, "\u03BD"], [120759, 1, "\u03BE"], [120760, 1, "\u03BF"], [120761, 1, "\u03C0"], [120762, 1, "\u03C1"], [[120763, 120764], 1, "\u03C3"], [120765, 1, "\u03C4"], [120766, 1, "\u03C5"], [120767, 1, "\u03C6"], [120768, 1, "\u03C7"], [120769, 1, "\u03C8"], [120770, 1, "\u03C9"], [120771, 1, "\u2202"], [120772, 1, "\u03B5"], [120773, 1, "\u03B8"], [120774, 1, "\u03BA"], [120775, 1, "\u03C6"], [120776, 1, "\u03C1"], [120777, 1, "\u03C0"], [[120778, 120779], 1, "\u03DD"], [[120780, 120781], 3], [120782, 1, "0"], [120783, 1, "1"], [120784, 1, "2"], [120785, 1, "3"], [120786, 1, "4"], [120787, 1, "5"], [120788, 1, "6"], [120789, 1, "7"], [120790, 1, "8"], [120791, 1, "9"], [120792, 1, "0"], [120793, 1, "1"], [120794, 1, "2"], [120795, 1, "3"], [120796, 1, "4"], [120797, 1, "5"], [120798, 1, "6"], [120799, 1, "7"], [120800, 1, "8"], [120801, 1, "9"], [120802, 1, "0"], [120803, 1, "1"], [120804, 1, "2"], [120805, 1, "3"], [120806, 1, "4"], [120807, 1, "5"], [120808, 1, "6"], [120809, 1, "7"], [120810, 1, "8"], [120811, 1, "9"], [120812, 1, "0"], [120813, 1, "1"], [120814, 1, "2"], [120815, 1, "3"], [120816, 1, "4"], [120817, 1, "5"], [120818, 1, "6"], [120819, 1, "7"], [120820, 1, "8"], [120821, 1, "9"], [120822, 1, "0"], [120823, 1, "1"], [120824, 1, "2"], [120825, 1, "3"], [120826, 1, "4"], [120827, 1, "5"], [120828, 1, "6"], [120829, 1, "7"], [120830, 1, "8"], [120831, 1, "9"], [[120832, 121343], 2], [[121344, 121398], 2], [[121399, 121402], 2], [[121403, 121452], 2], [[121453, 121460], 2], [121461, 2], [[121462, 121475], 2], [121476, 2], [[121477, 121483], 2], [[121484, 121498], 3], [[121499, 121503], 2], [121504, 3], [[121505, 121519], 2], [[121520, 122623], 3], [[122624, 122654], 2], [[122655, 122660], 3], [[122661, 122666], 2], [[122667, 122879], 3], [[122880, 122886], 2], [122887, 3], [[122888, 122904], 2], [[122905, 122906], 3], [[122907, 122913], 2], [122914, 3], [[122915, 122916], 2], [122917, 3], [[122918, 122922], 2], [[122923, 122927], 3], [122928, 1, "\u0430"], [122929, 1, "\u0431"], [122930, 1, "\u0432"], [122931, 1, "\u0433"], [122932, 1, "\u0434"], [122933, 1, "\u0435"], [122934, 1, "\u0436"], [122935, 1, "\u0437"], [122936, 1, "\u0438"], [122937, 1, "\u043A"], [122938, 1, "\u043B"], [122939, 1, "\u043C"], [122940, 1, "\u043E"], [122941, 1, "\u043F"], [122942, 1, "\u0440"], [122943, 1, "\u0441"], [122944, 1, "\u0442"], [122945, 1, "\u0443"], [122946, 1, "\u0444"], [122947, 1, "\u0445"], [122948, 1, "\u0446"], [122949, 1, "\u0447"], [122950, 1, "\u0448"], [122951, 1, "\u044B"], [122952, 1, "\u044D"], [122953, 1, "\u044E"], [122954, 1, "\uA689"], [122955, 1, "\u04D9"], [122956, 1, "\u0456"], [122957, 1, "\u0458"], [122958, 1, "\u04E9"], [122959, 1, "\u04AF"], [122960, 1, "\u04CF"], [122961, 1, "\u0430"], [122962, 1, "\u0431"], [122963, 1, "\u0432"], [122964, 1, "\u0433"], [122965, 1, "\u0434"], [122966, 1, "\u0435"], [122967, 1, "\u0436"], [122968, 1, "\u0437"], [122969, 1, "\u0438"], [122970, 1, "\u043A"], [122971, 1, "\u043B"], [122972, 1, "\u043E"], [122973, 1, "\u043F"], [122974, 1, "\u0441"], [122975, 1, "\u0443"], [122976, 1, "\u0444"], [122977, 1, "\u0445"], [122978, 1, "\u0446"], [122979, 1, "\u0447"], [122980, 1, "\u0448"], [122981, 1, "\u044A"], [122982, 1, "\u044B"], [122983, 1, "\u0491"], [122984, 1, "\u0456"], [122985, 1, "\u0455"], [122986, 1, "\u045F"], [122987, 1, "\u04AB"], [122988, 1, "\uA651"], [122989, 1, "\u04B1"], [[122990, 123022], 3], [123023, 2], [[123024, 123135], 3], [[123136, 123180], 2], [[123181, 123183], 3], [[123184, 123197], 2], [[123198, 123199], 3], [[123200, 123209], 2], [[123210, 123213], 3], [123214, 2], [123215, 2], [[123216, 123535], 3], [[123536, 123566], 2], [[123567, 123583], 3], [[123584, 123641], 2], [[123642, 123646], 3], [123647, 2], [[123648, 124111], 3], [[124112, 124153], 2], [[124154, 124895], 3], [[124896, 124902], 2], [124903, 3], [[124904, 124907], 2], [124908, 3], [[124909, 124910], 2], [124911, 3], [[124912, 124926], 2], [124927, 3], [[124928, 125124], 2], [[125125, 125126], 3], [[125127, 125135], 2], [[125136, 125142], 2], [[125143, 125183], 3], [125184, 1, "\u{1E922}"], [125185, 1, "\u{1E923}"], [125186, 1, "\u{1E924}"], [125187, 1, "\u{1E925}"], [125188, 1, "\u{1E926}"], [125189, 1, "\u{1E927}"], [125190, 1, "\u{1E928}"], [125191, 1, "\u{1E929}"], [125192, 1, "\u{1E92A}"], [125193, 1, "\u{1E92B}"], [125194, 1, "\u{1E92C}"], [125195, 1, "\u{1E92D}"], [125196, 1, "\u{1E92E}"], [125197, 1, "\u{1E92F}"], [125198, 1, "\u{1E930}"], [125199, 1, "\u{1E931}"], [125200, 1, "\u{1E932}"], [125201, 1, "\u{1E933}"], [125202, 1, "\u{1E934}"], [125203, 1, "\u{1E935}"], [125204, 1, "\u{1E936}"], [125205, 1, "\u{1E937}"], [125206, 1, "\u{1E938}"], [125207, 1, "\u{1E939}"], [125208, 1, "\u{1E93A}"], [125209, 1, "\u{1E93B}"], [125210, 1, "\u{1E93C}"], [125211, 1, "\u{1E93D}"], [125212, 1, "\u{1E93E}"], [125213, 1, "\u{1E93F}"], [125214, 1, "\u{1E940}"], [125215, 1, "\u{1E941}"], [125216, 1, "\u{1E942}"], [125217, 1, "\u{1E943}"], [[125218, 125258], 2], [125259, 2], [[125260, 125263], 3], [[125264, 125273], 2], [[125274, 125277], 3], [[125278, 125279], 2], [[125280, 126064], 3], [[126065, 126132], 2], [[126133, 126208], 3], [[126209, 126269], 2], [[126270, 126463], 3], [126464, 1, "\u0627"], [126465, 1, "\u0628"], [126466, 1, "\u062C"], [126467, 1, "\u062F"], [126468, 3], [126469, 1, "\u0648"], [126470, 1, "\u0632"], [126471, 1, "\u062D"], [126472, 1, "\u0637"], [126473, 1, "\u064A"], [126474, 1, "\u0643"], [126475, 1, "\u0644"], [126476, 1, "\u0645"], [126477, 1, "\u0646"], [126478, 1, "\u0633"], [126479, 1, "\u0639"], [126480, 1, "\u0641"], [126481, 1, "\u0635"], [126482, 1, "\u0642"], [126483, 1, "\u0631"], [126484, 1, "\u0634"], [126485, 1, "\u062A"], [126486, 1, "\u062B"], [126487, 1, "\u062E"], [126488, 1, "\u0630"], [126489, 1, "\u0636"], [126490, 1, "\u0638"], [126491, 1, "\u063A"], [126492, 1, "\u066E"], [126493, 1, "\u06BA"], [126494, 1, "\u06A1"], [126495, 1, "\u066F"], [126496, 3], [126497, 1, "\u0628"], [126498, 1, "\u062C"], [126499, 3], [126500, 1, "\u0647"], [[126501, 126502], 3], [126503, 1, "\u062D"], [126504, 3], [126505, 1, "\u064A"], [126506, 1, "\u0643"], [126507, 1, "\u0644"], [126508, 1, "\u0645"], [126509, 1, "\u0646"], [126510, 1, "\u0633"], [126511, 1, "\u0639"], [126512, 1, "\u0641"], [126513, 1, "\u0635"], [126514, 1, "\u0642"], [126515, 3], [126516, 1, "\u0634"], [126517, 1, "\u062A"], [126518, 1, "\u062B"], [126519, 1, "\u062E"], [126520, 3], [126521, 1, "\u0636"], [126522, 3], [126523, 1, "\u063A"], [[126524, 126529], 3], [126530, 1, "\u062C"], [[126531, 126534], 3], [126535, 1, "\u062D"], [126536, 3], [126537, 1, "\u064A"], [126538, 3], [126539, 1, "\u0644"], [126540, 3], [126541, 1, "\u0646"], [126542, 1, "\u0633"], [126543, 1, "\u0639"], [126544, 3], [126545, 1, "\u0635"], [126546, 1, "\u0642"], [126547, 3], [126548, 1, "\u0634"], [[126549, 126550], 3], [126551, 1, "\u062E"], [126552, 3], [126553, 1, "\u0636"], [126554, 3], [126555, 1, "\u063A"], [126556, 3], [126557, 1, "\u06BA"], [126558, 3], [126559, 1, "\u066F"], [126560, 3], [126561, 1, "\u0628"], [126562, 1, "\u062C"], [126563, 3], [126564, 1, "\u0647"], [[126565, 126566], 3], [126567, 1, "\u062D"], [126568, 1, "\u0637"], [126569, 1, "\u064A"], [126570, 1, "\u0643"], [126571, 3], [126572, 1, "\u0645"], [126573, 1, "\u0646"], [126574, 1, "\u0633"], [126575, 1, "\u0639"], [126576, 1, "\u0641"], [126577, 1, "\u0635"], [126578, 1, "\u0642"], [126579, 3], [126580, 1, "\u0634"], [126581, 1, "\u062A"], [126582, 1, "\u062B"], [126583, 1, "\u062E"], [126584, 3], [126585, 1, "\u0636"], [126586, 1, "\u0638"], [126587, 1, "\u063A"], [126588, 1, "\u066E"], [126589, 3], [126590, 1, "\u06A1"], [126591, 3], [126592, 1, "\u0627"], [126593, 1, "\u0628"], [126594, 1, "\u062C"], [126595, 1, "\u062F"], [126596, 1, "\u0647"], [126597, 1, "\u0648"], [126598, 1, "\u0632"], [126599, 1, "\u062D"], [126600, 1, "\u0637"], [126601, 1, "\u064A"], [126602, 3], [126603, 1, "\u0644"], [126604, 1, "\u0645"], [126605, 1, "\u0646"], [126606, 1, "\u0633"], [126607, 1, "\u0639"], [126608, 1, "\u0641"], [126609, 1, "\u0635"], [126610, 1, "\u0642"], [126611, 1, "\u0631"], [126612, 1, "\u0634"], [126613, 1, "\u062A"], [126614, 1, "\u062B"], [126615, 1, "\u062E"], [126616, 1, "\u0630"], [126617, 1, "\u0636"], [126618, 1, "\u0638"], [126619, 1, "\u063A"], [[126620, 126624], 3], [126625, 1, "\u0628"], [126626, 1, "\u062C"], [126627, 1, "\u062F"], [126628, 3], [126629, 1, "\u0648"], [126630, 1, "\u0632"], [126631, 1, "\u062D"], [126632, 1, "\u0637"], [126633, 1, "\u064A"], [126634, 3], [126635, 1, "\u0644"], [126636, 1, "\u0645"], [126637, 1, "\u0646"], [126638, 1, "\u0633"], [126639, 1, "\u0639"], [126640, 1, "\u0641"], [126641, 1, "\u0635"], [126642, 1, "\u0642"], [126643, 1, "\u0631"], [126644, 1, "\u0634"], [126645, 1, "\u062A"], [126646, 1, "\u062B"], [126647, 1, "\u062E"], [126648, 1, "\u0630"], [126649, 1, "\u0636"], [126650, 1, "\u0638"], [126651, 1, "\u063A"], [[126652, 126703], 3], [[126704, 126705], 2], [[126706, 126975], 3], [[126976, 127019], 2], [[127020, 127023], 3], [[127024, 127123], 2], [[127124, 127135], 3], [[127136, 127150], 2], [[127151, 127152], 3], [[127153, 127166], 2], [127167, 2], [127168, 3], [[127169, 127183], 2], [127184, 3], [[127185, 127199], 2], [[127200, 127221], 2], [[127222, 127231], 3], [127232, 3], [127233, 5, "0,"], [127234, 5, "1,"], [127235, 5, "2,"], [127236, 5, "3,"], [127237, 5, "4,"], [127238, 5, "5,"], [127239, 5, "6,"], [127240, 5, "7,"], [127241, 5, "8,"], [127242, 5, "9,"], [[127243, 127244], 2], [[127245, 127247], 2], [127248, 5, "(a)"], [127249, 5, "(b)"], [127250, 5, "(c)"], [127251, 5, "(d)"], [127252, 5, "(e)"], [127253, 5, "(f)"], [127254, 5, "(g)"], [127255, 5, "(h)"], [127256, 5, "(i)"], [127257, 5, "(j)"], [127258, 5, "(k)"], [127259, 5, "(l)"], [127260, 5, "(m)"], [127261, 5, "(n)"], [127262, 5, "(o)"], [127263, 5, "(p)"], [127264, 5, "(q)"], [127265, 5, "(r)"], [127266, 5, "(s)"], [127267, 5, "(t)"], [127268, 5, "(u)"], [127269, 5, "(v)"], [127270, 5, "(w)"], [127271, 5, "(x)"], [127272, 5, "(y)"], [127273, 5, "(z)"], [127274, 1, "\u3014s\u3015"], [127275, 1, "c"], [127276, 1, "r"], [127277, 1, "cd"], [127278, 1, "wz"], [127279, 2], [127280, 1, "a"], [127281, 1, "b"], [127282, 1, "c"], [127283, 1, "d"], [127284, 1, "e"], [127285, 1, "f"], [127286, 1, "g"], [127287, 1, "h"], [127288, 1, "i"], [127289, 1, "j"], [127290, 1, "k"], [127291, 1, "l"], [127292, 1, "m"], [127293, 1, "n"], [127294, 1, "o"], [127295, 1, "p"], [127296, 1, "q"], [127297, 1, "r"], [127298, 1, "s"], [127299, 1, "t"], [127300, 1, "u"], [127301, 1, "v"], [127302, 1, "w"], [127303, 1, "x"], [127304, 1, "y"], [127305, 1, "z"], [127306, 1, "hv"], [127307, 1, "mv"], [127308, 1, "sd"], [127309, 1, "ss"], [127310, 1, "ppv"], [127311, 1, "wc"], [[127312, 127318], 2], [127319, 2], [[127320, 127326], 2], [127327, 2], [[127328, 127337], 2], [127338, 1, "mc"], [127339, 1, "md"], [127340, 1, "mr"], [[127341, 127343], 2], [[127344, 127352], 2], [127353, 2], [127354, 2], [[127355, 127356], 2], [[127357, 127358], 2], [127359, 2], [[127360, 127369], 2], [[127370, 127373], 2], [[127374, 127375], 2], [127376, 1, "dj"], [[127377, 127386], 2], [[127387, 127404], 2], [127405, 2], [[127406, 127461], 3], [[127462, 127487], 2], [127488, 1, "\u307B\u304B"], [127489, 1, "\u30B3\u30B3"], [127490, 1, "\u30B5"], [[127491, 127503], 3], [127504, 1, "\u624B"], [127505, 1, "\u5B57"], [127506, 1, "\u53CC"], [127507, 1, "\u30C7"], [127508, 1, "\u4E8C"], [127509, 1, "\u591A"], [127510, 1, "\u89E3"], [127511, 1, "\u5929"], [127512, 1, "\u4EA4"], [127513, 1, "\u6620"], [127514, 1, "\u7121"], [127515, 1, "\u6599"], [127516, 1, "\u524D"], [127517, 1, "\u5F8C"], [127518, 1, "\u518D"], [127519, 1, "\u65B0"], [127520, 1, "\u521D"], [127521, 1, "\u7D42"], [127522, 1, "\u751F"], [127523, 1, "\u8CA9"], [127524, 1, "\u58F0"], [127525, 1, "\u5439"], [127526, 1, "\u6F14"], [127527, 1, "\u6295"], [127528, 1, "\u6355"], [127529, 1, "\u4E00"], [127530, 1, "\u4E09"], [127531, 1, "\u904A"], [127532, 1, "\u5DE6"], [127533, 1, "\u4E2D"], [127534, 1, "\u53F3"], [127535, 1, "\u6307"], [127536, 1, "\u8D70"], [127537, 1, "\u6253"], [127538, 1, "\u7981"], [127539, 1, "\u7A7A"], [127540, 1, "\u5408"], [127541, 1, "\u6E80"], [127542, 1, "\u6709"], [127543, 1, "\u6708"], [127544, 1, "\u7533"], [127545, 1, "\u5272"], [127546, 1, "\u55B6"], [127547, 1, "\u914D"], [[127548, 127551], 3], [127552, 1, "\u3014\u672C\u3015"], [127553, 1, "\u3014\u4E09\u3015"], [127554, 1, "\u3014\u4E8C\u3015"], [127555, 1, "\u3014\u5B89\u3015"], [127556, 1, "\u3014\u70B9\u3015"], [127557, 1, "\u3014\u6253\u3015"], [127558, 1, "\u3014\u76D7\u3015"], [127559, 1, "\u3014\u52DD\u3015"], [127560, 1, "\u3014\u6557\u3015"], [[127561, 127567], 3], [127568, 1, "\u5F97"], [127569, 1, "\u53EF"], [[127570, 127583], 3], [[127584, 127589], 2], [[127590, 127743], 3], [[127744, 127776], 2], [[127777, 127788], 2], [[127789, 127791], 2], [[127792, 127797], 2], [127798, 2], [[127799, 127868], 2], [127869, 2], [[127870, 127871], 2], [[127872, 127891], 2], [[127892, 127903], 2], [[127904, 127940], 2], [127941, 2], [[127942, 127946], 2], [[127947, 127950], 2], [[127951, 127955], 2], [[127956, 127967], 2], [[127968, 127984], 2], [[127985, 127991], 2], [[127992, 127999], 2], [[128e3, 128062], 2], [128063, 2], [128064, 2], [128065, 2], [[128066, 128247], 2], [128248, 2], [[128249, 128252], 2], [[128253, 128254], 2], [128255, 2], [[128256, 128317], 2], [[128318, 128319], 2], [[128320, 128323], 2], [[128324, 128330], 2], [[128331, 128335], 2], [[128336, 128359], 2], [[128360, 128377], 2], [128378, 2], [[128379, 128419], 2], [128420, 2], [[128421, 128506], 2], [[128507, 128511], 2], [128512, 2], [[128513, 128528], 2], [128529, 2], [[128530, 128532], 2], [128533, 2], [128534, 2], [128535, 2], [128536, 2], [128537, 2], [128538, 2], [128539, 2], [[128540, 128542], 2], [128543, 2], [[128544, 128549], 2], [[128550, 128551], 2], [[128552, 128555], 2], [128556, 2], [128557, 2], [[128558, 128559], 2], [[128560, 128563], 2], [128564, 2], [[128565, 128576], 2], [[128577, 128578], 2], [[128579, 128580], 2], [[128581, 128591], 2], [[128592, 128639], 2], [[128640, 128709], 2], [[128710, 128719], 2], [128720, 2], [[128721, 128722], 2], [[128723, 128724], 2], [128725, 2], [[128726, 128727], 2], [[128728, 128731], 3], [128732, 2], [[128733, 128735], 2], [[128736, 128748], 2], [[128749, 128751], 3], [[128752, 128755], 2], [[128756, 128758], 2], [[128759, 128760], 2], [128761, 2], [128762, 2], [[128763, 128764], 2], [[128765, 128767], 3], [[128768, 128883], 2], [[128884, 128886], 2], [[128887, 128890], 3], [[128891, 128895], 2], [[128896, 128980], 2], [[128981, 128984], 2], [128985, 2], [[128986, 128991], 3], [[128992, 129003], 2], [[129004, 129007], 3], [129008, 2], [[129009, 129023], 3], [[129024, 129035], 2], [[129036, 129039], 3], [[129040, 129095], 2], [[129096, 129103], 3], [[129104, 129113], 2], [[129114, 129119], 3], [[129120, 129159], 2], [[129160, 129167], 3], [[129168, 129197], 2], [[129198, 129199], 3], [[129200, 129201], 2], [[129202, 129279], 3], [[129280, 129291], 2], [129292, 2], [[129293, 129295], 2], [[129296, 129304], 2], [[129305, 129310], 2], [129311, 2], [[129312, 129319], 2], [[129320, 129327], 2], [129328, 2], [[129329, 129330], 2], [[129331, 129342], 2], [129343, 2], [[129344, 129355], 2], [129356, 2], [[129357, 129359], 2], [[129360, 129374], 2], [[129375, 129387], 2], [[129388, 129392], 2], [129393, 2], [129394, 2], [[129395, 129398], 2], [[129399, 129400], 2], [129401, 2], [129402, 2], [129403, 2], [[129404, 129407], 2], [[129408, 129412], 2], [[129413, 129425], 2], [[129426, 129431], 2], [[129432, 129442], 2], [[129443, 129444], 2], [[129445, 129450], 2], [[129451, 129453], 2], [[129454, 129455], 2], [[129456, 129465], 2], [[129466, 129471], 2], [129472, 2], [[129473, 129474], 2], [[129475, 129482], 2], [129483, 2], [129484, 2], [[129485, 129487], 2], [[129488, 129510], 2], [[129511, 129535], 2], [[129536, 129619], 2], [[129620, 129631], 3], [[129632, 129645], 2], [[129646, 129647], 3], [[129648, 129651], 2], [129652, 2], [[129653, 129655], 2], [[129656, 129658], 2], [[129659, 129660], 2], [[129661, 129663], 3], [[129664, 129666], 2], [[129667, 129670], 2], [[129671, 129672], 2], [[129673, 129679], 3], [[129680, 129685], 2], [[129686, 129704], 2], [[129705, 129708], 2], [[129709, 129711], 2], [[129712, 129718], 2], [[129719, 129722], 2], [[129723, 129725], 2], [129726, 3], [129727, 2], [[129728, 129730], 2], [[129731, 129733], 2], [[129734, 129741], 3], [[129742, 129743], 2], [[129744, 129750], 2], [[129751, 129753], 2], [[129754, 129755], 2], [[129756, 129759], 3], [[129760, 129767], 2], [129768, 2], [[129769, 129775], 3], [[129776, 129782], 2], [[129783, 129784], 2], [[129785, 129791], 3], [[129792, 129938], 2], [129939, 3], [[129940, 129994], 2], [[129995, 130031], 3], [130032, 1, "0"], [130033, 1, "1"], [130034, 1, "2"], [130035, 1, "3"], [130036, 1, "4"], [130037, 1, "5"], [130038, 1, "6"], [130039, 1, "7"], [130040, 1, "8"], [130041, 1, "9"], [[130042, 131069], 3], [[131070, 131071], 3], [[131072, 173782], 2], [[173783, 173789], 2], [[173790, 173791], 2], [[173792, 173823], 3], [[173824, 177972], 2], [[177973, 177976], 2], [177977, 2], [[177978, 177983], 3], [[177984, 178205], 2], [[178206, 178207], 3], [[178208, 183969], 2], [[183970, 183983], 3], [[183984, 191456], 2], [[191457, 191471], 3], [[191472, 192093], 2], [[192094, 194559], 3], [194560, 1, "\u4E3D"], [194561, 1, "\u4E38"], [194562, 1, "\u4E41"], [194563, 1, "\u{20122}"], [194564, 1, "\u4F60"], [194565, 1, "\u4FAE"], [194566, 1, "\u4FBB"], [194567, 1, "\u5002"], [194568, 1, "\u507A"], [194569, 1, "\u5099"], [194570, 1, "\u50E7"], [194571, 1, "\u50CF"], [194572, 1, "\u349E"], [194573, 1, "\u{2063A}"], [194574, 1, "\u514D"], [194575, 1, "\u5154"], [194576, 1, "\u5164"], [194577, 1, "\u5177"], [194578, 1, "\u{2051C}"], [194579, 1, "\u34B9"], [194580, 1, "\u5167"], [194581, 1, "\u518D"], [194582, 1, "\u{2054B}"], [194583, 1, "\u5197"], [194584, 1, "\u51A4"], [194585, 1, "\u4ECC"], [194586, 1, "\u51AC"], [194587, 1, "\u51B5"], [194588, 1, "\u{291DF}"], [194589, 1, "\u51F5"], [194590, 1, "\u5203"], [194591, 1, "\u34DF"], [194592, 1, "\u523B"], [194593, 1, "\u5246"], [194594, 1, "\u5272"], [194595, 1, "\u5277"], [194596, 1, "\u3515"], [194597, 1, "\u52C7"], [194598, 1, "\u52C9"], [194599, 1, "\u52E4"], [194600, 1, "\u52FA"], [194601, 1, "\u5305"], [194602, 1, "\u5306"], [194603, 1, "\u5317"], [194604, 1, "\u5349"], [194605, 1, "\u5351"], [194606, 1, "\u535A"], [194607, 1, "\u5373"], [194608, 1, "\u537D"], [[194609, 194611], 1, "\u537F"], [194612, 1, "\u{20A2C}"], [194613, 1, "\u7070"], [194614, 1, "\u53CA"], [194615, 1, "\u53DF"], [194616, 1, "\u{20B63}"], [194617, 1, "\u53EB"], [194618, 1, "\u53F1"], [194619, 1, "\u5406"], [194620, 1, "\u549E"], [194621, 1, "\u5438"], [194622, 1, "\u5448"], [194623, 1, "\u5468"], [194624, 1, "\u54A2"], [194625, 1, "\u54F6"], [194626, 1, "\u5510"], [194627, 1, "\u5553"], [194628, 1, "\u5563"], [[194629, 194630], 1, "\u5584"], [194631, 1, "\u5599"], [194632, 1, "\u55AB"], [194633, 1, "\u55B3"], [194634, 1, "\u55C2"], [194635, 1, "\u5716"], [194636, 1, "\u5606"], [194637, 1, "\u5717"], [194638, 1, "\u5651"], [194639, 1, "\u5674"], [194640, 1, "\u5207"], [194641, 1, "\u58EE"], [194642, 1, "\u57CE"], [194643, 1, "\u57F4"], [194644, 1, "\u580D"], [194645, 1, "\u578B"], [194646, 1, "\u5832"], [194647, 1, "\u5831"], [194648, 1, "\u58AC"], [194649, 1, "\u{214E4}"], [194650, 1, "\u58F2"], [194651, 1, "\u58F7"], [194652, 1, "\u5906"], [194653, 1, "\u591A"], [194654, 1, "\u5922"], [194655, 1, "\u5962"], [194656, 1, "\u{216A8}"], [194657, 1, "\u{216EA}"], [194658, 1, "\u59EC"], [194659, 1, "\u5A1B"], [194660, 1, "\u5A27"], [194661, 1, "\u59D8"], [194662, 1, "\u5A66"], [194663, 1, "\u36EE"], [194664, 3], [194665, 1, "\u5B08"], [[194666, 194667], 1, "\u5B3E"], [194668, 1, "\u{219C8}"], [194669, 1, "\u5BC3"], [194670, 1, "\u5BD8"], [194671, 1, "\u5BE7"], [194672, 1, "\u5BF3"], [194673, 1, "\u{21B18}"], [194674, 1, "\u5BFF"], [194675, 1, "\u5C06"], [194676, 3], [194677, 1, "\u5C22"], [194678, 1, "\u3781"], [194679, 1, "\u5C60"], [194680, 1, "\u5C6E"], [194681, 1, "\u5CC0"], [194682, 1, "\u5C8D"], [194683, 1, "\u{21DE4}"], [194684, 1, "\u5D43"], [194685, 1, "\u{21DE6}"], [194686, 1, "\u5D6E"], [194687, 1, "\u5D6B"], [194688, 1, "\u5D7C"], [194689, 1, "\u5DE1"], [194690, 1, "\u5DE2"], [194691, 1, "\u382F"], [194692, 1, "\u5DFD"], [194693, 1, "\u5E28"], [194694, 1, "\u5E3D"], [194695, 1, "\u5E69"], [194696, 1, "\u3862"], [194697, 1, "\u{22183}"], [194698, 1, "\u387C"], [194699, 1, "\u5EB0"], [194700, 1, "\u5EB3"], [194701, 1, "\u5EB6"], [194702, 1, "\u5ECA"], [194703, 1, "\u{2A392}"], [194704, 1, "\u5EFE"], [[194705, 194706], 1, "\u{22331}"], [194707, 1, "\u8201"], [[194708, 194709], 1, "\u5F22"], [194710, 1, "\u38C7"], [194711, 1, "\u{232B8}"], [194712, 1, "\u{261DA}"], [194713, 1, "\u5F62"], [194714, 1, "\u5F6B"], [194715, 1, "\u38E3"], [194716, 1, "\u5F9A"], [194717, 1, "\u5FCD"], [194718, 1, "\u5FD7"], [194719, 1, "\u5FF9"], [194720, 1, "\u6081"], [194721, 1, "\u393A"], [194722, 1, "\u391C"], [194723, 1, "\u6094"], [194724, 1, "\u{226D4}"], [194725, 1, "\u60C7"], [194726, 1, "\u6148"], [194727, 1, "\u614C"], [194728, 1, "\u614E"], [194729, 1, "\u614C"], [194730, 1, "\u617A"], [194731, 1, "\u618E"], [194732, 1, "\u61B2"], [194733, 1, "\u61A4"], [194734, 1, "\u61AF"], [194735, 1, "\u61DE"], [194736, 1, "\u61F2"], [194737, 1, "\u61F6"], [194738, 1, "\u6210"], [194739, 1, "\u621B"], [194740, 1, "\u625D"], [194741, 1, "\u62B1"], [194742, 1, "\u62D4"], [194743, 1, "\u6350"], [194744, 1, "\u{22B0C}"], [194745, 1, "\u633D"], [194746, 1, "\u62FC"], [194747, 1, "\u6368"], [194748, 1, "\u6383"], [194749, 1, "\u63E4"], [194750, 1, "\u{22BF1}"], [194751, 1, "\u6422"], [194752, 1, "\u63C5"], [194753, 1, "\u63A9"], [194754, 1, "\u3A2E"], [194755, 1, "\u6469"], [194756, 1, "\u647E"], [194757, 1, "\u649D"], [194758, 1, "\u6477"], [194759, 1, "\u3A6C"], [194760, 1, "\u654F"], [194761, 1, "\u656C"], [194762, 1, "\u{2300A}"], [194763, 1, "\u65E3"], [194764, 1, "\u66F8"], [194765, 1, "\u6649"], [194766, 1, "\u3B19"], [194767, 1, "\u6691"], [194768, 1, "\u3B08"], [194769, 1, "\u3AE4"], [194770, 1, "\u5192"], [194771, 1, "\u5195"], [194772, 1, "\u6700"], [194773, 1, "\u669C"], [194774, 1, "\u80AD"], [194775, 1, "\u43D9"], [194776, 1, "\u6717"], [194777, 1, "\u671B"], [194778, 1, "\u6721"], [194779, 1, "\u675E"], [194780, 1, "\u6753"], [194781, 1, "\u{233C3}"], [194782, 1, "\u3B49"], [194783, 1, "\u67FA"], [194784, 1, "\u6785"], [194785, 1, "\u6852"], [194786, 1, "\u6885"], [194787, 1, "\u{2346D}"], [194788, 1, "\u688E"], [194789, 1, "\u681F"], [194790, 1, "\u6914"], [194791, 1, "\u3B9D"], [194792, 1, "\u6942"], [194793, 1, "\u69A3"], [194794, 1, "\u69EA"], [194795, 1, "\u6AA8"], [194796, 1, "\u{236A3}"], [194797, 1, "\u6ADB"], [194798, 1, "\u3C18"], [194799, 1, "\u6B21"], [194800, 1, "\u{238A7}"], [194801, 1, "\u6B54"], [194802, 1, "\u3C4E"], [194803, 1, "\u6B72"], [194804, 1, "\u6B9F"], [194805, 1, "\u6BBA"], [194806, 1, "\u6BBB"], [194807, 1, "\u{23A8D}"], [194808, 1, "\u{21D0B}"], [194809, 1, "\u{23AFA}"], [194810, 1, "\u6C4E"], [194811, 1, "\u{23CBC}"], [194812, 1, "\u6CBF"], [194813, 1, "\u6CCD"], [194814, 1, "\u6C67"], [194815, 1, "\u6D16"], [194816, 1, "\u6D3E"], [194817, 1, "\u6D77"], [194818, 1, "\u6D41"], [194819, 1, "\u6D69"], [194820, 1, "\u6D78"], [194821, 1, "\u6D85"], [194822, 1, "\u{23D1E}"], [194823, 1, "\u6D34"], [194824, 1, "\u6E2F"], [194825, 1, "\u6E6E"], [194826, 1, "\u3D33"], [194827, 1, "\u6ECB"], [194828, 1, "\u6EC7"], [194829, 1, "\u{23ED1}"], [194830, 1, "\u6DF9"], [194831, 1, "\u6F6E"], [194832, 1, "\u{23F5E}"], [194833, 1, "\u{23F8E}"], [194834, 1, "\u6FC6"], [194835, 1, "\u7039"], [194836, 1, "\u701E"], [194837, 1, "\u701B"], [194838, 1, "\u3D96"], [194839, 1, "\u704A"], [194840, 1, "\u707D"], [194841, 1, "\u7077"], [194842, 1, "\u70AD"], [194843, 1, "\u{20525}"], [194844, 1, "\u7145"], [194845, 1, "\u{24263}"], [194846, 1, "\u719C"], [194847, 3], [194848, 1, "\u7228"], [194849, 1, "\u7235"], [194850, 1, "\u7250"], [194851, 1, "\u{24608}"], [194852, 1, "\u7280"], [194853, 1, "\u7295"], [194854, 1, "\u{24735}"], [194855, 1, "\u{24814}"], [194856, 1, "\u737A"], [194857, 1, "\u738B"], [194858, 1, "\u3EAC"], [194859, 1, "\u73A5"], [[194860, 194861], 1, "\u3EB8"], [194862, 1, "\u7447"], [194863, 1, "\u745C"], [194864, 1, "\u7471"], [194865, 1, "\u7485"], [194866, 1, "\u74CA"], [194867, 1, "\u3F1B"], [194868, 1, "\u7524"], [194869, 1, "\u{24C36}"], [194870, 1, "\u753E"], [194871, 1, "\u{24C92}"], [194872, 1, "\u7570"], [194873, 1, "\u{2219F}"], [194874, 1, "\u7610"], [194875, 1, "\u{24FA1}"], [194876, 1, "\u{24FB8}"], [194877, 1, "\u{25044}"], [194878, 1, "\u3FFC"], [194879, 1, "\u4008"], [194880, 1, "\u76F4"], [194881, 1, "\u{250F3}"], [194882, 1, "\u{250F2}"], [194883, 1, "\u{25119}"], [194884, 1, "\u{25133}"], [194885, 1, "\u771E"], [[194886, 194887], 1, "\u771F"], [194888, 1, "\u774A"], [194889, 1, "\u4039"], [194890, 1, "\u778B"], [194891, 1, "\u4046"], [194892, 1, "\u4096"], [194893, 1, "\u{2541D}"], [194894, 1, "\u784E"], [194895, 1, "\u788C"], [194896, 1, "\u78CC"], [194897, 1, "\u40E3"], [194898, 1, "\u{25626}"], [194899, 1, "\u7956"], [194900, 1, "\u{2569A}"], [194901, 1, "\u{256C5}"], [194902, 1, "\u798F"], [194903, 1, "\u79EB"], [194904, 1, "\u412F"], [194905, 1, "\u7A40"], [194906, 1, "\u7A4A"], [194907, 1, "\u7A4F"], [194908, 1, "\u{2597C}"], [[194909, 194910], 1, "\u{25AA7}"], [194911, 3], [194912, 1, "\u4202"], [194913, 1, "\u{25BAB}"], [194914, 1, "\u7BC6"], [194915, 1, "\u7BC9"], [194916, 1, "\u4227"], [194917, 1, "\u{25C80}"], [194918, 1, "\u7CD2"], [194919, 1, "\u42A0"], [194920, 1, "\u7CE8"], [194921, 1, "\u7CE3"], [194922, 1, "\u7D00"], [194923, 1, "\u{25F86}"], [194924, 1, "\u7D63"], [194925, 1, "\u4301"], [194926, 1, "\u7DC7"], [194927, 1, "\u7E02"], [194928, 1, "\u7E45"], [194929, 1, "\u4334"], [194930, 1, "\u{26228}"], [194931, 1, "\u{26247}"], [194932, 1, "\u4359"], [194933, 1, "\u{262D9}"], [194934, 1, "\u7F7A"], [194935, 1, "\u{2633E}"], [194936, 1, "\u7F95"], [194937, 1, "\u7FFA"], [194938, 1, "\u8005"], [194939, 1, "\u{264DA}"], [194940, 1, "\u{26523}"], [194941, 1, "\u8060"], [194942, 1, "\u{265A8}"], [194943, 1, "\u8070"], [194944, 1, "\u{2335F}"], [194945, 1, "\u43D5"], [194946, 1, "\u80B2"], [194947, 1, "\u8103"], [194948, 1, "\u440B"], [194949, 1, "\u813E"], [194950, 1, "\u5AB5"], [194951, 1, "\u{267A7}"], [194952, 1, "\u{267B5}"], [194953, 1, "\u{23393}"], [194954, 1, "\u{2339C}"], [194955, 1, "\u8201"], [194956, 1, "\u8204"], [194957, 1, "\u8F9E"], [194958, 1, "\u446B"], [194959, 1, "\u8291"], [194960, 1, "\u828B"], [194961, 1, "\u829D"], [194962, 1, "\u52B3"], [194963, 1, "\u82B1"], [194964, 1, "\u82B3"], [194965, 1, "\u82BD"], [194966, 1, "\u82E6"], [194967, 1, "\u{26B3C}"], [194968, 1, "\u82E5"], [194969, 1, "\u831D"], [194970, 1, "\u8363"], [194971, 1, "\u83AD"], [194972, 1, "\u8323"], [194973, 1, "\u83BD"], [194974, 1, "\u83E7"], [194975, 1, "\u8457"], [194976, 1, "\u8353"], [194977, 1, "\u83CA"], [194978, 1, "\u83CC"], [194979, 1, "\u83DC"], [194980, 1, "\u{26C36}"], [194981, 1, "\u{26D6B}"], [194982, 1, "\u{26CD5}"], [194983, 1, "\u452B"], [194984, 1, "\u84F1"], [194985, 1, "\u84F3"], [194986, 1, "\u8516"], [194987, 1, "\u{273CA}"], [194988, 1, "\u8564"], [194989, 1, "\u{26F2C}"], [194990, 1, "\u455D"], [194991, 1, "\u4561"], [194992, 1, "\u{26FB1}"], [194993, 1, "\u{270D2}"], [194994, 1, "\u456B"], [194995, 1, "\u8650"], [194996, 1, "\u865C"], [194997, 1, "\u8667"], [194998, 1, "\u8669"], [194999, 1, "\u86A9"], [195e3, 1, "\u8688"], [195001, 1, "\u870E"], [195002, 1, "\u86E2"], [195003, 1, "\u8779"], [195004, 1, "\u8728"], [195005, 1, "\u876B"], [195006, 1, "\u8786"], [195007, 3], [195008, 1, "\u87E1"], [195009, 1, "\u8801"], [195010, 1, "\u45F9"], [195011, 1, "\u8860"], [195012, 1, "\u8863"], [195013, 1, "\u{27667}"], [195014, 1, "\u88D7"], [195015, 1, "\u88DE"], [195016, 1, "\u4635"], [195017, 1, "\u88FA"], [195018, 1, "\u34BB"], [195019, 1, "\u{278AE}"], [195020, 1, "\u{27966}"], [195021, 1, "\u46BE"], [195022, 1, "\u46C7"], [195023, 1, "\u8AA0"], [195024, 1, "\u8AED"], [195025, 1, "\u8B8A"], [195026, 1, "\u8C55"], [195027, 1, "\u{27CA8}"], [195028, 1, "\u8CAB"], [195029, 1, "\u8CC1"], [195030, 1, "\u8D1B"], [195031, 1, "\u8D77"], [195032, 1, "\u{27F2F}"], [195033, 1, "\u{20804}"], [195034, 1, "\u8DCB"], [195035, 1, "\u8DBC"], [195036, 1, "\u8DF0"], [195037, 1, "\u{208DE}"], [195038, 1, "\u8ED4"], [195039, 1, "\u8F38"], [195040, 1, "\u{285D2}"], [195041, 1, "\u{285ED}"], [195042, 1, "\u9094"], [195043, 1, "\u90F1"], [195044, 1, "\u9111"], [195045, 1, "\u{2872E}"], [195046, 1, "\u911B"], [195047, 1, "\u9238"], [195048, 1, "\u92D7"], [195049, 1, "\u92D8"], [195050, 1, "\u927C"], [195051, 1, "\u93F9"], [195052, 1, "\u9415"], [195053, 1, "\u{28BFA}"], [195054, 1, "\u958B"], [195055, 1, "\u4995"], [195056, 1, "\u95B7"], [195057, 1, "\u{28D77}"], [195058, 1, "\u49E6"], [195059, 1, "\u96C3"], [195060, 1, "\u5DB2"], [195061, 1, "\u9723"], [195062, 1, "\u{29145}"], [195063, 1, "\u{2921A}"], [195064, 1, "\u4A6E"], [195065, 1, "\u4A76"], [195066, 1, "\u97E0"], [195067, 1, "\u{2940A}"], [195068, 1, "\u4AB2"], [195069, 1, "\u{29496}"], [[195070, 195071], 1, "\u980B"], [195072, 1, "\u9829"], [195073, 1, "\u{295B6}"], [195074, 1, "\u98E2"], [195075, 1, "\u4B33"], [195076, 1, "\u9929"], [195077, 1, "\u99A7"], [195078, 1, "\u99C2"], [195079, 1, "\u99FE"], [195080, 1, "\u4BCE"], [195081, 1, "\u{29B30}"], [195082, 1, "\u9B12"], [195083, 1, "\u9C40"], [195084, 1, "\u9CFD"], [195085, 1, "\u4CCE"], [195086, 1, "\u4CED"], [195087, 1, "\u9D67"], [195088, 1, "\u{2A0CE}"], [195089, 1, "\u4CF8"], [195090, 1, "\u{2A105}"], [195091, 1, "\u{2A20E}"], [195092, 1, "\u{2A291}"], [195093, 1, "\u9EBB"], [195094, 1, "\u4D56"], [195095, 1, "\u9EF9"], [195096, 1, "\u9EFE"], [195097, 1, "\u9F05"], [195098, 1, "\u9F0F"], [195099, 1, "\u9F16"], [195100, 1, "\u9F3B"], [195101, 1, "\u{2A600}"], [[195102, 196605], 3], [[196606, 196607], 3], [[196608, 201546], 2], [[201547, 201551], 3], [[201552, 205743], 2], [[205744, 262141], 3], [[262142, 262143], 3], [[262144, 327677], 3], [[327678, 327679], 3], [[327680, 393213], 3], [[393214, 393215], 3], [[393216, 458749], 3], [[458750, 458751], 3], [[458752, 524285], 3], [[524286, 524287], 3], [[524288, 589821], 3], [[589822, 589823], 3], [[589824, 655357], 3], [[655358, 655359], 3], [[655360, 720893], 3], [[720894, 720895], 3], [[720896, 786429], 3], [[786430, 786431], 3], [[786432, 851965], 3], [[851966, 851967], 3], [[851968, 917501], 3], [[917502, 917503], 3], [917504, 3], [917505, 3], [[917506, 917535], 3], [[917536, 917631], 3], [[917632, 917759], 3], [[917760, 917999], 7], [[918e3, 983037], 3], [[983038, 983039], 3], [[983040, 1048573], 3], [[1048574, 1048575], 3], [[1048576, 1114109], 3], [[1114110, 1114111], 3]]; } }); @@ -845,23 +845,25 @@ var require_tr46 = __commonJS({ } return null; } - function mapChars(domainName, { useSTD3ASCIIRules, processingOption }) { - let hasError = false; + function mapChars(domainName, { useSTD3ASCIIRules, transitionalProcessing }) { let processed = ""; for (const ch of domainName) { const [status, mapping] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules }); switch (status) { case STATUS_MAPPING.disallowed: - hasError = true; processed += ch; break; case STATUS_MAPPING.ignored: break; case STATUS_MAPPING.mapped: - processed += mapping; + if (transitionalProcessing && ch === "\u1E9E") { + processed += "ss"; + } else { + processed += mapping; + } break; case STATUS_MAPPING.deviation: - if (processingOption === "transitional") { + if (transitionalProcessing) { processed += mapping; } else { processed += ch; @@ -872,12 +874,19 @@ var require_tr46 = __commonJS({ break; } } - return { - string: processed, - error: hasError - }; + return processed; } - function validateLabel(label, { checkHyphens, checkBidi, checkJoiners, processingOption, useSTD3ASCIIRules }) { + function validateLabel(label, { + checkHyphens, + checkBidi, + checkJoiners, + transitionalProcessing, + useSTD3ASCIIRules, + isBidi + }) { + if (label.length === 0) { + return true; + } if (label.normalize("NFC") !== label) { return false; } @@ -887,12 +896,19 @@ var require_tr46 = __commonJS({ return false; } } - if (label.includes(".") || codePoints.length > 0 && regexes.combiningMarks.test(codePoints[0])) { + if (label.includes(".")) { + return false; + } + if (regexes.combiningMarks.test(codePoints[0])) { return false; } for (const ch of codePoints) { const [status] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules }); - if (processingOption === "transitional" && status !== STATUS_MAPPING.valid || processingOption === "nontransitional" && status !== STATUS_MAPPING.valid && status !== STATUS_MAPPING.deviation) { + if (transitionalProcessing) { + if (status !== STATUS_MAPPING.valid) { + return false; + } + } else if (status !== STATUS_MAPPING.valid && status !== STATUS_MAPPING.deviation) { return false; } } @@ -917,7 +933,7 @@ var require_tr46 = __commonJS({ } } } - if (checkBidi && codePoints.length > 0) { + if (checkBidi && isBidi) { let rtl; if (regexes.bidiS1LTR.test(codePoints[0])) { rtl = false; @@ -950,31 +966,37 @@ var require_tr46 = __commonJS({ return regexes.bidiDomain.test(domain); } function processing(domainName, options) { - const { processingOption } = options; - let { string, error } = mapChars(domainName, options); + let string = mapChars(domainName, options); string = string.normalize("NFC"); const labels = string.split("."); const isBidi = isBidiDomain(labels); + let error = false; for (const [i, origLabel] of labels.entries()) { let label = origLabel; - let curProcessing = processingOption; + let transitionalProcessingForThisLabel = options.transitionalProcessing; if (label.startsWith("xn--")) { - try { - label = punycode.decode(label.substring(4)); - labels[i] = label; - } catch (err) { + if (containsNonASCII(label)) { error = true; continue; } - curProcessing = "nontransitional"; + try { + label = punycode.decode(label.substring(4)); + } catch { + if (!options.ignoreInvalidPunycode) { + error = true; + continue; + } + } + labels[i] = label; + transitionalProcessingForThisLabel = false; } if (error) { continue; } const validation = validateLabel(label, { ...options, - processingOption: curProcessing, - checkBidi: options.checkBidi && isBidi + transitionalProcessing: transitionalProcessingForThisLabel, + isBidi }); if (!validation) { error = true; @@ -990,18 +1012,17 @@ var require_tr46 = __commonJS({ checkBidi = false, checkJoiners = false, useSTD3ASCIIRules = false, - processingOption = "nontransitional", - verifyDNSLength = false + verifyDNSLength = false, + transitionalProcessing = false, + ignoreInvalidPunycode = false } = {}) { - if (processingOption !== "transitional" && processingOption !== "nontransitional") { - throw new RangeError("processingOption must be either transitional or nontransitional"); - } const result = processing(domainName, { - processingOption, checkHyphens, checkBidi, checkJoiners, - useSTD3ASCIIRules + useSTD3ASCIIRules, + transitionalProcessing, + ignoreInvalidPunycode }); let labels = result.string.split("."); labels = labels.map((l) => { @@ -1036,14 +1057,16 @@ var require_tr46 = __commonJS({ checkBidi = false, checkJoiners = false, useSTD3ASCIIRules = false, - processingOption = "nontransitional" + transitionalProcessing = false, + ignoreInvalidPunycode = false } = {}) { const result = processing(domainName, { - processingOption, checkHyphens, checkBidi, checkJoiners, - useSTD3ASCIIRules + useSTD3ASCIIRules, + transitionalProcessing, + ignoreInvalidPunycode }); return { domain: result.string, diff --git a/api/url/urlpattern/urlpattern.js b/api/url/urlpattern/urlpattern.js index a955f62a5a..f0ae245e9d 100644 --- a/api/url/urlpattern/urlpattern.js +++ b/api/url/urlpattern/urlpattern.js @@ -1 +1 @@ -var k=class{type=3;name="";prefix="";value="";suffix="";modifier=3;constructor(t,r,n,o,c,l){this.type=t,this.name=r,this.prefix=n,this.value=o,this.suffix=c,this.modifier=l}hasCustomName(){return this.name!==""&&typeof this.name!="number"}},Pe=/[$_\p{ID_Start}]/u,Se=/[$_\u200C\u200D\p{ID_Continue}]/u,M=".*";function ke(e,t){return(t?/^[\x00-\xFF]*$/:/^[\x00-\x7F]*$/).test(e)}function v(e,t=!1){let r=[],n=0;for(;n<e.length;){let o=e[n],c=function(l){if(!t)throw new TypeError(l);r.push({type:"INVALID_CHAR",index:n,value:e[n++]})};if(o==="*"){r.push({type:"ASTERISK",index:n,value:e[n++]});continue}if(o==="+"||o==="?"){r.push({type:"OTHER_MODIFIER",index:n,value:e[n++]});continue}if(o==="\\"){r.push({type:"ESCAPED_CHAR",index:n++,value:e[n++]});continue}if(o==="{"){r.push({type:"OPEN",index:n,value:e[n++]});continue}if(o==="}"){r.push({type:"CLOSE",index:n,value:e[n++]});continue}if(o===":"){let l="",s=n+1;for(;s<e.length;){let i=e.substr(s,1);if(s===n+1&&Pe.test(i)||s!==n+1&&Se.test(i)){l+=e[s++];continue}break}if(!l){c(`Missing parameter name at ${n}`);continue}r.push({type:"NAME",index:n,value:l}),n=s;continue}if(o==="("){let l=1,s="",i=n+1,a=!1;if(e[i]==="?"){c(`Pattern cannot start with "?" at ${i}`);continue}for(;i<e.length;){if(!ke(e[i],!1)){c(`Invalid character '${e[i]}' at ${i}.`),a=!0;break}if(e[i]==="\\"){s+=e[i++]+e[i++];continue}if(e[i]===")"){if(l--,l===0){i++;break}}else if(e[i]==="("&&(l++,e[i+1]!=="?")){c(`Capturing groups are not allowed at ${i}`),a=!0;break}s+=e[i++]}if(a)continue;if(l){c(`Unbalanced pattern at ${n}`);continue}if(!s){c(`Missing pattern at ${n}`);continue}r.push({type:"REGEX",index:n,value:s}),n=i;continue}r.push({type:"CHAR",index:n,value:e[n++]})}return r.push({type:"END",index:n,value:""}),r}function D(e,t={}){let r=v(e);t.delimiter??="/#?",t.prefixes??="./";let n=`[^${x(t.delimiter)}]+?`,o=[],c=0,l=0,s="",i=new Set,a=f=>{if(l<r.length&&r[l].type===f)return r[l++].value},h=()=>a("OTHER_MODIFIER")??a("ASTERISK"),p=f=>{let u=a(f);if(u!==void 0)return u;let{type:d,index:T}=r[l];throw new TypeError(`Unexpected ${d} at ${T}, expected ${f}`)},O=()=>{let f="",u;for(;u=a("CHAR")??a("ESCAPED_CHAR");)f+=u;return f},xe=f=>f,L=t.encodePart||xe,I="",H=f=>{I+=f},$=()=>{I.length&&(o.push(new k(3,"","",L(I),"",3)),I="")},G=(f,u,d,T,Y)=>{let g=3;switch(Y){case"?":g=1;break;case"*":g=0;break;case"+":g=2;break}if(!u&&!d&&g===3){H(f);return}if($(),!u&&!d){if(!f)return;o.push(new k(3,"","",L(f),"",g));return}let m;d?d==="*"?m=M:m=d:m=n;let R=2;m===n?(R=1,m=""):m===M&&(R=0,m="");let S;if(u?S=u:d&&(S=c++),i.has(S))throw new TypeError(`Duplicate name '${S}'.`);i.add(S),o.push(new k(R,S,L(f),m,L(T),g))};for(;l<r.length;){let f=a("CHAR"),u=a("NAME"),d=a("REGEX");if(!u&&!d&&(d=a("ASTERISK")),u||d){let g=f??"";t.prefixes.indexOf(g)===-1&&(H(g),g=""),$();let m=h();G(g,u,d,"",m);continue}let T=f??a("ESCAPED_CHAR");if(T){H(T);continue}if(a("OPEN")){let g=O(),m=a("NAME"),R=a("REGEX");!m&&!R&&(R=a("ASTERISK"));let S=O();p("CLOSE");let be=h();G(g,m,R,S,be);continue}$(),p("END")}return o}function x(e){return e.replace(/([.+*?^${}()[\]|/\\])/g,"\\$1")}function X(e){return e&&e.ignoreCase?"ui":"u"}function Z(e,t,r){return F(D(e,r),t,r)}function y(e){switch(e){case 0:return"*";case 1:return"?";case 2:return"+";case 3:return""}}function F(e,t,r={}){r.delimiter??="/#?",r.prefixes??="./",r.sensitive??=!1,r.strict??=!1,r.end??=!0,r.start??=!0,r.endsWith="";let n=r.start?"^":"";for(let s of e){if(s.type===3){s.modifier===3?n+=x(s.value):n+=`(?:${x(s.value)})${y(s.modifier)}`;continue}t&&t.push(s.name);let i=`[^${x(r.delimiter)}]+?`,a=s.value;if(s.type===1?a=i:s.type===0&&(a=M),!s.prefix.length&&!s.suffix.length){s.modifier===3||s.modifier===1?n+=`(${a})${y(s.modifier)}`:n+=`((?:${a})${y(s.modifier)})`;continue}if(s.modifier===3||s.modifier===1){n+=`(?:${x(s.prefix)}(${a})${x(s.suffix)})`,n+=y(s.modifier);continue}n+=`(?:${x(s.prefix)}`,n+=`((?:${a})(?:`,n+=x(s.suffix),n+=x(s.prefix),n+=`(?:${a}))*)${x(s.suffix)})`,s.modifier===0&&(n+="?")}let o=`[${x(r.endsWith)}]|$`,c=`[${x(r.delimiter)}]`;if(r.end)return r.strict||(n+=`${c}?`),r.endsWith.length?n+=`(?=${o})`:n+="$",new RegExp(n,X(r));r.strict||(n+=`(?:${c}(?=${o}))?`);let l=!1;if(e.length){let s=e[e.length-1];s.type===3&&s.modifier===3&&(l=r.delimiter.indexOf(s)>-1)}return l||(n+=`(?=${c}|${o})`),new RegExp(n,X(r))}var b={delimiter:"",prefixes:"",sensitive:!0,strict:!0},B={delimiter:".",prefixes:"",sensitive:!0,strict:!0},q={delimiter:"/",prefixes:"/",sensitive:!0,strict:!0};function J(e,t){return e.length?e[0]==="/"?!0:!t||e.length<2?!1:(e[0]=="\\"||e[0]=="{")&&e[1]=="/":!1}function Q(e,t){return e.startsWith(t)?e.substring(t.length,e.length):e}function Ee(e,t){return e.endsWith(t)?e.substr(0,e.length-t.length):e}function W(e){return!e||e.length<2?!1:e[0]==="["||(e[0]==="\\"||e[0]==="{")&&e[1]==="["}var ee=["ftp","file","http","https","ws","wss"];function N(e){if(!e)return!0;for(let t of ee)if(e.test(t))return!0;return!1}function te(e,t){if(e=Q(e,"#"),t||e==="")return e;let r=new URL("https://example.com");return r.hash=e,r.hash?r.hash.substring(1,r.hash.length):""}function re(e,t){if(e=Q(e,"?"),t||e==="")return e;let r=new URL("https://example.com");return r.search=e,r.search?r.search.substring(1,r.search.length):""}function ne(e,t){return t||e===""?e:W(e)?j(e):z(e)}function se(e,t){if(t||e==="")return e;let r=new URL("https://example.com");return r.password=e,r.password}function ie(e,t){if(t||e==="")return e;let r=new URL("https://example.com");return r.username=e,r.username}function ae(e,t,r){if(r||e==="")return e;if(t&&!ee.includes(t))return new URL(`${t}:${e}`).pathname;let n=e[0]=="/";return e=new URL(n?e:"/-"+e,"https://example.com").pathname,n||(e=e.substring(2,e.length)),e}function oe(e,t,r){return _(t)===e&&(e=""),r||e===""?e:K(e)}function ce(e,t){return e=Ee(e,":"),t||e===""?e:A(e)}function _(e){switch(e){case"ws":case"http":return"80";case"wws":case"https":return"443";case"ftp":return"21";default:return""}}function A(e){if(e==="")return e;if(/^[-+.A-Za-z0-9]*$/.test(e))return e.toLowerCase();throw new TypeError(`Invalid protocol '${e}'.`)}function le(e){if(e==="")return e;let t=new URL("https://example.com");return t.username=e,t.username}function he(e){if(e==="")return e;let t=new URL("https://example.com");return t.password=e,t.password}function z(e){if(e==="")return e;if(/[\t\n\r #%/:<>?@[\]^\\|]/g.test(e))throw new TypeError(`Invalid hostname '${e}'`);let t=new URL("https://example.com");return t.hostname=e,t.hostname}function j(e){if(e==="")return e;if(/[^0-9a-fA-F[\]:]/g.test(e))throw new TypeError(`Invalid IPv6 hostname '${e}'`);return e.toLowerCase()}function K(e){if(e===""||/^[0-9]*$/.test(e)&&parseInt(e)<=65535)return e;throw new TypeError(`Invalid port '${e}'.`)}function fe(e){if(e==="")return e;let t=new URL("https://example.com");return t.pathname=e[0]!=="/"?"/-"+e:e,e[0]!=="/"?t.pathname.substring(2,t.pathname.length):t.pathname}function ue(e){return e===""?e:new URL(`data:${e}`).pathname}function pe(e){if(e==="")return e;let t=new URL("https://example.com");return t.search=e,t.search.substring(1,t.search.length)}function de(e){if(e==="")return e;let t=new URL("https://example.com");return t.hash=e,t.hash.substring(1,t.hash.length)}var U=class{#i;#n=[];#t={};#e=0;#s=1;#u=0;#c=0;#p=0;#d=0;#g=!1;constructor(t){this.#i=t}get result(){return this.#t}parse(){for(this.#n=v(this.#i,!0);this.#e<this.#n.length;this.#e+=this.#s){if(this.#s=1,this.#n[this.#e].type==="END"){if(this.#c===0){this.#P(),this.#l()?this.#r(9,1):this.#h()?(this.#r(8,1),this.#t.hash=""):(this.#r(7,0),this.#t.search="",this.#t.hash="");continue}else if(this.#c===2){this.#f(5);continue}this.#r(10,0);break}if(this.#p>0)if(this.#T())this.#p-=1;else continue;if(this.#O()){this.#p+=1;continue}switch(this.#c){case 0:this.#S()&&(this.#t.username="",this.#t.password="",this.#t.hostname="",this.#t.port="",this.#t.pathname="",this.#t.search="",this.#t.hash="",this.#f(1));break;case 1:if(this.#S()){this.#C();let t=7,r=1;this.#g&&(this.#t.pathname="/"),this.#E()?(t=2,r=3):this.#g&&(t=2),this.#r(t,r)}break;case 2:this.#x()?this.#f(3):(this.#b()||this.#h()||this.#l())&&this.#f(5);break;case 3:this.#R()?this.#r(4,1):this.#x()&&this.#r(5,1);break;case 4:this.#x()&&this.#r(5,1);break;case 5:this.#A()?this.#d+=1:this.#w()&&(this.#d-=1),this.#y()&&!this.#d?this.#r(6,1):this.#b()?this.#r(7,0):this.#h()?this.#r(8,1):this.#l()&&this.#r(9,1);break;case 6:this.#b()?this.#r(7,0):this.#h()?this.#r(8,1):this.#l()&&this.#r(9,1);break;case 7:this.#h()?this.#r(8,1):this.#l()&&this.#r(9,1);break;case 8:this.#l()&&this.#r(9,1);break;case 9:break;case 10:break}}}#r(t,r){switch(this.#c){case 0:break;case 1:this.#t.protocol=this.#o();break;case 2:break;case 3:this.#t.username=this.#o();break;case 4:this.#t.password=this.#o();break;case 5:this.#t.hostname=this.#o();break;case 6:this.#t.port=this.#o();break;case 7:this.#t.pathname=this.#o();break;case 8:this.#t.search=this.#o();break;case 9:this.#t.hash=this.#o();break;case 10:break}this.#k(t,r)}#k(t,r){this.#c=t,this.#u=this.#e+r,this.#e+=r,this.#s=0}#P(){this.#e=this.#u,this.#s=0}#f(t){this.#P(),this.#c=t}#m(t){return t<0&&(t=this.#n.length-t),t<this.#n.length?this.#n[t]:this.#n[this.#n.length-1]}#a(t,r){let n=this.#m(t);return n.value===r&&(n.type==="CHAR"||n.type==="ESCAPED_CHAR"||n.type==="INVALID_CHAR")}#S(){return this.#a(this.#e,":")}#E(){return this.#a(this.#e+1,"/")&&this.#a(this.#e+2,"/")}#x(){return this.#a(this.#e,"@")}#R(){return this.#a(this.#e,":")}#y(){return this.#a(this.#e,":")}#b(){return this.#a(this.#e,"/")}#h(){if(this.#a(this.#e,"?"))return!0;if(this.#n[this.#e].value!=="?")return!1;let t=this.#m(this.#e-1);return t.type!=="NAME"&&t.type!=="REGEX"&&t.type!=="CLOSE"&&t.type!=="ASTERISK"}#l(){return this.#a(this.#e,"#")}#O(){return this.#n[this.#e].type=="OPEN"}#T(){return this.#n[this.#e].type=="CLOSE"}#A(){return this.#a(this.#e,"[")}#w(){return this.#a(this.#e,"]")}#o(){let t=this.#n[this.#e],r=this.#m(this.#u).index;return this.#i.substring(r,t.index)}#C(){let t={};Object.assign(t,b),t.encodePart=A;let r=Z(this.#o(),void 0,t);this.#g=N(r)}};var V=["protocol","username","password","hostname","port","pathname","search","hash"],E="*";function ge(e,t){if(typeof e!="string")throw new TypeError("parameter 1 is not of type 'string'.");let r=new URL(e,t);return{protocol:r.protocol.substring(0,r.protocol.length-1),username:r.username,password:r.password,hostname:r.hostname,port:r.port,pathname:r.pathname,search:r.search!==""?r.search.substring(1,r.search.length):void 0,hash:r.hash!==""?r.hash.substring(1,r.hash.length):void 0}}function P(e,t){return t?C(e):e}function w(e,t,r){let n;if(typeof t.baseURL=="string")try{n=new URL(t.baseURL),e.protocol=P(n.protocol.substring(0,n.protocol.length-1),r),e.username=P(n.username,r),e.password=P(n.password,r),e.hostname=P(n.hostname,r),e.port=P(n.port,r),e.pathname=P(n.pathname,r),e.search=P(n.search.substring(1,n.search.length),r),e.hash=P(n.hash.substring(1,n.hash.length),r)}catch{throw new TypeError(`invalid baseURL '${t.baseURL}'.`)}if(typeof t.protocol=="string"&&(e.protocol=ce(t.protocol,r)),typeof t.username=="string"&&(e.username=ie(t.username,r)),typeof t.password=="string"&&(e.password=se(t.password,r)),typeof t.hostname=="string"&&(e.hostname=ne(t.hostname,r)),typeof t.port=="string"&&(e.port=oe(t.port,e.protocol,r)),typeof t.pathname=="string"){if(e.pathname=t.pathname,n&&!J(e.pathname,r)){let o=n.pathname.lastIndexOf("/");o>=0&&(e.pathname=P(n.pathname.substring(0,o+1),r)+e.pathname)}e.pathname=ae(e.pathname,e.protocol,r)}return typeof t.search=="string"&&(e.search=re(t.search,r)),typeof t.hash=="string"&&(e.hash=te(t.hash,r)),e}function C(e){return e.replace(/([+*?:{}()\\])/g,"\\$1")}function Re(e){return e.replace(/([.+*?^${}()[\]|/\\])/g,"\\$1")}function ye(e,t){t.delimiter??="/#?",t.prefixes??="./",t.sensitive??=!1,t.strict??=!1,t.end??=!0,t.start??=!0,t.endsWith="";let r=".*",n=`[^${Re(t.delimiter)}]+?`,o=/[$_\u200C\u200D\p{ID_Continue}]/u,c="";for(let l=0;l<e.length;++l){let s=e[l];if(s.type===3){if(s.modifier===3){c+=C(s.value);continue}c+=`{${C(s.value)}}${y(s.modifier)}`;continue}let i=s.hasCustomName(),a=!!s.suffix.length||!!s.prefix.length&&(s.prefix.length!==1||!t.prefixes.includes(s.prefix)),h=l>0?e[l-1]:null,p=l<e.length-1?e[l+1]:null;if(!a&&i&&s.type===1&&s.modifier===3&&p&&!p.prefix.length&&!p.suffix.length)if(p.type===3){let O=p.value.length>0?p.value[0]:"";a=o.test(O)}else a=!p.hasCustomName();if(!a&&!s.prefix.length&&h&&h.type===3){let O=h.value[h.value.length-1];a=t.prefixes.includes(O)}a&&(c+="{"),c+=C(s.prefix),i&&(c+=`:${s.name}`),s.type===2?c+=`(${s.value})`:s.type===1?i||(c+=`(${n})`):s.type===0&&(!i&&(!h||h.type===3||h.modifier!==3||a||s.prefix!=="")?c+="*":c+=`(${r})`),s.type===1&&i&&s.suffix.length&&o.test(s.suffix[0])&&(c+="\\"),c+=C(s.suffix),a&&(c+="}"),s.modifier!==3&&(c+=y(s.modifier))}return c}var me=class{#i;#n={};#t={};#e={};#s={};constructor(t={},r,n){try{let o;if(typeof r=="string"?o=r:n=r,typeof t=="string"){let i=new U(t);if(i.parse(),t=i.result,o===void 0&&typeof t.protocol!="string")throw new TypeError("A base URL must be provided for a relative constructor string.");t.baseURL=o}else{if(!t||typeof t!="object")throw new TypeError("parameter 1 is not of type 'string' and cannot convert to dictionary.");if(o)throw new TypeError("parameter 1 is not of type 'string'.")}typeof n>"u"&&(n={ignoreCase:!1});let c={ignoreCase:n.ignoreCase===!0},l={pathname:E,protocol:E,username:E,password:E,hostname:E,port:E,search:E,hash:E};this.#i=w(l,t,!0),_(this.#i.protocol)===this.#i.port&&(this.#i.port="");let s;for(s of V){if(!(s in this.#i))continue;let i={},a=this.#i[s];switch(this.#t[s]=[],s){case"protocol":Object.assign(i,b),i.encodePart=A;break;case"username":Object.assign(i,b),i.encodePart=le;break;case"password":Object.assign(i,b),i.encodePart=he;break;case"hostname":Object.assign(i,B),W(a)?i.encodePart=j:i.encodePart=z;break;case"port":Object.assign(i,b),i.encodePart=K;break;case"pathname":N(this.#n.protocol)?(Object.assign(i,q,c),i.encodePart=fe):(Object.assign(i,b,c),i.encodePart=ue);break;case"search":Object.assign(i,b,c),i.encodePart=pe;break;case"hash":Object.assign(i,b,c),i.encodePart=de;break}try{this.#s[s]=D(a,i),this.#n[s]=F(this.#s[s],this.#t[s],i),this.#e[s]=ye(this.#s[s],i)}catch{throw new TypeError(`invalid ${s} pattern '${this.#i[s]}'.`)}}}catch(o){throw new TypeError(`Failed to construct 'URLPattern': ${o.message}`)}}test(t={},r){let n={pathname:"",protocol:"",username:"",password:"",hostname:"",port:"",search:"",hash:""};if(typeof t!="string"&&r)throw new TypeError("parameter 1 is not of type 'string'.");if(typeof t>"u")return!1;try{typeof t=="object"?n=w(n,t,!1):n=w(n,ge(t,r),!1)}catch{return!1}let o;for(o of V)if(!this.#n[o].exec(n[o]))return!1;return!0}exec(t={},r){let n={pathname:"",protocol:"",username:"",password:"",hostname:"",port:"",search:"",hash:""};if(typeof t!="string"&&r)throw new TypeError("parameter 1 is not of type 'string'.");if(typeof t>"u")return;try{typeof t=="object"?n=w(n,t,!1):n=w(n,ge(t,r),!1)}catch{return null}let o={};r?o.inputs=[t,r]:o.inputs=[t];let c;for(c of V){let l=this.#n[c].exec(n[c]);if(!l)return null;let s={};for(let[i,a]of this.#t[c].entries())if(typeof a=="string"||typeof a=="number"){let h=l[i+1];s[a]=h}o[c]={input:n[c]??"",groups:s}}return o}static compareComponent(t,r,n){let o=(i,a)=>{for(let h of["type","modifier","prefix","value","suffix"]){if(i[h]<a[h])return-1;if(i[h]===a[h])continue;return 1}return 0},c=new k(3,"","","","",3),l=new k(0,"","","","",3),s=(i,a)=>{let h=0;for(;h<Math.min(i.length,a.length);++h){let p=o(i[h],a[h]);if(p)return p}return i.length===a.length?0:o(i[h]??c,a[h]??c)};return!r.#e[t]&&!n.#e[t]?0:r.#e[t]&&!n.#e[t]?s(r.#s[t],[l]):!r.#e[t]&&n.#e[t]?s([l],n.#s[t]):s(r.#s[t],n.#s[t])}get protocol(){return this.#e.protocol}get username(){return this.#e.username}get password(){return this.#e.password}get hostname(){return this.#e.hostname}get port(){return this.#e.port}get pathname(){return this.#e.pathname}get search(){return this.#e.search}get hash(){return this.#e.hash}};export{me as URLPattern}; +var R=class{type=3;name="";prefix="";value="";suffix="";modifier=3;constructor(t,r,n,o,c,l){this.type=t,this.name=r,this.prefix=n,this.value=o,this.suffix=c,this.modifier=l}hasCustomName(){return this.name!==""&&typeof this.name!="number"}},be=/[$_\p{ID_Start}]/u,Pe=/[$_\u200C\u200D\p{ID_Continue}]/u,M=".*";function Re(e,t){return(t?/^[\x00-\xFF]*$/:/^[\x00-\x7F]*$/).test(e)}function v(e,t=!1){let r=[],n=0;for(;n<e.length;){let o=e[n],c=function(l){if(!t)throw new TypeError(l);r.push({type:"INVALID_CHAR",index:n,value:e[n++]})};if(o==="*"){r.push({type:"ASTERISK",index:n,value:e[n++]});continue}if(o==="+"||o==="?"){r.push({type:"OTHER_MODIFIER",index:n,value:e[n++]});continue}if(o==="\\"){r.push({type:"ESCAPED_CHAR",index:n++,value:e[n++]});continue}if(o==="{"){r.push({type:"OPEN",index:n,value:e[n++]});continue}if(o==="}"){r.push({type:"CLOSE",index:n,value:e[n++]});continue}if(o===":"){let l="",s=n+1;for(;s<e.length;){let i=e.substr(s,1);if(s===n+1&&be.test(i)||s!==n+1&&Pe.test(i)){l+=e[s++];continue}break}if(!l){c(`Missing parameter name at ${n}`);continue}r.push({type:"NAME",index:n,value:l}),n=s;continue}if(o==="("){let l=1,s="",i=n+1,a=!1;if(e[i]==="?"){c(`Pattern cannot start with "?" at ${i}`);continue}for(;i<e.length;){if(!Re(e[i],!1)){c(`Invalid character '${e[i]}' at ${i}.`),a=!0;break}if(e[i]==="\\"){s+=e[i++]+e[i++];continue}if(e[i]===")"){if(l--,l===0){i++;break}}else if(e[i]==="("&&(l++,e[i+1]!=="?")){c(`Capturing groups are not allowed at ${i}`),a=!0;break}s+=e[i++]}if(a)continue;if(l){c(`Unbalanced pattern at ${n}`);continue}if(!s){c(`Missing pattern at ${n}`);continue}r.push({type:"REGEX",index:n,value:s}),n=i;continue}r.push({type:"CHAR",index:n,value:e[n++]})}return r.push({type:"END",index:n,value:""}),r}function D(e,t={}){let r=v(e);t.delimiter??="/#?",t.prefixes??="./";let n=`[^${S(t.delimiter)}]+?`,o=[],c=0,l=0,s="",i=new Set,a=h=>{if(l<r.length&&r[l].type===h)return r[l++].value},f=()=>a("OTHER_MODIFIER")??a("ASTERISK"),d=h=>{let u=a(h);if(u!==void 0)return u;let{type:p,index:A}=r[l];throw new TypeError(`Unexpected ${p} at ${A}, expected ${h}`)},T=()=>{let h="",u;for(;u=a("CHAR")??a("ESCAPED_CHAR");)h+=u;return h},Se=h=>h,L=t.encodePart||Se,I="",U=h=>{I+=h},$=()=>{I.length&&(o.push(new R(3,"","",L(I),"",3)),I="")},V=(h,u,p,A,Y)=>{let g=3;switch(Y){case"?":g=1;break;case"*":g=0;break;case"+":g=2;break}if(!u&&!p&&g===3){U(h);return}if($(),!u&&!p){if(!h)return;o.push(new R(3,"","",L(h),"",g));return}let m;p?p==="*"?m=M:m=p:m=n;let O=2;m===n?(O=1,m=""):m===M&&(O=0,m="");let P;if(u?P=u:p&&(P=c++),i.has(P))throw new TypeError(`Duplicate name '${P}'.`);i.add(P),o.push(new R(O,P,L(h),m,L(A),g))};for(;l<r.length;){let h=a("CHAR"),u=a("NAME"),p=a("REGEX");if(!u&&!p&&(p=a("ASTERISK")),u||p){let g=h??"";t.prefixes.indexOf(g)===-1&&(U(g),g=""),$();let m=f();V(g,u,p,"",m);continue}let A=h??a("ESCAPED_CHAR");if(A){U(A);continue}if(a("OPEN")){let g=T(),m=a("NAME"),O=a("REGEX");!m&&!O&&(O=a("ASTERISK"));let P=T();d("CLOSE");let xe=f();V(g,m,O,P,xe);continue}$(),d("END")}return o}function S(e){return e.replace(/([.+*?^${}()[\]|/\\])/g,"\\$1")}function X(e){return e&&e.ignoreCase?"ui":"u"}function Z(e,t,r){return F(D(e,r),t,r)}function k(e){switch(e){case 0:return"*";case 1:return"?";case 2:return"+";case 3:return""}}function F(e,t,r={}){r.delimiter??="/#?",r.prefixes??="./",r.sensitive??=!1,r.strict??=!1,r.end??=!0,r.start??=!0,r.endsWith="";let n=r.start?"^":"";for(let s of e){if(s.type===3){s.modifier===3?n+=S(s.value):n+=`(?:${S(s.value)})${k(s.modifier)}`;continue}t&&t.push(s.name);let i=`[^${S(r.delimiter)}]+?`,a=s.value;if(s.type===1?a=i:s.type===0&&(a=M),!s.prefix.length&&!s.suffix.length){s.modifier===3||s.modifier===1?n+=`(${a})${k(s.modifier)}`:n+=`((?:${a})${k(s.modifier)})`;continue}if(s.modifier===3||s.modifier===1){n+=`(?:${S(s.prefix)}(${a})${S(s.suffix)})`,n+=k(s.modifier);continue}n+=`(?:${S(s.prefix)}`,n+=`((?:${a})(?:`,n+=S(s.suffix),n+=S(s.prefix),n+=`(?:${a}))*)${S(s.suffix)})`,s.modifier===0&&(n+="?")}let o=`[${S(r.endsWith)}]|$`,c=`[${S(r.delimiter)}]`;if(r.end)return r.strict||(n+=`${c}?`),r.endsWith.length?n+=`(?=${o})`:n+="$",new RegExp(n,X(r));r.strict||(n+=`(?:${c}(?=${o}))?`);let l=!1;if(e.length){let s=e[e.length-1];s.type===3&&s.modifier===3&&(l=r.delimiter.indexOf(s)>-1)}return l||(n+=`(?=${c}|${o})`),new RegExp(n,X(r))}var x={delimiter:"",prefixes:"",sensitive:!0,strict:!0},B={delimiter:".",prefixes:"",sensitive:!0,strict:!0},q={delimiter:"/",prefixes:"/",sensitive:!0,strict:!0};function J(e,t){return e.length?e[0]==="/"?!0:!t||e.length<2?!1:(e[0]=="\\"||e[0]=="{")&&e[1]=="/":!1}function Q(e,t){return e.startsWith(t)?e.substring(t.length,e.length):e}function Ee(e,t){return e.endsWith(t)?e.substr(0,e.length-t.length):e}function W(e){return!e||e.length<2?!1:e[0]==="["||(e[0]==="\\"||e[0]==="{")&&e[1]==="["}var ee=["ftp","file","http","https","ws","wss"];function N(e){if(!e)return!0;for(let t of ee)if(e.test(t))return!0;return!1}function te(e,t){if(e=Q(e,"#"),t||e==="")return e;let r=new URL("https://example.com");return r.hash=e,r.hash?r.hash.substring(1,r.hash.length):""}function re(e,t){if(e=Q(e,"?"),t||e==="")return e;let r=new URL("https://example.com");return r.search=e,r.search?r.search.substring(1,r.search.length):""}function ne(e,t){return t||e===""?e:W(e)?j(e):z(e)}function se(e,t){if(t||e==="")return e;let r=new URL("https://example.com");return r.password=e,r.password}function ie(e,t){if(t||e==="")return e;let r=new URL("https://example.com");return r.username=e,r.username}function ae(e,t,r){if(r||e==="")return e;if(t&&!ee.includes(t))return new URL(`${t}:${e}`).pathname;let n=e[0]=="/";return e=new URL(n?e:"/-"+e,"https://example.com").pathname,n||(e=e.substring(2,e.length)),e}function oe(e,t,r){return _(t)===e&&(e=""),r||e===""?e:K(e)}function ce(e,t){return e=Ee(e,":"),t||e===""?e:y(e)}function _(e){switch(e){case"ws":case"http":return"80";case"wws":case"https":return"443";case"ftp":return"21";default:return""}}function y(e){if(e==="")return e;if(/^[-+.A-Za-z0-9]*$/.test(e))return e.toLowerCase();throw new TypeError(`Invalid protocol '${e}'.`)}function le(e){if(e==="")return e;let t=new URL("https://example.com");return t.username=e,t.username}function fe(e){if(e==="")return e;let t=new URL("https://example.com");return t.password=e,t.password}function z(e){if(e==="")return e;if(/[\t\n\r #%/:<>?@[\]^\\|]/g.test(e))throw new TypeError(`Invalid hostname '${e}'`);let t=new URL("https://example.com");return t.hostname=e,t.hostname}function j(e){if(e==="")return e;if(/[^0-9a-fA-F[\]:]/g.test(e))throw new TypeError(`Invalid IPv6 hostname '${e}'`);return e.toLowerCase()}function K(e){if(e===""||/^[0-9]*$/.test(e)&&parseInt(e)<=65535)return e;throw new TypeError(`Invalid port '${e}'.`)}function he(e){if(e==="")return e;let t=new URL("https://example.com");return t.pathname=e[0]!=="/"?"/-"+e:e,e[0]!=="/"?t.pathname.substring(2,t.pathname.length):t.pathname}function ue(e){return e===""?e:new URL(`data:${e}`).pathname}function de(e){if(e==="")return e;let t=new URL("https://example.com");return t.search=e,t.search.substring(1,t.search.length)}function pe(e){if(e==="")return e;let t=new URL("https://example.com");return t.hash=e,t.hash.substring(1,t.hash.length)}var H=class{#i;#n=[];#t={};#e=0;#s=1;#l=0;#o=0;#d=0;#p=0;#g=!1;constructor(t){this.#i=t}get result(){return this.#t}parse(){for(this.#n=v(this.#i,!0);this.#e<this.#n.length;this.#e+=this.#s){if(this.#s=1,this.#n[this.#e].type==="END"){if(this.#o===0){this.#b(),this.#f()?this.#r(9,1):this.#h()?this.#r(8,1):this.#r(7,0);continue}else if(this.#o===2){this.#u(5);continue}this.#r(10,0);break}if(this.#d>0)if(this.#A())this.#d-=1;else continue;if(this.#T()){this.#d+=1;continue}switch(this.#o){case 0:this.#P()&&this.#u(1);break;case 1:if(this.#P()){this.#C();let t=7,r=1;this.#E()?(t=2,r=3):this.#g&&(t=2),this.#r(t,r)}break;case 2:this.#S()?this.#u(3):(this.#x()||this.#h()||this.#f())&&this.#u(5);break;case 3:this.#O()?this.#r(4,1):this.#S()&&this.#r(5,1);break;case 4:this.#S()&&this.#r(5,1);break;case 5:this.#y()?this.#p+=1:this.#w()&&(this.#p-=1),this.#k()&&!this.#p?this.#r(6,1):this.#x()?this.#r(7,0):this.#h()?this.#r(8,1):this.#f()&&this.#r(9,1);break;case 6:this.#x()?this.#r(7,0):this.#h()?this.#r(8,1):this.#f()&&this.#r(9,1);break;case 7:this.#h()?this.#r(8,1):this.#f()&&this.#r(9,1);break;case 8:this.#f()&&this.#r(9,1);break;case 9:break;case 10:break}}this.#t.hostname!==void 0&&this.#t.port===void 0&&(this.#t.port="")}#r(t,r){switch(this.#o){case 0:break;case 1:this.#t.protocol=this.#c();break;case 2:break;case 3:this.#t.username=this.#c();break;case 4:this.#t.password=this.#c();break;case 5:this.#t.hostname=this.#c();break;case 6:this.#t.port=this.#c();break;case 7:this.#t.pathname=this.#c();break;case 8:this.#t.search=this.#c();break;case 9:this.#t.hash=this.#c();break;case 10:break}this.#o!==0&&t!==10&&([1,2,3,4].includes(this.#o)&&[6,7,8,9].includes(t)&&(this.#t.hostname??=""),[1,2,3,4,5,6].includes(this.#o)&&[8,9].includes(t)&&(this.#t.pathname??=this.#g?"/":""),[1,2,3,4,5,6,7].includes(this.#o)&&t===9&&(this.#t.search??="")),this.#R(t,r)}#R(t,r){this.#o=t,this.#l=this.#e+r,this.#e+=r,this.#s=0}#b(){this.#e=this.#l,this.#s=0}#u(t){this.#b(),this.#o=t}#m(t){return t<0&&(t=this.#n.length-t),t<this.#n.length?this.#n[t]:this.#n[this.#n.length-1]}#a(t,r){let n=this.#m(t);return n.value===r&&(n.type==="CHAR"||n.type==="ESCAPED_CHAR"||n.type==="INVALID_CHAR")}#P(){return this.#a(this.#e,":")}#E(){return this.#a(this.#e+1,"/")&&this.#a(this.#e+2,"/")}#S(){return this.#a(this.#e,"@")}#O(){return this.#a(this.#e,":")}#k(){return this.#a(this.#e,":")}#x(){return this.#a(this.#e,"/")}#h(){if(this.#a(this.#e,"?"))return!0;if(this.#n[this.#e].value!=="?")return!1;let t=this.#m(this.#e-1);return t.type!=="NAME"&&t.type!=="REGEX"&&t.type!=="CLOSE"&&t.type!=="ASTERISK"}#f(){return this.#a(this.#e,"#")}#T(){return this.#n[this.#e].type=="OPEN"}#A(){return this.#n[this.#e].type=="CLOSE"}#y(){return this.#a(this.#e,"[")}#w(){return this.#a(this.#e,"]")}#c(){let t=this.#n[this.#e],r=this.#m(this.#l).index;return this.#i.substring(r,t.index)}#C(){let t={};Object.assign(t,x),t.encodePart=y;let r=Z(this.#c(),void 0,t);this.#g=N(r)}};var G=["protocol","username","password","hostname","port","pathname","search","hash"],E="*";function ge(e,t){if(typeof e!="string")throw new TypeError("parameter 1 is not of type 'string'.");let r=new URL(e,t);return{protocol:r.protocol.substring(0,r.protocol.length-1),username:r.username,password:r.password,hostname:r.hostname,port:r.port,pathname:r.pathname,search:r.search!==""?r.search.substring(1,r.search.length):void 0,hash:r.hash!==""?r.hash.substring(1,r.hash.length):void 0}}function b(e,t){return t?C(e):e}function w(e,t,r){let n;if(typeof t.baseURL=="string")try{n=new URL(t.baseURL),t.protocol===void 0&&(e.protocol=b(n.protocol.substring(0,n.protocol.length-1),r)),!r&&t.protocol===void 0&&t.hostname===void 0&&t.port===void 0&&t.username===void 0&&(e.username=b(n.username,r)),!r&&t.protocol===void 0&&t.hostname===void 0&&t.port===void 0&&t.username===void 0&&t.password===void 0&&(e.password=b(n.password,r)),t.protocol===void 0&&t.hostname===void 0&&(e.hostname=b(n.hostname,r)),t.protocol===void 0&&t.hostname===void 0&&t.port===void 0&&(e.port=b(n.port,r)),t.protocol===void 0&&t.hostname===void 0&&t.port===void 0&&t.pathname===void 0&&(e.pathname=b(n.pathname,r)),t.protocol===void 0&&t.hostname===void 0&&t.port===void 0&&t.pathname===void 0&&t.search===void 0&&(e.search=b(n.search.substring(1,n.search.length),r)),t.protocol===void 0&&t.hostname===void 0&&t.port===void 0&&t.pathname===void 0&&t.search===void 0&&t.hash===void 0&&(e.hash=b(n.hash.substring(1,n.hash.length),r))}catch{throw new TypeError(`invalid baseURL '${t.baseURL}'.`)}if(typeof t.protocol=="string"&&(e.protocol=ce(t.protocol,r)),typeof t.username=="string"&&(e.username=ie(t.username,r)),typeof t.password=="string"&&(e.password=se(t.password,r)),typeof t.hostname=="string"&&(e.hostname=ne(t.hostname,r)),typeof t.port=="string"&&(e.port=oe(t.port,e.protocol,r)),typeof t.pathname=="string"){if(e.pathname=t.pathname,n&&!J(e.pathname,r)){let o=n.pathname.lastIndexOf("/");o>=0&&(e.pathname=b(n.pathname.substring(0,o+1),r)+e.pathname)}e.pathname=ae(e.pathname,e.protocol,r)}return typeof t.search=="string"&&(e.search=re(t.search,r)),typeof t.hash=="string"&&(e.hash=te(t.hash,r)),e}function C(e){return e.replace(/([+*?:{}()\\])/g,"\\$1")}function Oe(e){return e.replace(/([.+*?^${}()[\]|/\\])/g,"\\$1")}function ke(e,t){t.delimiter??="/#?",t.prefixes??="./",t.sensitive??=!1,t.strict??=!1,t.end??=!0,t.start??=!0,t.endsWith="";let r=".*",n=`[^${Oe(t.delimiter)}]+?`,o=/[$_\u200C\u200D\p{ID_Continue}]/u,c="";for(let l=0;l<e.length;++l){let s=e[l];if(s.type===3){if(s.modifier===3){c+=C(s.value);continue}c+=`{${C(s.value)}}${k(s.modifier)}`;continue}let i=s.hasCustomName(),a=!!s.suffix.length||!!s.prefix.length&&(s.prefix.length!==1||!t.prefixes.includes(s.prefix)),f=l>0?e[l-1]:null,d=l<e.length-1?e[l+1]:null;if(!a&&i&&s.type===1&&s.modifier===3&&d&&!d.prefix.length&&!d.suffix.length)if(d.type===3){let T=d.value.length>0?d.value[0]:"";a=o.test(T)}else a=!d.hasCustomName();if(!a&&!s.prefix.length&&f&&f.type===3){let T=f.value[f.value.length-1];a=t.prefixes.includes(T)}a&&(c+="{"),c+=C(s.prefix),i&&(c+=`:${s.name}`),s.type===2?c+=`(${s.value})`:s.type===1?i||(c+=`(${n})`):s.type===0&&(!i&&(!f||f.type===3||f.modifier!==3||a||s.prefix!=="")?c+="*":c+=`(${r})`),s.type===1&&i&&s.suffix.length&&o.test(s.suffix[0])&&(c+="\\"),c+=C(s.suffix),a&&(c+="}"),s.modifier!==3&&(c+=k(s.modifier))}return c}var me=class{#i;#n={};#t={};#e={};#s={};#l=!1;constructor(t={},r,n){try{let o;if(typeof r=="string"?o=r:n=r,typeof t=="string"){let i=new H(t);if(i.parse(),t=i.result,o===void 0&&typeof t.protocol!="string")throw new TypeError("A base URL must be provided for a relative constructor string.");t.baseURL=o}else{if(!t||typeof t!="object")throw new TypeError("parameter 1 is not of type 'string' and cannot convert to dictionary.");if(o)throw new TypeError("parameter 1 is not of type 'string'.")}typeof n>"u"&&(n={ignoreCase:!1});let c={ignoreCase:n.ignoreCase===!0},l={pathname:E,protocol:E,username:E,password:E,hostname:E,port:E,search:E,hash:E};this.#i=w(l,t,!0),_(this.#i.protocol)===this.#i.port&&(this.#i.port="");let s;for(s of G){if(!(s in this.#i))continue;let i={},a=this.#i[s];switch(this.#t[s]=[],s){case"protocol":Object.assign(i,x),i.encodePart=y;break;case"username":Object.assign(i,x),i.encodePart=le;break;case"password":Object.assign(i,x),i.encodePart=fe;break;case"hostname":Object.assign(i,B),W(a)?i.encodePart=j:i.encodePart=z;break;case"port":Object.assign(i,x),i.encodePart=K;break;case"pathname":N(this.#n.protocol)?(Object.assign(i,q,c),i.encodePart=he):(Object.assign(i,x,c),i.encodePart=ue);break;case"search":Object.assign(i,x,c),i.encodePart=de;break;case"hash":Object.assign(i,x,c),i.encodePart=pe;break}try{this.#s[s]=D(a,i),this.#n[s]=F(this.#s[s],this.#t[s],i),this.#e[s]=ke(this.#s[s],i),this.#l=this.#l||this.#s[s].some(f=>f.type===2)}catch{throw new TypeError(`invalid ${s} pattern '${this.#i[s]}'.`)}}}catch(o){throw new TypeError(`Failed to construct 'URLPattern': ${o.message}`)}}test(t={},r){let n={pathname:"",protocol:"",username:"",password:"",hostname:"",port:"",search:"",hash:""};if(typeof t!="string"&&r)throw new TypeError("parameter 1 is not of type 'string'.");if(typeof t>"u")return!1;try{typeof t=="object"?n=w(n,t,!1):n=w(n,ge(t,r),!1)}catch{return!1}let o;for(o of G)if(!this.#n[o].exec(n[o]))return!1;return!0}exec(t={},r){let n={pathname:"",protocol:"",username:"",password:"",hostname:"",port:"",search:"",hash:""};if(typeof t!="string"&&r)throw new TypeError("parameter 1 is not of type 'string'.");if(typeof t>"u")return;try{typeof t=="object"?n=w(n,t,!1):n=w(n,ge(t,r),!1)}catch{return null}let o={};r?o.inputs=[t,r]:o.inputs=[t];let c;for(c of G){let l=this.#n[c].exec(n[c]);if(!l)return null;let s={};for(let[i,a]of this.#t[c].entries())if(typeof a=="string"||typeof a=="number"){let f=l[i+1];s[a]=f}o[c]={input:n[c]??"",groups:s}}return o}static compareComponent(t,r,n){let o=(i,a)=>{for(let f of["type","modifier","prefix","value","suffix"]){if(i[f]<a[f])return-1;if(i[f]===a[f])continue;return 1}return 0},c=new R(3,"","","","",3),l=new R(0,"","","","",3),s=(i,a)=>{let f=0;for(;f<Math.min(i.length,a.length);++f){let d=o(i[f],a[f]);if(d)return d}return i.length===a.length?0:o(i[f]??c,a[f]??c)};return!r.#e[t]&&!n.#e[t]?0:r.#e[t]&&!n.#e[t]?s(r.#s[t],[l]):!r.#e[t]&&n.#e[t]?s([l],n.#s[t]):s(r.#s[t],n.#s[t])}get protocol(){return this.#e.protocol}get username(){return this.#e.username}get password(){return this.#e.password}get hostname(){return this.#e.hostname}get port(){return this.#e.port}get pathname(){return this.#e.pathname}get search(){return this.#e.search}get hash(){return this.#e.hash}get hasRegExpGroups(){return this.#l}};export{me as URLPattern}; diff --git a/api/util.js b/api/util.js index 08332f0e44..ea13e49b80 100644 --- a/api/util.js +++ b/api/util.js @@ -1,19 +1,17 @@ import { IllegalConstructorError } from './errors.js' import { Buffer } from './buffer.js' import { URL } from './url.js' +import types from './util/types.js' +import mime from './mime.js' import * as exports from './util.js' -const ObjectPrototype = Object.prototype -const Uint8ArrayPrototype = Uint8Array.prototype -const TypedArrayPrototype = Object.getPrototypeOf(Uint8ArrayPrototype) +export { types } -const AsyncFunction = (async () => {}).constructor -const TypedArray = TypedArrayPrototype.constructor +const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype) +const ObjectPrototype = Object.prototype -const kSocketCustomInspect = inspect.custom = Symbol.for('socket.util.inspect.custom') -const kNodeCustomInspect = inspect.custom = Symbol.for('nodejs.util.inspect.custom') -const kIgnoreInspect = inspect.ignore = Symbol.for('socket.util.inspect.ignore') +const kIgnoreInspect = inspect.ignore = Symbol.for('socket.runtime.util.inspect.ignore') function maybeURL (...args) { try { @@ -23,19 +21,91 @@ function maybeURL (...args) { } } +export const TextDecoder = globalThis.TextDecoder +export const TextEncoder = globalThis.TextEncoder +export const isArray = Array.isArray.bind(Array) + +export const inspectSymbols = [ + Symbol.for('socket.runtime.util.inspect.custom'), + Symbol.for('nodejs.util.inspect.custom') +] + +inspect.custom = inspectSymbols[0] + +export function debug (section) { + let enabled = false + const env = globalThis.__args?.env ?? {} + const sections = [].concat( + (env.SOCKET_DEBUG ?? '').split(','), + (env.NODE_DEBUG ?? '').split(',') + ).map((section) => section.trim()) + + if (section && sections.includes(section)) { + enabled = true + } + + function logger (...args) { + if (enabled) { + return console.debug(...args) + } + } + + Object.defineProperty(logger, 'enabled', { + configurable: false, + enumerable: false, + get: () => enabled, + set: (value) => { + if (value === true) { + enabled = true + } else if (value === false) { + enabled = false + } + } + }) + + return logger +} + export function hasOwnProperty (object, property) { return ObjectPrototype.hasOwnProperty.call(object, String(property)) } +export function isDate (object) { + return types.isDate(object) +} + export function isTypedArray (object) { - return object instanceof TypedArray + return types.isTypedArray(object) } -export function isArrayLike (object) { +export function isArrayLike (input) { return ( - (Array.isArray(object) || isTypedArray(object)) && - object !== TypedArrayPrototype && - object !== Buffer.prototype + (Array.isArray(input) || isTypedArray(input)) && + input !== TypedArrayPrototype && + input !== Buffer.prototype + ) +} + +export function isError (object) { + return types.isNativeError(object) || object instanceof globalThis.Error +} + +export function isSymbol (value) { + return typeof value === 'symbol' +} + +export function isNumber (value) { + return !isUndefined(value) && !isNull(value) && ( + typeof value === 'number' || + value instanceof Number + ) +} + +export function isBoolean (value) { + return !isUndefined(value) && !isNull(value) && ( + value === true || + value === false || + value instanceof Boolean ) } @@ -44,15 +114,11 @@ export function isArrayBufferView (buf) { } export function isAsyncFunction (object) { - return object instanceof AsyncFunction + return types.isAsyncFunction(object) } export function isArgumentsObject (object) { - return ( - !Array.isArray(object) && - isPlainObject(object) && - Number.isFinite(object.length) - ) + return types.isArgumentsObject(object) } export function isEmptyObject (object) { @@ -70,14 +136,36 @@ export function isObject (object) { ) } -export function isPlainObject (object) { +export function isUndefined (value) { + return value === undefined +} + +export function isNull (value) { + return value === null +} + +export function isNullOrUndefined (value) { + return isNull(value) || isUndefined(value) +} + +export function isPrimitive (value) { return ( - object !== null && - typeof object === 'object' && - Object.getPrototypeOf(object) === Object.prototype + isNullOrUndefined(value) || + typeof value === 'number' || + typeof value === 'string' || + typeof value === 'symbol' || + typeof value === 'boolean' ) } +export function isRegExp (value) { + return value && value instanceof RegExp +} + +export function isPlainObject (object) { + return types.isPlainObject(object) +} + export function isArrayBuffer (object) { return object !== null && object instanceof ArrayBuffer } @@ -106,12 +194,16 @@ export function isClass (value) { ) } +export function isBuffer (value) { + return Buffer.isBuffer(value) +} + export function isPromiseLike (object) { return isFunction(object?.then) } export function toString (object) { - return Object.prototype.toString(object) + return Object.prototype.toString.call(object) } export function toBuffer (object, encoding = undefined) { @@ -127,6 +219,7 @@ export function toBuffer (object, encoding = undefined) { } export function toProperCase (string) { + if (!string) return '' return string[0].toUpperCase() + string.slice(1) } @@ -147,28 +240,6 @@ export function splitBuffer (buffer, highWaterMark) { return buffers } -export function InvertedPromise () { - const context = {} - const promise = new Promise((resolve, reject) => { - Object.assign(context, { - resolve (value) { - promise.value = value - resolve(value) - return promise - }, - - reject (error) { - const err = new Error(error.message, { cause: error }) - promise.error = err - reject(err) - return promise - } - }) - }) - - return Object.assign(promise, context) -} - export function clamp (value, min, max) { if (!Number.isFinite(value)) { value = min @@ -195,22 +266,32 @@ export function promisify (original) { let object = Object.create(null) if ( + // @ts-ignore original[promisify.custom] && + // @ts-ignore typeof original[promisify.custom] === 'object' ) { + // @ts-ignore object = original[promisify.custom] } else if (original.promises && typeof original.promises === 'object') { object = original.promises } for (const key in original) { - object[key] = promisify(original[key]) + const value = original[key] + if (typeof value === 'function' || (value && typeof value === 'object')) { + object[key] = promisify(original[key].bind(original)) + } else { + object[key] = original[key] + } } + // @ts-ignore Object.defineProperty(object, promisify.custom, { configurable: true, enumerable: false, writable: false, + // @ts-ignore __proto__: null, value: object }) @@ -222,12 +303,16 @@ export function promisify (original) { throw new TypeError('Expecting original to be a function or object.') } + // @ts-ignore if (original[promisify.custom]) { + // @ts-ignore const fn = original[promisify.custom] + // @ts-ignore Object.defineProperty(fn, promisify.custom, { configurable: true, enumerable: false, writable: false, + // @ts-ignore __proto__: null, value: fn }) @@ -235,7 +320,9 @@ export function promisify (original) { return fn } + // @ts-ignore const argumentNames = Array.isArray(original[promisify.args]) + // @ts-ignore ? original[promisify.args] : [] @@ -279,12 +366,21 @@ export function inspect (value, options) { ), ...options, - options + options: { + stylize (label, style) { + return label + }, + ...options + } } return formatValue(ctx, value, ctx.depth) function formatValue (ctx, value, depth) { + if (value instanceof Symbol || typeof value === 'symbol') { + return String(value) + } + // nodejs `value.inspect()` parity if ( ctx.customInspect && @@ -305,28 +401,22 @@ export function inspect (value, options) { } return formatted - } else if ( - ( - isFunction(value?.[kNodeCustomInspect]) && - value?.[kNodeCustomInspect] !== inspect - ) || - ( - isFunction(value?.[kSocketCustomInspect]) && - value?.[kSocketCustomInspect] !== inspect - ) - ) { - const formatted = (value[kNodeCustomInspect] || value[kSocketCustomInspect]).call( - value, - depth, - ctx.options, - inspect - ) - - if (typeof formatted !== 'string') { - return formatValue(ctx, formatted, depth) + } else if (value) { + for (const inspectSymbol of inspectSymbols) { + if (isFunction(value[inspectSymbol]) && value[inspectSymbol] !== inspect) { + const formatted = value[inspectSymbol]( + depth, + ctx.options, + inspect + ) + + if (typeof formatted !== 'string') { + return formatValue(ctx, formatted, depth) + } + + return formatted + } } - - return formatted } } @@ -368,15 +458,24 @@ export function inspect (value, options) { const braces = ['{', '}'] const isArrayLikeValue = isArrayLike(value) - if (value instanceof Map) { - braces[0] = `Map(${value.size}) ${braces[0]}` - } else if (value instanceof Set) { - braces[0] = `Set(${value.size}) ${braces[0]}` + try { + if (value instanceof MIMEParams) { + braces[0] = `MIMEParams(${value.size}) ${braces[0]}` + } else if (value instanceof Map) { + braces[0] = `Map(${value.size}) ${braces[0]}` + } else if (value instanceof Set) { + braces[0] = `Set(${value.size}) ${braces[0]}` + } + } catch { + braces.splice(0, braces.length) } - const keys = value instanceof Map - ? Array.from(value.keys()) - : new Set(Object.keys(value)) + let keys = [] + try { + keys = value instanceof Map + ? Array.from(value.keys()) + : new Set(Object.keys(value)) + } catch {} const enumerableKeys = value instanceof Set ? Array(value.size).fill(0).map((_, i) => i) @@ -386,7 +485,9 @@ export function inspect (value, options) { try { const hidden = Object.getOwnPropertyNames(value) for (const key of hidden) { - keys.add(key) + if (value instanceof Error && !/stack|message|name/.test(key)) { + keys.add(key) + } } } catch (err) {} } @@ -429,6 +530,26 @@ export function inspect (value, options) { } } + if (isArgumentsObject(value)) { + typename = 'Arguments' + braces[0] = '{' + braces[1] = '}' + } else if (types.isSetIterator(value)) { + typename = 'Set Iterator' + } else if (types.isMapIterator(value)) { + typename = 'Map Iterator' + } else if (types.isIterator(value)) { + typename = 'Iterator' + } else if (types.isAsyncIterator(value)) { + typename = 'AsyncIterator' + } else if (types.isGeneratorFunction(value)) { + typename = 'GeneratorFunction' + } else if (types.isGeneratorObject(value)) { + typename = 'Generator' + } else if (types.isAsyncGeneratorFunction(value)) { + typename = 'AsyncGeneratorFunction' + } + if (!(value instanceof Map || value instanceof Set)) { if ( typeof value === 'object' && @@ -471,12 +592,15 @@ export function inspect (value, options) { const output = [] - if (isArrayLikeValue || value instanceof Set) { + if (!isArgumentsObject(value) && (isArrayLikeValue || value instanceof Set)) { // const items = isArrayLikeValue ? value : Array.from(value.values()) const size = isArrayLikeValue ? value.length : value.size for (let i = 0; i < size; ++i) { const key = String(i) if (value instanceof Set || hasOwnProperty(value, key)) { + if (key === 'length' && Array.isArray(value)) { + continue + } output.push(formatProperty( ctx, value, @@ -489,15 +613,34 @@ export function inspect (value, options) { } for (const key of keys) { - if (!/^\d+$/.test(key)) { - output.push(...Array.from(keys).map((key) => formatProperty( + if (!/^\d+$/.test(key) && key !== 'length') { + output.push(formatProperty( ctx, value, depth, enumerableKeys, key, true - ))) + )) + } + } + } else if (typeof value === 'function') { + for (const key of keys) { + if ( + !/^\d+$/.test(key) && + key !== 'name' && + key !== 'length' && + key !== 'prototype' && + key !== 'constructor' + ) { + output.push(formatProperty( + ctx, + value, + depth, + enumerableKeys, + key, + false + )) } } } else { @@ -516,13 +659,18 @@ export function inspect (value, options) { if (value instanceof Error) { let out = '' - if (value?.message && !value?.stack?.startsWith(`${value?.name}: ${value?.message}`)) { + if (value?.message && !value?.stack?.startsWith?.(`${value?.name}: ${value?.message}`)) { out += `${value.name}: ${value.message}\n` } const formatWebkitErrorStackLine = (line) => { - const [symbol = '', location = ''] = line.split('@') - const output = [] + const [symbol = '', location = ''] = line.endsWith('@') + ? [line.slice(0, -1)] + : line.startsWith('@') + ? ['', line.slice(1)] + : line.split('@') + + let output = [] const root = new URL('../', import.meta.url || globalThis.location.href).pathname let [context, lineno, colno] = ( @@ -548,15 +696,20 @@ export function inspect (value, options) { output.push(`(${context}:${lineno})`) } else if (context) { output.push(`${context}`) + } else if (!symbol) { + output.push('<anonymous>') } + output = output.map((entry) => entry.trim()).filter(Boolean) + if (output.length) { output.unshift(' at') } + return output.filter(Boolean).join(' ') } - out += (value.stack || '') + out += (typeof value?.stack === 'string' ? value.stack : '') .split('\n') .map((line) => line.includes(`${value.name}: ${value.message}`) || /^\s*at\s/.test(line) ? line @@ -800,10 +953,11 @@ export function parseHeaders (headers) { } return headers - .split('\n') + .split(/\r?\n/) .map((l) => l.trim().split(':')) - .filter((e) => e.length === 2) - .map((e) => [e[0].trim().toLowerCase(), e[1].trim().toLowerCase()]) + .filter((e) => e.length >= 2) + .map((e) => [e[0].trim().toLowerCase(), e.slice(1).join(':').trim().toLowerCase()]) + .filter((e) => e[0].length && e[1].length) } export function noop () {} @@ -824,4 +978,37 @@ export function compareBuffers (a, b) { return toBuffer(a).compare(toBuffer(b)) } +export function inherits (Constructor, Super) { + Object.defineProperty(Constructor, 'super_', { + configurable: true, + writable: true, + value: Super, + __proto__: null + }) + + Object.setPrototypeOf(Constructor.prototype, Super.prototype) +} + +export const ESM_TEST_REGEX = /\b(import\s*[\w{},*\s]*\s*from\s*['"][^'"]+['"]|export\s+(?:\*\s*from\s*['"][^'"]+['"]|default\s*from\s*['"][^'"]+['"]|[\w{}*\s,]+))\s*(?:;|\b)/ + +/** + * @ignore + * @param {string} source + * @return {boolean} + */ +export function isESMSource (source) { + if (ESM_TEST_REGEX.test(source)) { + return true + } + + return false +} + +export function deprecate (...args) { + // noop +} + +export const MIMEType = mime.MIMEType +export const MIMEParams = mime.MIMEParams + export default exports diff --git a/api/util/types.js b/api/util/types.js new file mode 100644 index 0000000000..ee84d35e81 --- /dev/null +++ b/api/util/types.js @@ -0,0 +1,500 @@ +import * as exports from './types.js' + +const Uint8ArrayPrototype = Uint8Array.prototype +const TypedArrayPrototype = Object.getPrototypeOf(Uint8ArrayPrototype) +const TypedArray = TypedArrayPrototype.constructor + +const AsyncGeneratorFunction = async function * () {}.constructor +const GeneratorFunction = function * () {}.constructor +const AsyncFunction = (async () => {}).constructor + +const AsyncGeneratorPrototype = Object.getPrototypeOf(function * () {}()).constructor.prototype +const GeneratorPrototype = Object.getPrototypeOf(function * () {}()).constructor.prototype + +const AsyncIteratorPrototype = Object.getPrototypeOf(AsyncGeneratorPrototype) +const IteratorPrototype = Object.getPrototypeOf(GeneratorPrototype) + +const MapIteratorPrototype = Object.getPrototypeOf((new Map())[Symbol.iterator]()) +const SetIteratorPrototype = Object.getPrototypeOf((new Set())[Symbol.iterator]()) + +/** + * Returns `true` if input is an `Array`. + * @param {any} input + * @return {boolean} + */ +export const isArray = Array.isArray.bind(Array) + +/** + * Returns `true` if input is a plan `Object` instance. + * @param {any} input + * @return {boolean} + */ +export function isPlainObject (input) { + return ( + input !== null && + typeof input === 'object' && + Object.getPrototypeOf(input) === Object.prototype + ) +} + +/** + * Returns `true` if input is an `AsyncFunction` + * @param {any} input + * @return {boolean} + */ +export function isAsyncFunction (input) { + return typeof input === 'function' && input instanceof AsyncFunction +} + +/** + * Returns `true` if input is an `Function` + * @param {any} input + * @return {boolean} + */ +export function isFunction (input) { + return typeof input === 'function' +} + +/** + * Returns `true` if input is an `AsyncFunction` object. + * @param {any} input + * @return {boolean} + */ +export function isAsyncFunctionObject (input) { + return typeof input === 'object' && input instanceof AsyncFunction +} + +/** + * Returns `true` if input is an `Function` object. + * @param {any} input + * @return {boolean} + */ +export function isFunctionObject (input) { + return typeof input === 'object' && input instanceof Function +} + +/** + * Always returns `false`. + * @param {any} input + * @return {boolean} + */ +export function isExternal (input) { + return false +} + +/** + * Returns `true` if input is a `Date` instance. + * @param {any} input + * @return {boolean} + */ +export function isDate (input) { + return input instanceof Date +} + +/** + * Returns `true` if input is an `arguments` object. + * @param {any} input + * @return {boolean} + */ +export function isArgumentsObject (input) { + return ( + !Array.isArray(input) && + isPlainObject(input) && + Number.isFinite(input.length) && + typeof input[Symbol.iterator] === 'function' && + 'callee' in input // access may throw error + ) +} + +/** + * Returns `true` if input is a `BigInt` object. + * @param {any} input + * @return {boolean} + */ +export function isBigIntObject (input) { + return input instanceof BigInt +} + +/** + * Returns `true` if input is a `Boolean` object. + * @param {any} input + * @return {boolean} + */ +export function isBooleanObject (input) { + return input instanceof Boolean +} + +/** + * Returns `true` if input is a `Number` object. + * @param {any} input + * @return {boolean} + */ +export function isNumberObject (input) { + return input instanceof Number +} + +/** + * Returns `true` if input is a `String` object. + * @param {any} input + * @return {boolean} + */ +export function isStringObject (input) { + return input instanceof String +} + +/** + * Returns `true` if input is a `Symbol` object. + * @param {any} input + * @return {boolean} + */ +export function isSymbolObject (input) { + return input instanceof Symbol +} + +/** + * Returns `true` if input is native `Error` instance. + * @param {any} input + * @return {boolean} + */ +export function isNativeError (input) { + return input instanceof Error +} + +/** + * Returns `true` if input is a `RegExp` instance. + * @param {any} input + * @return {boolean} + */ +export function isRegExp (input) { + return input instanceof RegExp +} + +/** + * Returns `true` if input is a `GeneratorFunction`. + * @param {any} input + * @return {boolean} + */ +export function isGeneratorFunction (input) { + return input instanceof GeneratorFunction +} + +/** + * Returns `true` if input is an `AsyncGeneratorFunction`. + * @param {any} input + * @return {boolean} + */ +export function isAsyncGeneratorFunction (input) { + return input instanceof AsyncGeneratorFunction +} + +/** + * Returns `true` if input is an instance of a `Generator`. + * @param {any} input + * @return {boolean} + */ +export function isGeneratorObject (input) { + // eslint-disable-next-line + return input && typeof input === 'object' && GeneratorPrototype.isPrototypeOf(input) +} + +/** + * Returns `true` if input is a `Promise` instance. + * @param {any} input + * @return {boolean} + */ +export function isPromise (input) { + return input instanceof Promise +} + +/** + * Returns `true` if input is a `Map` instance. + * @param {any} input + * @return {boolean} + */ +export function isMap (input) { + return input instanceof Map +} + +/** + * Returns `true` if input is a `Set` instance. + * @param {any} input + * @return {boolean} + */ +export function isSet (input) { + return input instanceof Set +} + +/** + * Returns `true` if input is an instance of an `Iterator`. + * @param {any} input + * @return {boolean} + */ +export function isIterator (input) { + // eslint-disable-next-line + return input && typeof input === 'object' && IteratorPrototype.isPrototypeOf(input) +} + +/** + * Returns `true` if input is an instance of an `AsyncIterator`. + * @param {any} input + * @return {boolean} + */ +export function isAsyncIterator (input) { + // eslint-disable-next-line + return input && typeof input === 'object' && AsyncIteratorPrototype.isPrototypeOf(input) +} + +/** + * Returns `true` if input is an instance of a `MapIterator`. + * @param {any} input + * @return {boolean} + */ +export function isMapIterator (input) { + // eslint-disable-next-line + return input && typeof input === 'object' && MapIteratorPrototype.isPrototypeOf(input) +} + +/** + * Returns `true` if input is an instance of a `SetIterator`. + * @param {any} input + * @return {boolean} + */ +export function isSetIterator (input) { + // eslint-disable-next-line + return input && typeof input === 'object' && SetIteratorPrototype.isPrototypeOf(input) +} + +/** + * Returns `true` if input is a `WeakMap` instance. + * @param {any} input + * @return {boolean} + */ +export function isWeakMap (input) { + return input instanceof WeakMap +} + +/** + * Returns `true` if input is a `WeakSet` instance. + * @param {any} input + * @return {boolean} + */ +export function isWeakSet (input) { + return input instanceof WeakSet +} + +/** + * Returns `true` if input is an `ArrayBuffer` instance. + * @param {any} input + * @return {boolean} + */ +export function isArrayBuffer (input) { + return input instanceof ArrayBuffer +} + +/** + * Returns `true` if input is an `DataView` instance. + * @param {any} input + * @return {boolean} + */ +export function isDataView (input) { + return input instanceof DataView +} + +/** + * Returns `true` if input is a `SharedArrayBuffer`. + * This will always return `false` if a `SharedArrayBuffer` + * type is not available. + * @param {any} input + * @return {boolean} + */ +export function isSharedArrayBuffer (input) { + if (typeof globalThis.SharedArrayBuffer === 'function') { + return input instanceof globalThis.SharedArrayBuffer + } + return false +} + +/** + * Not supported. This function will return `false` always. + * @param {any} input + * @return {boolean} + */ +export function isProxy (input) { + return false +} + +/** + * Returns `true` if input looks like a module namespace object. + * @param {any} input + * @return {boolean} + */ +export function isModuleNamespaceObject (input) { + return ( + input && + typeof input === 'object' && + Object.getPrototypeOf(input) === null && + Reflect.isExtensible(input) === false && + input[Symbol.toStringTag]?.() === 'Module' + ) +} + +/** + * Returns `true` if input is an `ArrayBuffer` of `SharedArrayBuffer`. + * @param {any} input + * @return {boolean} + */ +export function isAnyArrayBuffer (input) { + return isArrayBuffer(input) || isSharedArrayBuffer(input) +} + +/** + * Returns `true` if input is a "boxed" primitive. + * @param {any} input + * @return {boolean} + */ +export function isBoxedPrimitive (input) { + return ( + isSymbolObject(input) || + isBigIntObject(input) || + isNumberObject(input) || + isStringObject(input) || + isBooleanObject(input) || + isFunctionObject(input) || + isAsyncFunctionObject(input) + ) +} + +/** + * Returns `true` if input is an `ArrayBuffer` view. + * @param {any} input + * @return {boolean} + */ +export function isArrayBufferView (input) { + return isDataView(input) || ArrayBuffer.isView(input) +} + +/** + * Returns `true` if input is a `TypedArray` instance. + * @param {any} input + * @return {boolean} + */ +export function isTypedArray (input) { + return input instanceof TypedArray +} + +/** + * Returns `true` if input is an `Uint8Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isUint8Array (input) { + return input instanceof Uint8Array +} + +/** + * Returns `true` if input is an `Uint8ClampedArray` instance. + * @param {any} input + * @return {boolean} + */ +export function isUint8ClampedArray (input) { + return input instanceof Uint8ClampedArray +} + +/** + * Returns `true` if input is an `Uint16Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isUint16Array (input) { + return input instanceof Uint16Array +} + +/** + * Returns `true` if input is an `Uint32Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isUint32Array (input) { + return input instanceof Uint32Array +} + +/** + * Returns `true` if input is an Int8Array`` instance. + * @param {any} input + * @return {boolean} + */ +export function isInt8Array (input) { + return input instanceof Int8Array +} + +/** + * Returns `true` if input is an `Int16Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isInt16Array (input) { + return input instanceof Int16Array +} + +/** + * Returns `true` if input is an `Int32Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isInt32Array (input) { + return input instanceof Int32Array +} + +/** + * Returns `true` if input is an `Float32Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isFloat32Array (input) { + return input instanceof Float32Array +} + +/** + * Returns `true` if input is an `Float64Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isFloat64Array (input) { + return input instanceof Float64Array +} + +/** + * Returns `true` if input is an `BigInt64Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isBigInt64Array (input) { + return input instanceof BigInt64Array +} + +/** + * Returns `true` if input is an `BigUint64Array` instance. + * @param {any} input + * @return {boolean} + */ +export function isBigUint64Array (input) { + return input instanceof BigUint64Array +} + +/** + * @ignore + * @param {any} input + * @return {boolean} + */ +export function isKeyObject (input) { + return false +} + +/** + * Returns `true` if input is a `CryptoKey` instance. + * @param {any} input + * @return {boolean} + */ +export function isCryptoKey (input) {} + +export default exports diff --git a/api/vm.js b/api/vm.js index 55ed73e4dd..55b09de77d 100644 --- a/api/vm.js +++ b/api/vm.js @@ -1,5 +1,5 @@ /** - * @module VM + * @module vm * * This module enables compiling and running JavaScript source code in an * isolated execution context optionally with a user supplied context object. @@ -19,14 +19,16 @@ * // that exists on the user context * value = json.value * ` - * const result = await vm.runIntContext(source, context) + * const result = await vm.runInContext(source, context) * console.log(context.value) // set from `json.value` in VM context * ``` */ +/* eslint-disable no-new-func */ /* global ErrorEvent, EventTarget, MessagePort */ import { maybeMakeError } from './ipc.js' -import { SharedWorker } from './worker.js' +import { SharedWorker } from './shared-worker/index.js' +import { isESMSource } from './util.js' import application from './application.js' import globals from './internal/globals.js' import process from './process.js' @@ -40,9 +42,12 @@ const Uint8ArrayPrototype = Uint8Array.prototype const TypedArrayPrototype = Object.getPrototypeOf(Uint8ArrayPrototype) const TypedArray = TypedArrayPrototype.constructor +const kContextTag = Symbol('socket.vm.Context') +const kWorkerContextReady = Symbol('socket.vm.ContextWorker.ready') + const VM_WINDOW_INDEX = 47 const VM_WINDOW_TITLE = 'socket:vm' -const VM_WINDOW_PATH = `${globalThis.origin}/socket/vm/index.html` +const VM_WINDOW_PATH = '/socket/vm/index.html' let contextWorker = null let contextWindow = null @@ -55,6 +60,17 @@ let contextWindow = null // resources created in the script "world" in the VM realm const scripts = new WeakMap() +// A weak mapping of created contexts +const contexts = new WeakMap() +// a weak mapping of created global objects +const globalObjects = new WeakMap() + +// a shared context when one is not given +const sharedContext = createContext({}) + +// blob URL caches key by content hash +const blobURLCache = new Map() + // A weak mapping of values to reference objects const references = Object.assign(new WeakMap(), { // A mapping of reference IDs to weakly held `Reference` instances @@ -69,6 +85,33 @@ function isArrayBuffer (object) { return object instanceof ArrayBuffer } +function convertSourceToString (source) { + if (source && typeof source !== 'string') { + if (typeof source.valueOf === 'function') { + source = source.valueOf() + } + + if (typeof source.toString === 'function') { + source = source.toString() + } + } + + if (typeof source !== 'string') { + throw new TypeError( + 'Expecting Script source to be a string ' + + `or a value that can be converted to one. Received: ${source}` + ) + } + + return source +} + +/** + * Shared broadcast for virtual machaines + * @type {BroadcastChannel} + */ +export const channel = new BroadcastChannel('socket.runtime.vm') + /** * @ignore * @param {object[]} transfer @@ -161,14 +204,53 @@ export function applyOutputContextReferences (context) { visitObject(context) function visitObject (object) { - for (const key in object) { + if (object.__vmScriptReference__ && 'value' in object) { + object = object.value + } + + if (!object || typeof object !== 'object') { + return + } + + const keys = new Set(Object.keys(object)) + if ( + object && + typeof object === 'object' && + !(object instanceof Reference) && + Object.getPrototypeOf(object) !== Object.prototype && + Object.getPrototypeOf(object) !== Array.prototype + ) { + const prototype = Object.getPrototypeOf(object) + if (prototype) { + const descriptors = Object.getOwnPropertyDescriptors(prototype) + if (descriptors) { + for (const key of Object.keys(descriptors)) { + if (key !== 'constructor') { + keys.add(key) + } + } + } + } + } + + for (const key of keys) { if (key.startsWith('__vmScriptReferenceArgs_')) { Reflect.deleteProperty(object, key) continue } - const value = object[key] + let value = object[key] if (value && typeof value === 'object') { + if (Symbol.toStringTag in value) { + const tag = typeof value[Symbol.toStringTag] === 'function' + ? value[Symbol.toStringTag]() + : value[Symbol.toStringTag] + + if (tag === 'Module') { + value = object[key] = { ...value } + } + } + if (!(value.__vmScriptReference__ === true && value.id)) { visitObject(value) } @@ -243,36 +325,103 @@ export function applyContextDifferences ( Reflect.set(currentContext, key, ref.value) } else if (script) { const container = { - async [key] (...args) { + [key]: function (...args) { + const isConstructorCall = this instanceof container[key] const scriptReferenceArgsKey = `__vmScriptReferenceArgs_${reference.id}__` Reflect.set(contextReference, scriptReferenceArgsKey, args) Reflect.set(contextReference, reference.id, reference) - try { - return await script.runInContext(contextReference, { + const promise = new Promise((resolve, reject) => { + const promise = script.runInContext(contextReference, { mode: 'classic', - source: `globalObject['${reference.id}'](...globalObject['${scriptReferenceArgsKey}'])` + source: `${isConstructorCall ? 'new ' : ''}globalObject['${reference.id}'](...globalObject['${scriptReferenceArgsKey}'])` }) - // eslint-disable-next-line - } catch (err) { - throw err - } finally { + + promise.then(resolve).catch(reject) + }) + + promise.finally(() => { Reflect.deleteProperty(contextReference, reference.id) Reflect.deleteProperty(contextReference, scriptReferenceArgsKey) + }) + + if (!isConstructorCall) { + return promise.then((result) => { + if (result?.__vmScriptReference__ === true) { + return result.value + } + + return result + }) } + + return new Proxy(function () {}, { + get (target, property, receiver) { + return new Proxy(function () {}, { + apply (target, __, argumentList) { + return apply(promise) + function apply (result) { + if (!result?.then) { + return result + } + + return result + .then((result) => { + if (result?.value) { + applyContextDifferences(result, result, contextReference) + if (typeof result.value === 'object' && property in result.value) { + if (typeof result.value[property] === 'function') { + return result.value[property](...argumentList) + } + + return result.value[property] + } + + return result.value + } else { + return result + } + }) + .then(apply) + } + } + }) + }, + apply (target, thisArg, argumentList) { + return promise + .then((result) => typeof result === 'function' + ? result(...args) + : result + ) + .then((result) => typeof result === 'function' + ? isConstructorCall + ? result.call(thisArg, ...argumentList) + : result.bind(thisArg, ...argumentList) + : result + ) + .then((result) => { + applyContextDifferences(result, result, contextReference) + return result + }) + } + }) } } - // bind `null` this for proxy function - const proxyFunction = container[key].bind(null) // wrap into container for named function, called in tail with an // intentional omission of `await` for an async call stack collapse // this preserves naming in `console.log`: // [AsyncFunction: functionName] // while also removing an unneeded tail call in a stack trace const containerForNamedFunction = { - async [key] (...args) { return proxyFunction(...args) } + [key]: function (...args) { + if (this instanceof containerForNamedFunction[key]) { + return new container[key](...args) + } + + return container[key].call(this, ...args) + } } // the reference ID was created on the other side, just use it here @@ -282,8 +431,9 @@ export function applyContextDifferences ( putReference(new Reference( reference.id, containerForNamedFunction[key], - contextReference) - ) + contextReference, + { external: true } + )) // emplace an actual function on `currentContext` at the property // `key` which will do the actual proxy call to the VM script @@ -342,8 +492,16 @@ export function applyContextDifferences ( * @param {object=} [options] */ export function wrapFunctionSource (source, options = null) { - if (source.includes('return') || source.includes(';') || source.includes('throw')) { - source = `{ ${source} }` + source = source.trim() + if ( + source.startsWith('{') || + source.startsWith('async') || + source.startsWith('function') || + source.startsWith('class') + ) { + source = `(${source})` + } else if (source.includes('return') || source.includes(';') || source.includes('throw')) { + source = `{\n${source}\n}` } else if (source.includes('\n')) { const parts = source.trim().split('\n') const last = parts.pop() @@ -357,11 +515,11 @@ export function wrapFunctionSource (source, options = null) { source = parts.concat(`return ${last}`).join('\n') } - source = `{ ${source} }` + source = `{\n${source}\n}` } return ` - with (this) { return ((${options?.async ? 'async' : ''} () => ${source})()) } + with (this) { return (${options?.async ? 'async' : ''} (arguments) => ${source})(typeof arguments !== 'undefined' ? arguments : []); } //# sourceURL=${options?.filename || 'wrapped-function-source.js'} `.trim() } @@ -439,14 +597,10 @@ export const RESERVED_GLOBAL_INTRINSICS = [ 'self', 'this', 'window', - 'globalThis', - 'globalObject', 'webkit', 'chrome', 'external', 'postMessage', - 'console', - 'globalThis', 'Infinity', 'NaN', 'undefined', @@ -511,6 +665,24 @@ export const RESERVED_GLOBAL_INTRINSICS = [ * `Script` instance. */ export class Reference { + /** + * Predicate function to determine if a `value` is an internal or external + * script reference value. + * @param {amy} value + * @return {boolean} + */ + static isReference (value) { + if (references.has(value)) { + return true + } + + if (value?.__vmScriptReference__ === true && typeof value?.id === 'string') { + return true + } + + return false + } + /** * The underlying reference ID. * @ignore @@ -539,13 +711,34 @@ export class Reference { */ #context = null + /** + * A boolean value to indicate if the underlying reference value is an + * intrinsic value. + * @type {boolean} + */ + #isIntrinsic = false + + /** + * The intrinsic type this reference may be an instance of or directly refer to. + * @type {function|object} + */ + #intrinsicType = null + + /** + * A boolean value to indicate if the underlying reference value is an + * external reference value. + * @type {boolean} + */ + #isExternal = false + /** * `Reference` class constructor. * @param {string} id * @param {any} value * @param {object=} [context] + * @param {object=} [options] */ - constructor (id, value, context = null) { + constructor (id, value, context = null, options) { this.#id = id this.#type = value !== null ? typeof value : 'undefined' this.#value = value !== null && value !== undefined @@ -555,6 +748,10 @@ export class Reference { this.#context = context !== null && context !== undefined ? new WeakRef(context) : null + + this.#intrinsicType = getIntrinsicType(this.#value) + this.#isIntrinsic = isIntrinsic(this.#value) + this.#isExternal = options?.external === true } /** @@ -582,6 +779,26 @@ export class Reference { return this.#value } + /** + * The name of the type. + * @type {string?} + */ + get name () { + if (this.type === 'function') { + return this.value.name + } + + if (this.value && this.type === 'object') { + const prototype = Reflect.getPrototypeOf(this.value) + + if (prototype?.constructor?.name) { + return prototype.constructor.name + } + } + + return null + } + /** * The `Script` this value belongs to, if available. * @type {Script?} @@ -598,6 +815,32 @@ export class Reference { return this.#context?.deref?.() ?? null } + /** + * A boolean value to indicate if the underlying reference value is an + * intrinsic value. + * @type {boolean} + */ + get isIntrinsic () { + return this.#isIntrinsic + } + + /** + * A boolean value to indicate if the underlying reference value is an + * external reference value. + * @type {boolean} + */ + get isExternal () { + return this.#isExternal + } + + /** + * The intrinsic type this reference may be an instance of or directly refer to. + * @type {function|object} + */ + get intrinsicType () { + return this.#intrinsicType + } + /** * Releases strongly held value and weak references * to the "context object". @@ -612,13 +855,37 @@ export class Reference { * @param {boolean=} [includeValue = false] */ toJSON (includeValue = false) { - const { value, type, id } = this + const { isIntrinsic, name, type, id } = this + const intrinsicType = getIntrinsicTypeString(this.intrinsicType) + let { value } = this + const json = { + __vmScriptReference__: true, + id, + type, + name, + isIntrinsic, + intrinsicType + } if (includeValue) { - return { __vmScriptReference__: true, id, type, value } + if ( + value && + typeof value === 'object' && + Symbol.toStringTag in value + ) { + const tag = typeof value[Symbol.toStringTag] === 'function' + ? value[Symbol.toStringTag]() + : value[Symbol.toStringTag] + + if (tag === 'Module') { + value = { ...value } + } + } + + json.value = value } - return { __vmScriptReference__: true, id, type } + return json } } @@ -664,7 +931,7 @@ export class Script extends EventTarget { this.#id = crypto.randomBytes(8).toString('base64') this.#source = source - this.#context = options?.context ?? null + this.#context = options?.context ?? {} if (typeof options?.filename === 'string' && options.filename) { this.#filename = options.filename @@ -673,7 +940,7 @@ export class Script extends EventTarget { gc.ref(this) this.#ready = getContextWindow() - .then(getContextWorker()) + .then(() => getContextWorker()) .catch((error) => { this.dispatchEvent(new ErrorEvent('error', { error })) }) @@ -710,6 +977,14 @@ export class Script extends EventTarget { return this.#ready } + /** + * The default script context object + * @type {object} + */ + get context () { + return this.#context + } + /** * Implements `gc.finalizer` for gc'd resource cleanup. * @return {gc.Finalizer} @@ -747,7 +1022,7 @@ export class Script extends EventTarget { async runInContext (context, options = null) { await this.ready - const contextReference = context ?? this.context + const contextReference = createContext(context ?? this.context) context = { ...(context ?? this.#context) } const filename = options?.filename || this.filename @@ -792,9 +1067,26 @@ export class Script extends EventTarget { if (event.data.err) { reject(maybeMakeError(event.data.err)) } else { - const result = { data: event.data.data } - applyContextDifferences(result, event.data, contextReference) - resolve(result.data) + const { data } = event + const result = { data: data.data } + // check if result data is an external reference + const isReference = Reference.isReference(result.data) + const name = isReference ? result.data.name : null + + if (name) { + result[name] = result.data + data[name] = data.data + delete data.data + delete result.data + } + + applyContextDifferences(result, data, contextReference) + + if (name) { + resolve(result[name]) + } else { + resolve(result.data) + } } } } @@ -856,9 +1148,26 @@ export class Script extends EventTarget { if (event.data.err) { reject(maybeMakeError(event.data.err)) } else { - const result = { data: event.data.data } - applyContextDifferences(result, event.data, contextReference) - resolve(result.data) + const { data } = event + const result = { data: data.data } + // check if result data is an external reference + const isReference = Reference.isReference(result.data) + const name = isReference ? result.data.name : null + + if (name) { + result[name] = result.data + data[name] = data.data + delete data.data + delete result.data + } + + applyContextDifferences(result, data, contextReference) + + if (name) { + resolve(result[name]) + } else { + resolve(result.data) + } } } } @@ -887,8 +1196,6 @@ export class Script extends EventTarget { /** * Gets the VM context window. * This function will create it if it does not already exist. - * The current window will be used on Android or iOS platforms as there can - * only be one window. * @return {Promise<import('./window.js').ApplicationWindow} */ export async function getContextWindow () { @@ -897,87 +1204,47 @@ export async function getContextWindow () { return contextWindow } - const currentWindow = await application.getCurrentWindow() - - // just return the current window for android/ios as there can only ever be one - if ( - os.platform() === 'ios' || - os.platform() === 'android' || - (os.platform() === 'win32' && !process.env.COREWEBVIEW2_22_AVAILABLE) - ) { - contextWindow = currentWindow - - if (!contextWindow.frame) { - const frameId = `__${os.platform()}-vm-frame__` - const existingFrame = globalThis.document.querySelector( - `iframe[id="${frameId}"]` - ) - - if (existingFrame) { - existingFrame.parentElement.removeChild(existingFrame) - } - - const frame = globalThis.document.createElement('iframe') - - frame.setAttribute('sandbox', 'allow-same-origin allow-scripts') - frame.src = VM_WINDOW_PATH - frame.id = frameId - - Object.assign(frame.style, { - display: 'none', - height: 0, - width: 0 - }) - - const target = ( - globalThis.document.head ?? - globalThis.document.body ?? - globalThis.document - ) - - target.appendChild(frame) - contextWindow.frame = frame - contextWindow.ready = new Promise((resolve, reject) => { - frame.onload = resolve - frame.onerror = (event) => { - reject(new Error('Failed to load VM context window frame', { - cause: event.error ?? event - })) - } - }) - } - - await contextWindow.ready - return contextWindow - } - - const existingContextWindow = await application.getWindow(VM_WINDOW_INDEX) + const existingContextWindow = await application.getWindow(VM_WINDOW_INDEX, { max: false }) const pendingContextWindow = ( existingContextWindow ?? application.createWindow({ - canExit: true, - headless: true, - debug: true, + canExit: false, + headless: !process.env.SOCKET_RUNTIME_VM_DEBUG, + // @ts-ignore + debug: Boolean(process.env.SOCKET_RUNTIME_VM_DEBUG), index: VM_WINDOW_INDEX, title: VM_WINDOW_TITLE, - path: VM_WINDOW_PATH + path: VM_WINDOW_PATH, + config: { + webview_watch_reload: false + } }) ) const promises = [] - promises.push(pendingContextWindow) + promises.push(Promise.resolve(pendingContextWindow)) if (!existingContextWindow) { - const eventName = `vm:${VM_WINDOW_INDEX}:ready` promises.push(new Promise((resolve) => { - globalThis.addEventListener(eventName, resolve, { once: true }) + const timeout = setTimeout(resolve, 500) + channel.addEventListener('message', function onMessage (event) { + if (event.data?.ready === VM_WINDOW_INDEX) { + clearTimeout(timeout) + resolve(null) + channel.removeEventListener('message', onMessage) + } + }) })) } const ready = Promise.all(promises) + contextWindow = pendingContextWindow + contextWindow.ready = ready + await ready contextWindow = await pendingContextWindow contextWindow.ready = ready + return contextWindow } @@ -987,19 +1254,16 @@ export async function getContextWindow () { */ export async function getContextWorker () { if (contextWorker) { - return await contextWorker.ready + await contextWorker.ready + await contextWorker[kWorkerContextReady] + return contextWorker } - // just return the current window for android/ios as there can only ever be one - if ( - os.platform() === 'ios' || - os.platform() === 'android' || - (os.platform() === 'win32' && !process.env.COREWEBVIEW2_22_AVAILABLE) - ) { + if (os.platform() === 'win32' && !process.env.COREWEBVIEW2_22_AVAILABLE) { if (globalThis.window && globalThis.top === globalThis.window) { // inside global top window contextWorker = new ContextWorkerInterface() - contextWorker.ready = Promise.resolve(contextWorker) + contextWorker[kWorkerContextReady] = Promise.resolve(contextWorker) globals.register('vm.contextWorker', contextWorker) } else if ( globalThis.window && @@ -1007,8 +1271,9 @@ export async function getContextWorker () { globalThis.location.pathname === new URL(VM_WINDOW_PATH).pathname ) { // inside realm frame + // @ts-ignore contextWorker = new ContextWorkerInterfaceProxy(globalThis.top.__globals) - contextWorker.ready = Promise.resolve(contextWorker) + contextWorker[kWorkerContextReady] = Promise.resolve(contextWorker) } else { throw new TypeError('Unable to determine VM context worker') } @@ -1017,7 +1282,7 @@ export async function getContextWorker () { type: 'module' }) - contextWorker.ready = new Promise((resolve, reject) => { + contextWorker[kWorkerContextReady] = new Promise((resolve, reject) => { contextWorker.addEventListener('error', (event) => { reject(new Error('Failed to initialize VM Context SharedWorker', { cause: event.error ?? event @@ -1045,14 +1310,17 @@ export async function getContextWorker () { if ( globalThis.window && globalThis.top !== globalThis.window && - globalThis.location.pathname === new URL(VM_WINDOW_INDEX).pathname + globalThis.location.pathname === new URL(VM_WINDOW_PATH).pathname ) { + // @ts-ignore globalThis.top.__globals.register('vm.contextWorker', contextWorker) } } }) - return await contextWorker.ready + await contextWorker.ready + await contextWorker[kWorkerContextReady] + return contextWorker } /** @@ -1070,7 +1338,7 @@ export async function terminateContextWindow () { const currentContextWindow = await pendingContextWindow await currentContextWindow.close() - const existingContextWindow = await application.getWindow(VM_WINDOW_INDEX) + const existingContextWindow = await application.getWindow(VM_WINDOW_INDEX, { max: false }) if (existingContextWindow) { await existingContextWindow.close() @@ -1094,7 +1362,7 @@ export async function terminateContextWorker () { * Creates a prototype object of known global reserved intrinsics. * @ignore */ -export function createIntrinsics () { +export function createIntrinsics (options) { const descriptors = Object.create(null) const propertyNames = Object.getOwnPropertyNames(globalThis) const propertySymbols = Object.getOwnPropertySymbols(globalThis) @@ -1102,7 +1370,7 @@ export function createIntrinsics () { for (const property of propertyNames) { const intrinsic = Object.getOwnPropertyDescriptor(globalThis, property) const descriptor = Object.assign(Object.create(null), { - configurable: false, + configurable: options?.configurable === true, enumerable: true, value: intrinsic.value ?? globalThis[property] ?? undefined }) @@ -1112,7 +1380,7 @@ export function createIntrinsics () { for (const symbol of propertySymbols) { descriptors[symbol] = { - configurable: false, + configurable: options?.configurable === true, enumberale: false, value: globalThis[symbol] } @@ -1121,15 +1389,109 @@ export function createIntrinsics () { return Object.create(null, descriptors) } +/** + * Returns `true` if value is an intrinsic, otherwise `false`. + * @param {any} value + * @return {boolean} + */ +export function isIntrinsic (value) { + if (value === undefined) { + return true + } + + if (value === null) { + return null + } + + for (const key of RESERVED_GLOBAL_INTRINSICS) { + const intrinsic = globalThis[key] + if (intrinsic === value) { + return true + } else if (typeof intrinsic === 'function' && typeof value === 'object') { + const prototype = Object.getPrototypeOf(value) + if (prototype === intrinsic.prototype) { + return true + } + } + } + + return false +} + +/** + * Get the intrinsic type of a given `value`. + * @param {any} + * @return {function|object|null|undefined} + */ +export function getIntrinsicType (value) { + if (value === undefined) { + return undefined + } + + if (value === null) { + return null + } + + for (const key of RESERVED_GLOBAL_INTRINSICS) { + const intrinsic = globalThis[key] + if (intrinsic === value) { + return intrinsic + } else if (typeof intrinsic === 'function' && typeof value === 'object') { + const prototype = Object.getPrototypeOf(value) + if (prototype === intrinsic.prototype) { + return intrinsic + } + } + } + + return undefined +} + +/** + * Get the intrinsic type string of a given `value`. + * @param {any} + * @return {string|null} + */ +export function getIntrinsicTypeString (value) { + if (value === null) { + return null + } + + if (value === undefined) { + return 'undefined' + } + + for (const key of RESERVED_GLOBAL_INTRINSICS) { + const intrinsic = globalThis[key] + if (intrinsic === value) { + return key + } else if (typeof intrinsic === 'function' && typeof value === 'object') { + const prototype = Object.getPrototypeOf(value) + if (prototype === intrinsic.prototype) { + return key + } + } + } + + return null +} + /** * Creates a global proxy object for context execution. * @ignore * @param {object} context + * @param {object=} [options] * @return {Proxy} */ -export function createGlobalObject (context) { +export function createGlobalObject (context, options = null) { + const existing = context && globals.get(context) + + if (existing) { + return existing + } + const prototype = Object.getPrototypeOf(globalThis) - const intrinsics = createIntrinsics() + const intrinsics = createIntrinsics(options) const descriptors = Object.getOwnPropertyDescriptors(intrinsics) const globalObject = Object.create(prototype, descriptors) @@ -1143,8 +1505,9 @@ export function createGlobalObject (context) { } catch {} } - return new Proxy(target, { - get (_, property, receiver) { + const handler = {} + const traps = { + get (_, property) { if (property === 'console') { return console } @@ -1188,22 +1551,26 @@ export function createGlobalObject (context) { return prototype }, - setPrototypeOf (_, prototype) { + setPrototypeOf () { return false }, defineProperty (_, property, descriptor) { if (RESERVED_GLOBAL_INTRINSICS.includes(property)) { - return false + return true } if (context) { - Reflect.defineProperty(context, property, descriptor) - return true + return ( + Reflect.defineProperty(context, property, descriptor) && + Reflect.getOwnPropertyDescriptor(context, property) !== undefined + ) } - Reflect.defineProperty(globalObject, property, descriptor) - return true + return ( + Reflect.defineProperty(globalObject, property, descriptor) && + Reflect.getOwnPropertyDescriptor(globalObject, property) !== undefined + ) }, deleteProperty (_, property) { @@ -1278,7 +1645,33 @@ export function createGlobalObject (context) { return false } - }) + } + + if (Array.isArray(options?.traps)) { + for (const trap of options.traps) { + if (typeof traps[trap] === 'function') { + handler[trap] = traps[trap] + } + } + } else if (options?.traps && typeof options?.traps === 'object') { + for (const key in traps) { + if (options.traps[key] !== false) { + handler[key] = traps[key] + } + } + } else { + for (const key in traps) { + handler[key] = traps[key] + } + } + + const proxy = new Proxy(target, handler) + + if (context) { + globalObjects.set(context, proxy) + } + + return proxy } /** @@ -1287,7 +1680,11 @@ export function createGlobalObject (context) { * @return {boolean} */ export function detectFunctionSourceType (source) { - return /^\s*(import|export)\s.*$/gm.test(source) ? 'module' : 'classic' + if (isESMSource(source)) { + return 'module' + } + + return 'classic' } /** @@ -1298,15 +1695,26 @@ export function detectFunctionSourceType (source) { * @return {function} */ export function compileFunction (source, options = null) { + source = convertSourceToString(source) options = { ...options } + // detect source type naively if (!options?.type) { options.type = detectFunctionSourceType(source) } if (options?.type === 'module') { - const blob = new Blob([source], { type: 'text/javascript' }) - const url = URL.createObjectURL(blob) + const hash = crypto.murmur3(source) + let url = null + + if (blobURLCache.has(hash)) { + url = blobURLCache.get(hash) + } else { + const blob = new Blob([source], { type: 'text/javascript' }) + url = URL.createObjectURL(blob) + blobURLCache.set(hash, url) + } + const moduleSource = ` const module = await import("${url}") const exports = {} @@ -1328,20 +1736,25 @@ export function compileFunction (source, options = null) { async: true, wrap: false }) - } else { - const globalObject = createGlobalObject(options?.context) - const wrappedSource = options?.wrap === false - ? source - : wrapFunctionSource(source, options) + } + + const globalObject = ( + globalObjects.get(options?.context) ?? + createGlobalObject(options?.context) + ) - const compiled = options?.async === true - // eslint-disable-next-line - ? new AsyncFunction(wrappedSource) - // eslint-disable-next-line - : new Function(wrappedSource) + const wrappedSource = options?.wrap === false + ? source + : wrapFunctionSource(source, options) - return compiled.bind(globalObject) - } + const args = Array.from(options?.scope || []).concat(wrappedSource) + const compiled = options?.async === true + // @ts-ignore + ? new AsyncFunction(...args) + // @ts-ignore + : new Function(...args) + + return compiled.bind(globalObject, globalObject) } /** @@ -1349,32 +1762,46 @@ export function compileFunction (source, options = null) { * context is preserved until the `context` object that points to it is * garbage collected or there are no longer any references to it and its * associated `Script` instance. - * @param {string} source - * @param {ScriptOptions=} [options] + * @param {string|object|function} source * @param {object=} [context] + * @param {ScriptOptions=} [options] * @return {Promise<any>} */ -export async function runInContext (source, options, context) { - const script = scripts.get(options?.context ?? context) ?? new Script(source, options) +export async function runInContext (source, context, options) { + source = convertSourceToString(source) + context = ( + context?.context ?? + options?.context ?? + context ?? + sharedContext + ) - if (options?.context ?? context) { - scripts.set(options?.context ?? context, script) - } + const script = scripts.get(context) ?? new Script(source, options) - return await script.runInContext(options?.context ?? context, { ...options, source }) + scripts.set(context, script) + + const result = await script.runInContext(context, { + ...options, + source + }) + + return result } /** * Run `source` JavaScript in new context. The script context is destroyed after * execution. This is typically a "one off" isolated run. * @param {string} source - * @param {ScriptOptions=} [options] * @param {object=} [context] + * @param {ScriptOptions=} [options] * @return {Promise<any>} */ -export async function runInNewContext (source, options, context) { +export async function runInNewContext (source, context, options) { + source = convertSourceToString(source) + context = options?.context ?? context?.context ?? context ?? {} const script = new Script(source, options) - const result = await script.runInNewContext(options.context ?? context, options) + scripts.set(script.context, script) + const result = await script.runInNewContext(context, options) await script.destroy() return result } @@ -1386,6 +1813,8 @@ export async function runInNewContext (source, options, context) { * @return {Promise<any>} */ export async function runInThisContext (source, options) { + source = convertSourceToString(source) + const script = new Script(source, options) const result = await script.runInThisContext(options) await script.destroy() @@ -1414,11 +1843,12 @@ export function putReference (reference) { * Create a `Reference` for a `value` in a script `context`. * @param {any} value * @param {object} context + * @param {object=} [options] * @return {Reference} */ -export function createReference (value, context) { +export function createReference (value, context, options = null) { const id = crypto.randomBytes(8).toString('base64') - const reference = new Reference(id, value, context) + const reference = new Reference(id, value, context, options) putReference(reference) return reference } @@ -1449,24 +1879,52 @@ export function removeReference (id) { * @param {object} object * @return {object[]} */ -export function getTrasferables (object) { +export function getTransferables (object) { const transferables = [] findMessageTransfers(transferables, object) return transferables } +/** + * @ignore + * @param {object} object + * @return {object} + */ +export function createContext (object) { + if (isContext(object)) { + return object + } else if (object && typeof object === 'object') { + contexts.set(object, kContextTag) + } + + return object +} + +/** + * Returns `true` if `object` is a "context" object. + * @param {object} + * @return {boolean} + */ +export function isContext (object) { + return contexts.has(object) +} + export default { + createGlobalObject, compileFunction, createReference, getContextWindow, getContextWorker, getReference, - getTrasferables, + getTransferables, putReference, Reference, removeReference, runInContext, runInNewContext, runInThisContext, - Script + Script, + createContext, + isContext, + channel } diff --git a/api/vm/init.js b/api/vm/init.js index 648cd912c3..479cad73a6 100644 --- a/api/vm/init.js +++ b/api/vm/init.js @@ -14,7 +14,7 @@ class World extends EventTarget { this.id = id this.frame = createWorld({ id }) this.ready = new Promise((resolve) => { - this.frame.addEventListener('load', resolve, { once: true }) + this.frame.contentWindow.addEventListener('load', resolve, { once: true }) }) } @@ -59,24 +59,14 @@ class State { init () { globalThis.addEventListener('message', this.onMessage) hooks.onReady(async () => { + const currentWindow = await application.getCurrentWindow() + this.worker = await vm.getContextWorker() this.worker.port.addEventListener('message', this.onWorkerMessage) this.worker.port.addEventListener('mesageerror', this.onWorkerMessageError) this.worker.port.postMessage({ type: 'realm' }) - const windows = await application.getWindows() - const currentWindow = await application.getCurrentWindow() - - for (const index in windows) { - const window = windows[index] - if (window.index !== currentWindow.index) { - await currentWindow.send({ - window: window.index, - event: `vm:${currentWindow.index}:ready`, - value: {} - }) - } - } + vm.channel.postMessage({ ready: currentWindow.index }) }) } @@ -137,7 +127,10 @@ class State { } onWorkerMessageError (event) { - console.error('onWorkerMessageError', event) + globalThis.reportError( + event.error ?? + new Error('An unknown VM worker error occurred', { cause: event }) + ) } } diff --git a/api/vm/worker.js b/api/vm/worker.js index 09559180c6..9a629336cb 100644 --- a/api/vm/worker.js +++ b/api/vm/worker.js @@ -2,13 +2,6 @@ const Uint8ArrayPrototype = Uint8Array.prototype const TypedArrayPrototype = Object.getPrototypeOf(Uint8ArrayPrototype) const TypedArray = TypedArrayPrototype.constructor -// eslint-disable-next-line -function reportError (err) { - if (typeof globalThis.reportError === 'function') { - globalThis.reportError(err) - } -} - function isTypedArray (object) { return object instanceof TypedArray } @@ -152,8 +145,7 @@ class State { } onPortMessage (port, event) { - // debug echo - // port.postMessage(event.data) + // port.postMessage(event.data) // debug echo if (event.data?.type === 'terminate-worker') { for (const port of this.ports) { diff --git a/api/vm/world.js b/api/vm/world.js index bfa3a04db3..274ae65d69 100644 --- a/api/vm/world.js +++ b/api/vm/world.js @@ -101,9 +101,17 @@ globalThis.addEventListener('message', async (event) => { } if (typeof result === 'function') { - result = vm.createReference(result, context) - } else if (typeof result === 'object') { - vm.applyOutputContextReferences(result) + result = vm.createReference(result, context).toJSON() + } else if (result && typeof result === 'object') { + if ( + Object.getPrototypeOf(result) === Object.prototype || + result instanceof Array + ) { + vm.applyOutputContextReferences(result) + } else { + vm.applyOutputContextReferences(result) + result = vm.createReference(result, context).toJSON(true) + } } vm.applyOutputContextReferences(context) diff --git a/api/window.js b/api/window.js index 106cc62801..b8cf3b6aec 100644 --- a/api/window.js +++ b/api/window.js @@ -1,6 +1,6 @@ // @ts-check /** - * @module Window + * @module window * * Provides ApplicationWindow class and methods * @@ -13,6 +13,7 @@ import { isValidPercentageValue } from './util.js' import * as statuses from './window/constants.js' import location from './location.js' import { URL } from './url.js' +import client from './application/client.js' import hotkey from './window/hotkey.js' import menu from './application/menu.js' import ipc from './ipc.js' @@ -32,8 +33,10 @@ export function formatURL (url) { * Represents a window in the application */ export class ApplicationWindow { + #id = null #index #options + #channel = null #senderWindowIndex = globalThis.__args.index #listeners = {} // TODO(@chicoxyzzy): add parent and children? (needs native process support) @@ -42,8 +45,10 @@ export class ApplicationWindow { static hotkey = hotkey constructor ({ index, ...options }) { + this.#id = options?.id this.#index = index this.#options = options + this.#channel = new BroadcastChannel(`socket.runtime.window.${this.#index}`) } #updateOptions (response) { @@ -51,11 +56,20 @@ export class ApplicationWindow { if (err) { throw new Error(err) } - const { index, ...options } = data + const { id, index, ...options } = data + this.#id = id ?? null this.#options = options return data } + /** + * The unique ID of this window. + * @type {string} + */ + get id () { + return this.#id + } + /** * Get the index of the window * @return {number} - the index of the window @@ -71,6 +85,14 @@ export class ApplicationWindow { return hotkey } + /** + * The broadcast channel for this window. + * @type {BroadcastChannel} + */ + get channel () { + return this.#channel + } + /** * Get the size of the window * @return {{ width: number, height: number }} - the size of the window @@ -82,6 +104,17 @@ export class ApplicationWindow { } } + /** + * Get the position of the window + * @return {{ x: number, y: number }} - the position of the window + */ + getPosition () { + return { + x: this.#options.x, + y: this.#options.y + } + } + /** * Get the title of the window * @return {string} - the title of the window @@ -103,7 +136,7 @@ export class ApplicationWindow { * @return {Promise<object>} - the options of the window */ async close () { - const { data, err } = await ipc.send('window.close', { + const { data, err } = await ipc.request('window.close', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) @@ -118,7 +151,7 @@ export class ApplicationWindow { * @return {Promise<ipc.Result>} */ async show () { - const response = await ipc.send('window.show', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) + const response = await ipc.request('window.show', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) return this.#updateOptions(response) } @@ -127,7 +160,7 @@ export class ApplicationWindow { * @return {Promise<ipc.Result>} */ async hide () { - const response = await ipc.send('window.hide', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) + const response = await ipc.request('window.hide', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) return this.#updateOptions(response) } @@ -136,7 +169,7 @@ export class ApplicationWindow { * @return {Promise<ipc.Result>} */ async maximize () { - const response = await ipc.send('window.maximize', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) + const response = await ipc.request('window.maximize', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) return this.#updateOptions(response) } @@ -145,7 +178,7 @@ export class ApplicationWindow { * @return {Promise<ipc.Result>} */ async minimize () { - const response = await ipc.send('window.minimize', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) + const response = await ipc.request('window.minimize', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) return this.#updateOptions(response) } @@ -154,7 +187,7 @@ export class ApplicationWindow { * @return {Promise<ipc.Result>} */ async restore () { - const response = await ipc.send('window.restore', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) + const response = await ipc.request('window.restore', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) return this.#updateOptions(response) } @@ -164,7 +197,7 @@ export class ApplicationWindow { * @return {Promise<ipc.Result>} */ async setTitle (title) { - const response = await ipc.send('window.setTitle', { index: this.#senderWindowIndex, targetWindowIndex: this.#index, value: title }) + const response = await ipc.request('window.setTitle', { index: this.#senderWindowIndex, targetWindowIndex: this.#index, value: title }) return this.#updateOptions(response) } @@ -207,7 +240,54 @@ export class ApplicationWindow { options.height = opts.height.toString() } - const response = await ipc.send('window.setSize', options) + const response = await ipc.request('window.setSize', options) + return this.#updateOptions(response) + } + + /** + * Sets the position of the window + * @param {object} opts - an options object + * @param {(number|string)=} opts.x - the x position of the window + * @param {(number|string)=} opts.y - the y position of the window + * @return {Promise<object>} + * @throws {Error} - if the x or y is invalid + */ + async setPosition (opts) { + // default values + const options = { + targetWindowIndex: this.#index, + index: this.#senderWindowIndex + } + + if ((opts.x != null && typeof opts.x !== 'number' && typeof opts.x !== 'string') || + (typeof opts.x === 'string' && !isValidPercentageValue(opts.x)) || + (typeof opts.x === 'number' && !(Number.isInteger(opts.x) && opts.x > 0))) { + throw new Error(`Window x must be an integer number or a string with a valid percentage value from 0 to 100 ending with %. Got ${opts.x} instead.`) + } + + if (typeof opts.x === 'string' && isValidPercentageValue(opts.x)) { + options.x = opts.x + } + + if (typeof opts.x === 'number') { + options.x = opts.x.toString() + } + + if ((opts.y != null && typeof opts.y !== 'number' && typeof opts.y !== 'string') || + (typeof opts.y === 'string' && !isValidPercentageValue(opts.y)) || + (typeof opts.y === 'number' && !(Number.isInteger(opts.y) && opts.y > 0))) { + throw new Error(`Window y must be an integer number or a string with a valid percentage value from 0 to 100 ending with %. Got ${opts.y} instead.`) + } + + if (typeof opts.y === 'string' && isValidPercentageValue(opts.y)) { + options.y = opts.y + } + + if (typeof opts.y === 'number') { + options.y = opts.y.toString() + } + + const response = await ipc.request('window.setPosition', options) return this.#updateOptions(response) } @@ -217,7 +297,7 @@ export class ApplicationWindow { * @return {Promise<ipc.Result>} */ async navigate (path) { - const response = await ipc.send('window.navigate', { index: this.#senderWindowIndex, targetWindowIndex: this.#index, url: formatURL(path) }) + const response = await ipc.request('window.navigate', { index: this.#senderWindowIndex, targetWindowIndex: this.#index, url: formatURL(path) }) return this.#updateOptions(response) } @@ -226,7 +306,7 @@ export class ApplicationWindow { * @return {Promise<object>} */ async showInspector () { - const { data, err } = await ipc.send('window.showInspector', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) + const { data, err } = await ipc.request('window.showInspector', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) if (err) { throw err } @@ -243,10 +323,18 @@ export class ApplicationWindow { * @return {Promise<object>} */ async setBackgroundColor (opts) { - const response = await ipc.send('window.setBackgroundColor', { index: this.#senderWindowIndex, targetWindowIndex: this.#index, ...opts }) + const response = await ipc.request('window.setBackgroundColor', { index: this.#senderWindowIndex, targetWindowIndex: this.#index, ...opts }) return this.#updateOptions(response) } + /** + * Gets the background color of the window + * @return {Promise<object>} + */ + async getBackgroundColor () { + return await ipc.request('window.getBackgroundColor', { index: this.#senderWindowIndex, targetWindowIndex: this.#index }) + } + /** * Opens a native context menu. * @param {object} options - an options object @@ -312,7 +400,7 @@ export class ApplicationWindow { } /** - * This is a high-level API that you should use instead of `ipc.send` when + * This is a high-level API that you should use instead of `ipc.request` when * you want to send a message to another window or to the backend. * * @param {object} options - an options object @@ -342,14 +430,14 @@ export class ApplicationWindow { const value = typeof options.value !== 'string' ? JSON.stringify(options.value) : options.value if (options.backend === true) { - return await ipc.send('process.write', { + return await ipc.request('process.write', { index: this.#senderWindowIndex, event: options.event, value: value !== undefined ? JSON.stringify(value) : null }) } - return await ipc.send('window.send', { + return await ipc.request('window.send', { index: this.#senderWindowIndex, targetWindowIndex: options.window, event: options.event, @@ -367,7 +455,7 @@ export class ApplicationWindow { if (this.#index === this.#senderWindowIndex) { globalThis.dispatchEvent(new MessageEvent('message', message)) } else { - return await ipc.send('window.send', { + return await ipc.request('window.send', { index: this.#senderWindowIndex, targetWindowIndex: this.#index, event: 'message', @@ -377,12 +465,32 @@ export class ApplicationWindow { } /** - * Opens an URL in the default browser. - * @param {object} options - * @returns {Promise<ipc.Result>} + * Opens an URL in the default application associated with the URL protocol, + * such as 'https:' for the default web browser. + * @param {string} value + * @returns {Promise<{ url: string }>} + */ + async openExternal (value) { + const result = await ipc.request('platform.openExternal', value) + + if (result.err) { + throw result.err + } + + return result.data + } + + /** + * Opens a file in the default file explorer. + * @param {string} value + * @returns {Promise} */ - async openExternal (options) { - return await ipc.send('platform.openExternal', options) + async revealFile (value) { + const result = await ipc.request('platform.revealFile', value) + + if (result.err) { + throw result.err + } } // public EventEmitter methods @@ -481,7 +589,7 @@ export class ApplicationWindow { } } -export { hotkey } +export { client, hotkey } export default ApplicationWindow diff --git a/api/window/constants.js b/api/window/constants.js index c694d32459..013e7a0b5b 100644 --- a/api/window/constants.js +++ b/api/window/constants.js @@ -1,3 +1,5 @@ +import * as exports from './constants.js' + export const WINDOW_ERROR = -1 export const WINDOW_NONE = 0 export const WINDOW_CREATING = 10 @@ -13,4 +15,4 @@ export const WINDOW_EXITED = 51 export const WINDOW_KILLING = 60 export const WINDOW_KILLED = 61 -export * as default from './constants.js' +export default exports diff --git a/api/window/hotkey.js b/api/window/hotkey.js index ec3a33f2f6..21e462787f 100644 --- a/api/window/hotkey.js +++ b/api/window/hotkey.js @@ -34,7 +34,7 @@ export class Bindings extends EventTarget { * @ignore * @type {BroadcastChannel} */ - #channel = new BroadcastChannel('window.hotkey.bindings') + #channel = new BroadcastChannel('socket.runtime.window.hotkey.bindings') /** * The source `EventTarget` to listen for 'hotkey' events on diff --git a/api/worker.js b/api/worker.js index 0e8db46c43..d4db7c958b 100644 --- a/api/worker.js +++ b/api/worker.js @@ -1,24 +1,6 @@ -import SharedWorker from './internal/shared-worker.js' -import console from './console.js' +import { ServiceWorker } from './service-worker/instance.js' +import { SharedWorker } from './shared-worker/index.js' +import { Worker } from './worker_threads.js' -export { SharedWorker } - -// eslint-disable-next-line no-new-func -const GlobalWorker = new Function('return this.Worker')() - -class UnsupportedWorker extends EventTarget { - constructor () { - super() - console.warn('Worker is not supported in this environment') - } -} - -/** - * @type {import('dom').Worker} - */ -export const Worker = GlobalWorker || UnsupportedWorker - -/** - * @type {import('dom').SharedWorker} - */ +export { SharedWorker, ServiceWorker, Worker } export default Worker diff --git a/api/worker_threads.js b/api/worker_threads.js new file mode 100644 index 0000000000..5a3ae499b1 --- /dev/null +++ b/api/worker_threads.js @@ -0,0 +1,417 @@ +import { Writable, Readable } from './stream.js' +import { getTransferables } from './vm.js' +import init, { SHARE_ENV } from './worker_threads/init.js' +import { maybeMakeError } from './ipc.js' +import { AsyncResource } from './async/resource.js' +import { EventEmitter } from './events.js' +import { env } from './process.js' +/** + + * A pool of known worker threads. + * @type {<Map<string, Worker>} + */ +export const workers = new Map() + +/** + * `true` if this is the "main" thread, otherwise `false` + * The "main" thread is the top level webview window. + * @type {boolean} + */ +export const isMainThread = init.state.isMainThread + +/** + * The main thread `MessagePort` which is `null` when the + * current context is not the "main thread". + * @type {MessagePort?} + */ +export const mainPort = init.state.mainPort + +/** + * A worker thread `BroadcastChannel` class. + */ +export class BroadcastChannel extends globalThis.BroadcastChannel {} + +/** + * A worker thread `MessageChannel` class. + */ +export class MessageChannel extends globalThis.MessageChannel {} + +/** + * A worker thread `MessagePort` class. + */ +export class MessagePort extends globalThis.MessagePort {} + +// inherit `EventEmitter` +Object.assign(BroadcastChannel.prototype, EventEmitter.prototype) +Object.assign(MessageChannel.prototype, EventEmitter.prototype) +Object.assign(MessagePort.prototype, EventEmitter.prototype) + +/** + * The current unique thread ID. + * @type {number} + */ +export const threadId = isMainThread + ? 0 + : globalThis.RUNTIME_WORKER_ID + ? (parseInt(globalThis.RUNTIME_WORKER_ID ?? '0') || 0) + : (parseInt(globalThis.__args.client?.id) || 0) + +/** + * The parent `MessagePort` instance + * @type {MessagePort?} + */ +export const parentPort = init.state.parentPort + +/** + * Transferred "worker data" when creating a new `Worker` instance. + * @type {any?} + */ +export const workerData = init.state.workerData + +/** + * Set shared worker environment data. + * @param {string} key + * @param {any} value + */ +export function setEnvironmentData (key, value) { + // update this thread state + init.state.env[key] = value + + for (const worker of workers.values()) { + const transfer = getTransferables(value) + worker.postMessage({ + worker_threads: { + env: { key, value } + } + }, { transfer }) + } +} + +/** + * Get shared worker environment data. + * @param {string} key + * @return {any} + */ +export function getEnvironmentData (key) { + return init.state.env[key] ?? null +} + +export class Pipe extends AsyncResource { + #worker = null + #reading = true + + /** + * `Pipe` class constructor. + * @param {Childworker} worker + * @ignore + */ + constructor (worker) { + super('Pipe') + + this.#worker = worker + + if (worker.stdout) { + const { emit } = worker.stdout + worker.stdout.emit = (...args) => { + if (!this.reading) return false + return this.runInAsyncScope(() => { + return emit.call(worker.stdout, ...args) + }) + } + } + + if (worker.stderr) { + const { emit } = worker.stderr + worker.stderr.emit = (...args) => { + if (!this.reading) return false + return this.runInAsyncScope(() => { + return emit(worker.stderr, ...args) + }) + } + } + + worker.once('close', () => this.destroy()) + worker.once('exit', () => this.destroy()) + } + + /** + * `true` if the pipe is still reading, otherwise `false`. + * @type {boolean} + */ + get reading () { + return this.#reading + } + + /** + * Destroys the pipe + */ + destroy () { + this.#reading = false + } +} + +/** + * @typedef {{ + * env?: object, + * stdin?: boolean = false, + * stdout?: boolean = false, + * stderr?: boolean = false, + * workerData?: any, + * transferList?: any[], + * eval?: boolean = false + * }} WorkerOptions + +/** + * A worker thread that can communicate directly with a parent thread, + * share environment data, and process streamed data. + */ +export class Worker extends EventEmitter { + #resource = null + #worker = null + #stdin = null + #stdout = null + #stderr = null + + /** + * `Worker` class constructor. + * @param {string} filename + * @param {WorkerOptions=} [options] + */ + constructor (filename, options = null) { + super() + + options = { ...options } + + const url = '/socket/worker_threads/init.js' + this.#resource = new AsyncResource('WorkerThread') + + this.onWorkerMessage = this.onWorkerMessage.bind(this) + this.onProcessEnvironmentEvent = this.onProcessEnvironmentEvent.bind(this) + + if (options.env === SHARE_ENV) { + options.env = SHARE_ENV.toString() + env.addEventListener('set', this.onProcessEnvironmentEvent) + env.addEventListener('delete', this.onProcessEnvironmentEvent) + } + + if (options.stdin === true) { + this.#stdin = new Writable({ + write (data, cb) { + const transfer = getTransferables(data) + this.#worker.postMessage( + { worker_threads: { stdin: { data } } }, + { transfer } + ) + + cb(null) + } + }) + } + + if (options.stdout === true) { + this.#stdout = new Readable() + } + + if (options.stderr === true) { + this.#stderr = new Readable() + } + + this.#worker = new globalThis.Worker(url.toString()) + this.#worker.addEventListener('message', this.onWorkerMessage) + + if (options.workerData) { + const transfer = options.transferList ?? getTransferables(options.workerData) + const message = { + worker_threads: { workerData: options.workerData } + } + + this.#worker.postMessage(message, { transfer }) + } + + this.#worker.postMessage({ + worker_threads: { + init: { + id: this.id, + url: new URL(filename, globalThis.location.href).toString(), + eval: options?.eval === true, + process: { + env: options.env ?? {}, + stdin: options.stdin === true, + stdout: options.stdout === true, + stderr: options.stdout === true + } + } + } + }) + } + + /** + * The unique ID for this `Worker` thread instace. + * @type {number} + */ + get id () { + return this.#worker.id + } + + get threadId () { + return this.id + } + + /** + * A `Writable` standard input stream if `{ stdin: true }` was set when + * creating this `Worker` instance. + * @type {import('./stream.js').Writable?} + */ + get stdin () { + return this.#stdin + } + + /** + * A `Readable` standard output stream if `{ stdout: true }` was set when + * creating this `Worker` instance. + * @type {import('./stream.js').Readable?} + */ + get stdout () { + return this.#stdout + } + + /** + * A `Readable` standard error stream if `{ stderr: true }` was set when + * creating this `Worker` instance. + * @type {import('./stream.js').Readable?} + */ + get stderr () { + return this.#stderr + } + + /** + * Terminates the `Worker` instance + */ + terminate () { + this.#worker.terminate() + workers.delete(this.id) + this.#worker.removeEventListener('message', this.onMainThreadMessage) + + env.removeEventListener('set', this.onProcessEnvironmentEvent) + env.removeEventListener('delete', this.onProcessEnvironmentEvent) + + if (this.#stdin) { + this.#stdin.destroy() + this.#stdin = null + } + + if (this.#stdout) { + this.#stdout.destroy() + this.#stdout = null + } + + if (this.#stderr) { + this.#stderr.destroy() + this.#stderr = null + } + } + + /** + * Handles incoming worker messages. + * @ignore + * @param {MessageEvent} event + */ + onWorkerMessage (event) { + const request = event.data?.worker_threads ?? {} + + if (request.online?.id) { + workers.set(this.id, this) + this.#resource.runInAsyncScope(() => { + this.emit('online') + }) + } + + if (request.error) { + this.#resource.runInAsyncScope(() => { + this.emit('error', maybeMakeError(request.error)) + }) + } + + if (request.process?.stdout?.data && this.#stdout) { + queueMicrotask(() => { + this.#resource.runInAsyncScope(() => { + this.#stdout.push(request.process.stdout.data) + }) + }) + } + + if (request.process?.stderr?.data && this.#stderr) { + queueMicrotask(() => { + this.#resource.runInAsyncScope(() => { + this.#stderr.push(request.process.stderr.data) + }) + }) + } + + if (/set|delete/.test(request.process?.env?.type ?? '')) { + if (request.process.env.type === 'set') { + Reflect.set(env, request.process.env.key, request.process.env.value) + } else if (request.process.env.type === 'delete') { + Reflect.deleteProperty(env, request.process.env.key) + } + } + + if (request.process?.exit) { + this.#worker.terminate() + this.#resource.runInAsyncScope(() => { + this.emit('exit', request.process.exit.code ?? 0) + }) + } + + if (event.data?.worker_threads) { + event.stopImmediatePropagation() + return false + } + + this.#resource.runInAsyncScope(() => { + this.emit('message', event.data) + }) + + if (mainPort) { + this.#resource.runInAsyncScope(() => { + mainPort.dispatchEvent(new MessageEvent('message', event)) + }) + } + } + + /** + * Handles process environment change events + * @ignore + * @param {import('./process.js').ProcessEnvironmentEvent} event + */ + onProcessEnvironmentEvent (event) { + this.#worker.postMessage({ + worker_threads: { + process: { + env: { + type: event.type, + key: event.key, + value: event.value + } + } + } + }) + } + + postMessage (...args) { + this.#worker.postMessage(...args) + } +} + +export { SHARE_ENV, init } + +export default { + Worker, + isMainThread, + parentPort, + setEnvironmentData, + getEnvironmentData, + workerData, + threadId, + SHARE_ENV +} diff --git a/api/worker_threads/init.js b/api/worker_threads/init.js new file mode 100644 index 0000000000..b798eeafd0 --- /dev/null +++ b/api/worker_threads/init.js @@ -0,0 +1,317 @@ +import vm, { getTransferables } from '../vm.js' +import { Writable, Readable } from '../stream.js' +import process, { env } from '../process.js' + +export const SHARE_ENV = Symbol.for('socket.runtime.worker_threads.SHARE_ENV') +export const isMainThread = Boolean( + globalThis.window && + globalThis.top === globalThis.window && + globalThis.window === globalThis +) + +export const state = { + isMainThread, + + parentPort: isMainThread + ? null + : Object.create(MessagePort.prototype), + + mainPort: isMainThread + ? Object.create(MessagePort.prototype) + : null, + + workerData: null, + url: null, + env: {}, + id: 0 +} + +if (!isMainThread) { + process.exit = (code) => { + globalThis.postMessage({ + worker_threads: { process: { exit: { code } } } + }) + } + + globalThis.addEventListener('message', onMainThreadMessage) + globalThis.addEventListener('error', (event) => { + propagateWorkerError( + event.error ?? + new Error( + event.reason?.message ?? + event.reason ?? + 'An unknown error occurred' + ) + ) + }) + + globalThis.addEventListener('unhandledrejection', (event) => { + propagateWorkerError( + event.error ?? + new Error( + event.reason?.message ?? + event.reason ?? + 'An unknown error occurred' + ) + ) + }) +} + +function propagateWorkerError (err) { + globalThis.postMessage({ + worker_threads: { + error: { + name: err.name, + type: err.constructor.name, + code: err.code ?? undefined, + stack: err.stack, + message: err.message + } + } + }) +} + +async function onMainThreadMessage (event) { + const request = event.data?.worker_threads ?? {} + + if (request.workerData) { + state.workerData = request.workerData + } + + if (request.init && request.init.id && request.init.url) { + state.id = request.init.id + state.url = request.init.url + + if ( + request.init.process?.env && + typeof request.init.process.env === 'object' + ) { + for (const key in request.init.process.env) { + process.env[key] = request.init.process.env[key] + } + } else if (request.init.process?.env === SHARE_ENV.toString()) { + env.addEventListener('set', (event) => { + globalThis.postMessage({ + worker_threads: { + process: { + env: { + type: event.type, + key: event.key, + value: event.value + } + } + } + }) + }) + + env.addEventListener('delete', (event) => { + globalThis.postMessage({ + worker_threads: { + process: { + env: { + type: event.type, + key: event.key + } + } + } + }) + }) + } + + if (request.init.process?.stdin === true) { + process.stdin = new Readable() + } + + if (request.init.process?.stdout === true) { + process.stdout = new Writable({ + write (data, cb) { + const transfer = getTransferables(data) + globalThis.postMessage({ + worker_threads: { + process: { + stdout: { data } + } + } + }, { transfer }) + + cb(null) + } + }) + } + + if (request.init.process?.stderr === true) { + process.stderr = new Writable({ + write (data, cb) { + const transfer = getTransferables(data) + globalThis.postMessage({ + worker_threads: { + process: { + stderr: { data } + } + } + }, { transfer }) + + cb(null) + } + }) + } + + if (request.init.eval === true) { + state.url = '' + await vm.runInThisContext(request.init.url).catch(propagateWorkerError) + } else { + await import(state.url).catch(propagateWorkerError) + } + + globalThis.postMessage({ + worker_threads: { online: { id: state.id } } + }) + } + + if (request.env && typeof request.env === 'object') { + for (const key in request.env) { + state.env[key] = request.env + } + } + + if (/set|delete/.test(request.process?.env?.type ?? '')) { + if (request.process.env.type === 'set') { + Reflect.set(env, request.process.env.key, request.process.env.value) + } else if (request.process.env.type === 'delete') { + Reflect.deleteProperty(env, request.process.env.key) + } + } + + if (request.process?.stdin?.data && process.stdin) { + process.stdin.push(request.process.stdin.data) + } + + if (event.data?.worker_threads) { + event.stopImmediatePropagation() + return false + } +} + +if (state.parentPort) { + let onmessageerror = null + let onmessage = null + Object.defineProperties(state.parentPort, { + postMessage: { + configurable: false, + enumerable: false, + value: globalThis.top + ? globalThis.top.postMessage.bind(globalThis.top) + : globalThis.postMessage.bind(globalThis) + }, + + close: { + configurable: false, + enumerable: false, + value () {} + }, + + start: { + configurable: false, + enumerable: false, + value () {} + }, + + onmessage: { + enumerable: true, + get: () => onmessage, + set: (value) => { + if (typeof onmessage === 'function') { + globalThis.removeEventListener('message', onmessage) + } + + onmessage = null + + if (typeof value === 'function') { + onmessage = value + globalThis.addEventListener('message', onmessage) + } + } + }, + + onmessageerror: { + enumerable: true, + get: () => onmessageerror, + set: (value) => { + if (typeof onmessageerror === 'function') { + globalThis.removeEventListener('messageerror', onmessageerror) + } + + onmessageerror = null + + if (typeof value === 'function') { + onmessageerror = value + globalThis.addEventListener('messageerror', onmessageerror) + } + } + } + }) +} else if (state.mainPort) { + let onmessageerror = null + let onmessage = null + Object.defineProperties(state.mainPort, { + addEventListener: { + configurable: false, + enumerable: false, + value (...args) { + return globalThis.addEventListener(...args) + } + }, + + removeEventListener: { + configurable: false, + enumerable: false, + value (...args) { + return globalThis.removeEventListener(...args) + } + }, + + dispatchEvent: { + configurable: false, + enumerable: false, + value (...args) { + return globalThis.dispatchEvent(...args) + } + }, + + onmessage: { + enumerable: true, + get: () => onmessage, + set: (value) => { + if (typeof onmessage === 'function') { + globalThis.removeEventListener('message', onmessage) + } + + onmessage = null + + if (typeof value === 'function') { + onmessage = value + globalThis.addEventListener('message', onmessage) + } + } + }, + + onmessageerror: { + enumerable: true, + get: () => onmessageerror, + set: (value) => { + if (typeof onmessageerror === 'function') { + globalThis.removeEventListener('messageerror', onmessageerror) + } + + onmessageerror = null + + if (typeof value === 'function') { + onmessageerror = value + globalThis.addEventListener('messageerror', onmessageerror) + } + } + } + }) +} + +export default { state } diff --git a/bin/android-functions.sh b/bin/android-functions.sh index 951ca26feb..d39e4c41fc 100755 --- a/bin/android-functions.sh +++ b/bin/android-functions.sh @@ -450,9 +450,9 @@ function android_arch_includes() { #get abi specific includes and sysroot local arch=$1 local include=( - "-I$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include/$(android_arch "$arch")-linux-android$(android_eabi "$arch")" - "-I$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include" - "--sysroot=$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/$(android_arch "$arch")-linux-android$(android_eabi "$arch")" + "-I$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/$(android_host_platform "$(host_os)")-x86_64/sysroot/usr/include/$(android_arch "$arch")-linux-android$(android_eabi "$arch")" + "-I$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/$(android_host_platform "$(host_os)")-x86_64/sysroot/usr/include" + "--sysroot=$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/$(android_host_platform "$(host_os)")-x86_64/sysroot/usr/lib/$(android_arch "$arch")-linux-android$(android_eabi "$arch")" ) echo "${include[@]}" @@ -488,7 +488,7 @@ function android_supported_abis() { export ANDROID_DEPS_ERROR declare ANDROID_PLATFORM="34" -declare NDK_VERSION="26.0.10792818" +declare NDK_VERSION="26.1.10909125" export BUILD_ANDROID @@ -830,7 +830,9 @@ function android_fte() { fi NDK_BUILD="$ANDROID_HOME/ndk/$NDK_VERSION/ndk-build$(use_bin_ext ".cmd")" + NDK_TOOLCHAINS="$ANDROID_HOME/ndk/$NDK_VERSION/toolchains" export NDK_BUILD + export NDK_TOOLCHAINS rc=$? [[ -n "$set_exit_code" ]] && exit $rc diff --git a/bin/build-runtime-library.sh b/bin/build-runtime-library.sh index d5f0283bac..a06358efc1 100755 --- a/bin/build-runtime-library.sh +++ b/bin/build-runtime-library.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash declare root="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)" -declare clang="${CXX:-"$CLANG"}" +declare clang="${CXX:-"${CLANG:-"$(which clang++)"}"}" declare cache_path="$root/build/cache" source "$root/bin/functions.sh" @@ -10,13 +10,13 @@ export CPU_CORES=$(set_cpu_cores) declare args=() declare pids=() declare force=0 +declare d="" declare arch="$(host_arch)" declare host_arch=$arch declare host=$(host_os) declare platform="desktop" -declare d="" if [[ "$host" == "Win32" ]]; then # We have to differentiate release and debug for Win32 if [[ -n "$DEBUG" ]]; then @@ -80,6 +80,7 @@ while (( $# > 0 )); do export TARGET_IPHONE_SIMULATOR=1 elif [[ "$1" = "android" ]]; then platform="$1"; + d="" export TARGET_OS_ANDROID=1 else platform="$1"; @@ -88,11 +89,6 @@ while (( $# > 0 )); do continue fi - # Don't rebuild if header mtimes are newer than .o files - Be sure to manually delete affected assets as required - if [[ "$arg" == "--ignore-header-mtimes" ]]; then - ignore_header_mtimes=1; continue - fi - args+=("$arg") done @@ -100,18 +96,24 @@ declare objects=() declare sources=( $(find "$root"/src/app/*.cc) $(find "$root"/src/core/*.cc) - $(find "$root"/src/ipc/*.cc) + $(find "$root"/src/core/modules/*.cc) $(find "$root"/src/extension/*.cc) + $(find "$root"/src/ipc/*.cc) + $(find "$root"/src/platform/*.cc) + $(find "$root"/src/serviceworker/*.cc) + #$(find "$root"/src/sharedworker/*.cc) + "$root/build/llama/common/common.cpp" + "$root/build/llama/common/sampling.cpp" + "$root/build/llama/common/json-schema-to-grammar.cpp" + "$root/build/llama/common/grammar-parser.cpp" + "$root/build/llama/llama.cpp" + "$root/src/window/manager.cc" "$root/src/window/dialog.cc" + "$root/src/window/hotkey.cc" ) -declare test_headers=() declare cflags -if [[ "$platform" = "desktop" ]]; then - sources+=("$root/src/window/hotkey.cc") -fi - if [[ "$platform" = "android" ]]; then source "$root/bin/android-functions.sh" android_fte @@ -123,24 +125,28 @@ if [[ "$platform" = "android" ]]; then clang="$(android_clang "$ANDROID_HOME" "$NDK_VERSION" "$host" "$host_arch" "++")" clang_target="$(android_clang_target "$arch")" - sources+=("$root/src/process/unix.cc") + sources+=("$root/src/core/process/unix.cc") + sources+=($(find "$root/src/platform/android"/*.cc)) + sources+=("$root/src/window/android.cc") elif [[ "$host" = "Darwin" ]]; then sources+=("$root/src/window/apple.mm") - if (( TARGET_OS_IPHONE)) || (( TARGET_IPHONE_SIMULATOR )); then - cflags=("-sdk" "iphoneos" "$clang") - clang="xcrun" + + if (( TARGET_OS_IPHONE)); then + clang="xcrun -sdk iphoneos "$clang"" + elif (( TARGET_IPHONE_SIMULATOR )); then + clang="xcrun -sdk iphonesimulator "$clang"" else - sources+=("$root/src/process/unix.cc") + sources+=("$root/src/core/process/unix.cc") fi elif [[ "$host" = "Linux" ]]; then sources+=("$root/src/window/linux.cc") - sources+=("$root/src/process/unix.cc") + sources+=("$root/src/core/process/unix.cc") elif [[ "$host" = "Win32" ]]; then sources+=("$root/src/window/win.cc") - sources+=("$root/src/process/win.cc") + sources+=("$root/src/core/process/win.cc") fi -cflags+=($("$root/bin/cflags.sh")) +cflags+=($(ARCH="$arch" "$root/bin/cflags.sh")) if [[ "$platform" = "android" ]]; then cflags+=("$clang_target ${android_includes[*]}") @@ -154,44 +160,134 @@ cd "$(dirname "$output_directory")" echo "# building runtime static libary ($arch-$platform)" for source in "${sources[@]}"; do declare src_directory="$root/src" + declare object="${source/.cc/$d.o}" - declare object="${object/$src_directory/$output_directory}" + object="${object/.cpp/$d.o}" + + declare build_dir="$root/build" + + if [[ "$object" =~ ^"$src_directory" ]]; then + object="${object/$src_directory/$output_directory}" + else + object="${object/$build_dir/$output_directory}" + fi + objects+=("$object") done -if [[ -z "$ignore_header_mtimes" ]]; then - test_headers+="$(find "$root/src"/core/*.hh)" -fi +objects+=("$output_directory/llama/build-info.o") + +function generate_llama_build_info () { + build_number="0" + build_commit="unknown" + build_compiler="unknown" + build_target="unknown" + + if out=$(git rev-list --count HEAD); then + # git is broken on WSL so we need to strip extra newlines + build_number=$(printf '%s' "$out" | tr -d '\n') + fi + + if out=$(git rev-parse --short HEAD); then + build_commit=$(printf '%s' "$out" | tr -d '\n') + fi + + if [[ "$(host_os)" == "Win32" ]]; then + if out=$("$clang" --version | head -1); then + build_compiler="$out" + fi + + if out=$("$clang" -dumpmachine); then + build_target="$out" + fi + else + if out=$(eval "$clang" --version | head -1); then + build_compiler="$out" + fi + + if out=$(eval "$clang" -dumpmachine); then + build_target="$out" + fi + fi + + echo "# generating llama build info" + declare source="$output_directory/llama/build-info.cpp" + + cat > $source << LLAMA_BUILD_INFO + int LLAMA_BUILD_NUMBER = $build_number; + char const *LLAMA_COMMIT = "$build_commit"; + char const *LLAMA_COMPILER = "$build_compiler"; + char const *LLAMA_BUILD_TARGET = "$build_target"; +LLAMA_BUILD_INFO + + quiet "$clang" "${cflags[@]}" -c $source -o ${source/cpp/o} || onsignal +} + +function build_linux_desktop_extension_object () { + declare source="$root/src/desktop/extension/linux.cc" + declare destination="$root/build/$arch-$platform/objects/extensions/linux.o" + + mkdir -p "$(dirname "$destination")" + + if ! test -f "$object" || (( $(stat_mtime "$source") > $(stat_mtime "$destination") )); then + quiet $clang "${cflags[@]}" -DSOCKET_RUNTIME_DESKTOP_EXTENSION=1 -c "$source" -o "$destination" || onsignal + return $? + fi + + return 0 +} function main () { trap onsignal INT TERM local i=0 local max_concurrency=$CPU_CORES - local newest_mtime=0 - newest_mtime="$(latest_mtime ${test_headers[@]})" - mkdir -p "$output_directory/include" cp -rf "$root/include"/* "$output_directory/include" + rm -f "$output_directory/include/socket/_user-config-bytes.hh" + + generate_llama_build_info || return $? + + if [[ "$host" = "Linux" ]] && [[ "$platform" = "desktop" ]]; then + build_linux_desktop_extension_object || return $? + fi for source in "${sources[@]}"; do - if (( i++ > max_concurrency )); then - for pid in "${pids[@]}"; do - wait "$pid" 2>/dev/null - done - i=0 + if (( ${#pids[@]} > max_concurrency )); then + wait "${pids[0]}" 2>/dev/null + pids=("${pids[@]:1}") fi { declare src_directory="$root/src" declare object="${source/.cc/$d.o}" - declare object="${object/$src_directory/$output_directory}" + declare header="${source/.cc/.hh}" + declare build_dir="$root/build" + + object="${object/.cpp/$d.o}" + header="${header/.cpp/.h}" - if (( force )) || ! test -f "$object" || (( newest_mtime > $(stat_mtime "$object") )) || (( $(stat_mtime "$source") > $(stat_mtime "$object") )); then + if [[ "$object" =~ ^"$src_directory" ]]; then + object="${object/$src_directory/$output_directory}" + else + object="${object/$build_dir/$output_directory}" + fi + + if + (( force )) || + ! test -f "$object" || + (( $(stat_mtime "$source") > $(stat_mtime "$object") )) || + (( $(stat_mtime "$header") > $(stat_mtime "$source") )); + then mkdir -p "$(dirname "$object")" + echo "# compiling object ($arch-$platform) $(basename "$source")" quiet "$clang" "${cflags[@]}" -c "$source" -o "$object" || onsignal echo "ok - built ${source/$src_directory\//} -> ${object/$output_directory\//} ($arch-$platform)" + + if (( $(stat_mtime "$header") > $(stat_mtime "$source") )); then + touch "$source" + fi fi } & pids+=($!) done @@ -242,8 +338,10 @@ function main () { fi fi - if [[ "$host" = "Linux" ]] && [[ "$platform" = "desktop" ]]; then - "$root/bin/generate-socket-runtime-pkg-config.sh" + if [[ "$platform" = "desktop" ]]; then + if [[ "$host" = "Linux" ]] || [[ "$host" = "Darwin" ]]; then + "$root/bin/generate-socket-runtime-pkg-config.sh" + fi fi if [[ "$platform" == "android" ]]; then diff --git a/bin/cflags.sh b/bin/cflags.sh index 2c054a8951..05b347bfd6 100755 --- a/bin/cflags.sh +++ b/bin/cflags.sh @@ -8,11 +8,16 @@ declare IOS_SIMULATOR_VERSION_MIN="${IOS_SIMULATOR_VERSION_MIN:-$IPHONEOS_VERSIO declare cflags=() declare arch="$(uname -m | sed 's/aarch64/arm64/g')" arch=${ARCH:-$arch} -declare host="$(uname -s)" +declare host="${TARGET_HOST:-"$(uname -s)"}" declare platform="desktop" declare ios_sdk_path="" +cflags+=( + $CFLAGS + $CXXFLAGS +) + if [[ "$host" = "Linux" ]]; then if [ -n "$WSL_DISTRO_NAME" ] || uname -r | grep 'Microsoft'; then host="Win32" @@ -47,16 +52,17 @@ else fi cflags+=( - $CFLAG - $CXXFLAGS -std=c++2a - -fvisibility=hidden + -ferror-limit=6 -I"$root/include" -I"$root/build/uv/include" + -I"$root/build" + -I"$root/build/llama" + -I"$root/build/llama/common" -I"$root/build/include" - -DSSC_BUILD_TIME="$(date '+%s')" - -DSSC_VERSION_HASH=$(git rev-parse --short=8 HEAD) - -DSSC_VERSION=$(cat "$root/VERSION.txt") + -DSOCKET_RUNTIME_BUILD_TIME="$(date '+%s')" + -DSOCKET_RUNTIME_VERSION_HASH=$(git rev-parse --short=8 HEAD) + -DSOCKET_RUNTIME_VERSION=$(cat "$root/VERSION.txt") ) if (( TARGET_OS_IPHONE )) || (( TARGET_IPHONE_SIMULATOR )); then @@ -96,12 +102,38 @@ if (( !TARGET_OS_ANDROID && !TARGET_ANDROID_EMULATOR )); then -D_DLL -DWIN32 -DWIN32_LEAN_AND_MEAN - -Xlinker /NODEFAULTLIB:libcmt + "-Xlinker /NODEFAULTLIB:libcmt" -Wno-nonportable-include-path ) if [[ -n "$DEBUG" ]]; then cflags+=("-D_DEBUG") fi + + ## TODO(@jwerle): figure this out for macOS + if [[ "$(uname -s)" == "Linux" ]]; then + cflags+=( + "-fdeclspec" + "-I/usr/share/mingw-w64/include" + "-I/usr/include/x86_64-linux-gnu/c++/12/" + "-I/usr/lib/gcc/x86_64-w64-mingw32/10-win32/include/" + "-I/usr/lib/gcc/x86_64-w64-mingw32/10-posix/include/c++" + "-I/usr/lib/gcc/x86_64-w64-mingw32/10-win32/include/c++" + "-I/usr/lib/gcc/x86_64-w64-mingw32/10-win32/include/c++/x86_64-w64-mingw32" + "-I/usr/lib/gcc/x86_64-w64-mingw32/10-win32/include/c++/backward" + "-DWIN32" + "-DWINVER=0x0A00" + "-D_WIN32_WINNT=0x0A00" + "-D_WIN32" + "-D_WIN64" + "-D_MSC_VER=1940" + "-D_MSC_FULL_VER=193933519" + "-D_MSC_BUILD=0" + "-D_GLIBCXX_HAS_GTHREADS=1" + "-DSOCKET_RUNTIME_CROSS_COMPILED_HOST=1" + "-DSOCKET_RUNTIME_PLATFORM_WANTS_MINGW=1" + $(pkg-config gthread-2.0 --cflags) + ) + fi fi fi @@ -113,6 +145,7 @@ done if [[ -n "$DEBUG" ]]; then cflags+=("-g") cflags+=("-O0") + cflags+=("-DSOCKET_RUNTIME_BUILD_DEBUG=1") else cflags+=("-Os") fi diff --git a/bin/clean.sh b/bin/clean.sh index 7916108d6c..f27b0f62da 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -14,6 +14,10 @@ elif (( TARGET_IPHONE_SIMULATOR )); then platform="iPhoneSimulator" fi +if (( $# == 0 )); then + do_clean_env_only=1 +fi + while (( $# > 0 )); do declare arg="$1"; shift if [[ "$arg" = "--arch" ]]; then @@ -23,6 +27,7 @@ while (( $# > 0 )); do if [[ "$arg" = "--full" ]]; then do_full_clean=1 + do_clean_env_only=1 continue elif [[ "$arg" = "--platform" ]]; then if [[ "$1" = "ios" ]] || [[ "$1" = "iPhoneOS" ]] || [[ "$1" = "iphoneos" ]]; then diff --git a/bin/docs-generator/config.js b/bin/docs-generator/config.js index 24af8184d0..d1aad7c3a1 100644 --- a/bin/docs-generator/config.js +++ b/bin/docs-generator/config.js @@ -37,14 +37,16 @@ function parseIni (iniText) { function createConfigMd (sections) { let md = '<!-- This file is generated by bin/docs-generator/config.js -->\n' md += '<!-- Do not edit this file directly. -->\n\n' - md += '# Configuration basics\n' + md += '# Configuration\n' + md += '## Overview\n' md += ` -The configuration file is a simple INI \`socket.ini\` file in the root of the project. -The file is read on startup and the values are used to configure the project. -Sometimes it's useful to overide the values in \`socket.ini\` or keep some of the values local (e.g. \`[ios] simulator_device\`) -or secret (e.g. \`[ios] codesign_identity\`, \`[ios] provisioning_profile\`, etc.) -This can be done by creating a file called \`.sscrc\` in the root of the project. -It is possible to override both Command Line Interface (CLI) and Configuration File (INI) options. + +The configuration file is an INI file (\`socket.ini\`) in the root of every Socket runtime project. +The file is read at compile time. Sometimes it's useful to overide its values or keep some of the +values locally, only on your computer (e.g. \`[ios] simulator_device\`) or secrets +(e.g. \`[ios] codesign_identity\`, \`[ios] provisioning_profile\`, etc.); this can be done by +creating a file called \`.sscrc\` in the root of the project. It is possible to override both +Command Line Interface (CLI) and Configuration File (INI) options. Example: @@ -69,7 +71,7 @@ platform = ios ; override the \`ssc build --platform\` CLI option [settings.ios] ; override the \`[ios]\` section in \`socket.ini\` codesign_identity = "iPhone Developer: John Doe (XXXXXXXXXX)" -distribution_method = "ad-hoc" +distribution_method = "release-testing" provisioning_profile = "johndoe.mobileprovision" simulator_device = "iPhone 15" \`\`\` @@ -83,7 +85,7 @@ simulator_device = "iPhone 15" ` md += '\n' Object.entries(sections).forEach(([sectionName, settings]) => { - md += `# \`${sectionName}\`\n` + md += `### \`${sectionName}\`\n` md += '\n' md += 'Key | Default Value | Description\n' md += ':--- | :--- | :---\n' diff --git a/bin/functions.sh b/bin/functions.sh index 9698952e9a..1f2fd808f1 100755 --- a/bin/functions.sh +++ b/bin/functions.sh @@ -22,6 +22,11 @@ fi function stat_mtime () { stat $stat_format_arg $stat_mtime_spec "$1" 2>/dev/null + local rc=$? + if (( rc != 0 )); then + echo 0 + fi + return $rc } function latest_mtime() { @@ -93,9 +98,17 @@ function quiet () { declare command="$1"; shift if [ -n "$VERBOSE" ]; then echo "$command" "$@" + if [[ "$(host_os)" != "Win32" ]]; then + eval "$command $@" + else "$command" "$@" + fi else - "$command" "$@" > /dev/null 2>&1 + if [[ "$(host_os)" != "Win32" ]]; then + eval "$command $@" > /dev/null 2>&1 + else + "$command" "$@" > /dev/null 2>&1 + fi fi return $? diff --git a/bin/generate-compile-flags-txt.sh b/bin/generate-compile-flags-txt.sh index b3379a96b6..cb94dffe30 100755 --- a/bin/generate-compile-flags-txt.sh +++ b/bin/generate-compile-flags-txt.sh @@ -6,6 +6,8 @@ declare platform="desktop" declare force=0 declare args=() +source "$root/bin/android-functions.sh" + if (( TARGET_OS_IPHONE )); then arch="arm64" platform="iPhoneOS" @@ -43,12 +45,22 @@ while (( $# > 0 )); do arch="aarch64" platform="Android"; export TARGET_OS_ANDROID=1 + android_fte > /dev/null args+=("-U__APPLE__") + args+=("-D__ANDROID__=1") + args+=("--sysroot=$NDK_TOOLCHAINS/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m )/sysroot") + args+=("-I$NDK_TOOLCHAINS/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m )/sysroot/usr/include/$(uname -m)-$(uname -s | tr '[:upper:]' '[:lower:]')-android") + args+=("-I$NDK_TOOLCHAINS/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m )/sysroot/usr/include/c++/v1") elif [[ "$1" = "android-emulator" ]] || [[ "$1" = "AndroidEmulator" ]]; then arch="x86_64" platform="AndroidEmulator"; export TARGET_ANDROID_EMULATOR=1 + android_fte > /dev/null args+=("-U__APPLE__") + args+=("-D__ANDROID__=1") + args+=("--sysroot=$NDK_TOOLCHAINS/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m )/sysroot") + args+=("-I$NDK_TOOLCHAINS/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m )/sysroot/usr/include/$(uname -m)-$(uname -s | tr '[:upper:]' '[:lower:]')-android") + args+=("-I$NDK_TOOLCHAINS/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m )/sysroot/usr/include/c++/v1") else platform="$1"; fi diff --git a/bin/generate-docs.js b/bin/generate-docs.js index 132befba36..8d94d4d92d 100755 --- a/bin/generate-docs.js +++ b/bin/generate-docs.js @@ -8,7 +8,15 @@ import { generateCli } from './docs-generator/cli.js' const VERSION = `v${(await fs.readFile('./VERSION.txt', 'utf8')).trim()}` const isCurrentTag = execSync('git describe --tags --always').toString().trim() === VERSION -const tagNotPresentOnRemote = execSync(`git ls-remote --tags origin ${VERSION}`).toString().length === 0 + +let tagNotPresentOnRemote = false +if (!isCurrentTag) { + try { + tagNotPresentOnRemote = execSync(`git ls-remote --tags origin ${VERSION}`).toString().length === 0 + } catch (err) { + console.warn(err.message) + } +} const gitTagOrBranch = (isCurrentTag || tagNotPresentOnRemote) ? VERSION : 'master' diff --git a/bin/generate-gradle-files.sh b/bin/generate-gradle-files.sh index b1e9e5a2f8..9f080a5b52 100755 --- a/bin/generate-gradle-files.sh +++ b/bin/generate-gradle-files.sh @@ -10,49 +10,65 @@ rm -f "$root/build.gradle" "$root/gradle.properties" ## build.gradle cat > "$root/build.gradle" << GRADLE buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '1.9.20' repositories { google() mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:\$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-android-extensions:\$kotlin_version" } } allprojects { + ext.kotlin_version = '1.9.20' repositories { google() mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } } } +apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'com.android.application' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' android { compileSdkVersion 34 - ndkVersion "26.0.10792818" + ndkVersion "26.1.10909125" flavorDimensions "default" defaultConfig { - applicationId "__BUNDLE_IDENTIFIER__" + applicationId "socket.runtime" minSdkVersion 24 targetSdkVersion 34 versionCode 1 versionName "0.0.1" } + + sourceSets { + main { + java { + srcDir 'src' + } + } + } } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.73' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:\$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'androidx.fragment:fragment-ktx:1.7.1' + implementation 'androidx.lifecycle:lifecycle-process:2.7.0' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core-ktx:2.2.0' - implementation 'androidx.webkit:webkit:1.8.0' + implementation 'androidx.core:core-ktx:1.13.0' + implementation 'androidx.webkit:webkit:1.9.0' } GRADLE @@ -68,4 +84,4 @@ android.experimental.legacyTransform.forceNonIncremental=true kotlin.code.style=official GRADLE -gradle wrapper +gradle wrapper && "$root/gradlew" androidDependencies diff --git a/bin/generate-typescript-typings.sh b/bin/generate-typescript-typings.sh index ec1b2cd330..6a2b2edc75 100755 --- a/bin/generate-typescript-typings.sh +++ b/bin/generate-typescript-typings.sh @@ -7,7 +7,7 @@ cd "$root" || exit $? "$root/node_modules/.bin/tsc" --emitDeclarationOnly --module es2022 --outFile api/index.tmp || exit $? cat api/index.tmp.d.ts api/global.d.ts \ - | sed 's/declare module "\(.*\)"/declare module "socket:\1"/g' \ + | sed 's/declare module "\(.*\)"/\ndeclare module "socket:\1"/g' \ | sed 's/from "\(.*\)"/from "socket:\1"/g' \ | sed 's/import("\(.*\)")/import("socket:\1")/g' \ | sed 's/namespace \_\_\_.*$//g' > api/index.d.ts \ diff --git a/bin/install.sh b/bin/install.sh index a74be3a111..c2d72f4779 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -220,12 +220,15 @@ function _build_cli { # local libs=($("echo" -l{socket-runtime})) local libs="" + # + # Add libuv, socket-runtime and llama + # if [[ "$(uname -s)" != *"_NT"* ]]; then - libs=($("echo" -l{uv,socket-runtime})) + libs=($("echo" -l{uv,llama,socket-runtime})) fi if [[ -n "$VERBOSE" ]]; then - echo "# cli libs: $libs, $(uname -s)" + echo "# cli libs: ${libs[@]}, $(uname -s)" fi local ldflags=($("$root/bin/ldflags.sh" --arch "$arch" --platform $platform ${libs[@]})) @@ -281,6 +284,7 @@ function _build_cli { test_sources+=("$static_libs") elif [[ "$(uname -s)" == "Linux" ]]; then static_libs+=("$BUILD_DIR/$arch-$platform/lib/libuv.a") + static_libs+=("$BUILD_DIR/$arch-$platform/lib/libllama.a") static_libs+=("$BUILD_DIR/$arch-$platform/lib/libsocket-runtime.a") fi @@ -336,14 +340,14 @@ function _build_runtime_library() { } function _get_web_view2() { - if [[ "$(uname -s)" != *"_NT"* ]]; then + if [[ "$(uname -s)" != *"_NT"* ]] && [ -z "$FORCE_WEBVIEW2_DOWNLOAD" ]; then return fi local arch="$(host_arch)" local platform="desktop" - if test -f "$BUILD_DIR/$arch-$platform/lib$d/WebView2LoaderStatic.lib"; then + if [ -z "$FORCE_WEBVIEW2_DOWNLOAD" ] && test -f "$BUILD_DIR/$arch-$platform/lib$d/WebView2LoaderStatic.lib"; then echo "$BUILD_DIR/$arch-$platform/lib$d/WebView2LoaderStatic.lib exists." return fi @@ -353,7 +357,7 @@ function _get_web_view2() { echo "# Downloading Webview2" - curl -L https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2357-prerelease --output "$tmp/webview2.zip" + curl -L https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2592.51 --output "$tmp/webview2.zip" cd "$tmp" || exit 1 unzip -q "$tmp/webview2.zip" mkdir -p "$BUILD_DIR/include" @@ -512,7 +516,14 @@ function _prepare { mv "$tempmkl" "$BUILD_DIR/uv/CMakeLists.txt" fi - die $? "not ok - unable to clone. See trouble shooting guide in the README.md file" + die $? "not ok - unable to clone libuv. See trouble shooting guide in the README.md file" + fi + + if [ ! -d "$BUILD_DIR/llama" ]; then + git clone --depth=1 https://github.com/socketsupply/llama.cpp.git "$BUILD_DIR/llama" > /dev/null 2>&1 + # rm -rf $BUILD_DIR/llama/.git + + die $? "not ok - unable to clone llama. See trouble shooting guide in the README.md file" fi echo "ok - directories prepared" @@ -530,7 +541,7 @@ function _install { fi fi - # TODO(@mribbons): Set lib types based on platform, after mobile CI is working + # TODO(@heapwolf): Set lib types based on platform, after mobile CI is working if test -d "$BUILD_DIR/$arch-$platform/objects"; then echo "# copying objects to $SOCKET_HOME/objects/$arch-$platform" @@ -556,8 +567,12 @@ function _install { echo "# copying libraries to $SOCKET_HOME/lib$_d/$arch-$platform" rm -rf "$SOCKET_HOME/lib$_d/$arch-$platform" mkdir -p "$SOCKET_HOME/lib$_d/$arch-$platform" + if [[ "$platform" != "android" ]]; then cp -rfp "$BUILD_DIR/$arch-$platform"/lib$_d/*.a "$SOCKET_HOME/lib$_d/$arch-$platform" + if [[ "$host" == "Darwin" ]] && [[ "$platform" != "desktop" ]]; then + cp -rfp "$BUILD_DIR/$arch-$platform"/lib/*.metallib "$SOCKET_HOME/lib/$arch-$platform" + fi fi if [[ "$host" == "Win32" ]] && [[ "$platform" == "desktop" ]]; then cp -rfp "$BUILD_DIR/$arch-$platform"/lib$_d/*.lib "$SOCKET_HOME/lib$_d/$arch-$platform" @@ -571,14 +586,14 @@ function _install { exit 1 fi - if [ "$host" == "Linux" ]; then - echo "# copying pkgconfig to $SOCKET_HOME/pkgconfig" - rm -rf "$SOCKET_HOME/pkgconfig" - mkdir -p "$SOCKET_HOME/pkgconfig" - cp -rfp "$BUILD_DIR/$arch-$platform/pkgconfig"/* "$SOCKET_HOME/pkgconfig" - fi - if [ "$platform" == "desktop" ]; then + if [ "$host" == "Linux" ] || [ "$host" == "Darwin" ]; then + echo "# copying pkgconfig to $SOCKET_HOME/pkgconfig" + rm -rf "$SOCKET_HOME/pkgconfig" + mkdir -p "$SOCKET_HOME/pkgconfig" + cp -rfp "$BUILD_DIR/$arch-desktop/pkgconfig"/* "$SOCKET_HOME/pkgconfig" + fi + echo "# copying js api to $SOCKET_HOME/api" mkdir -p "$SOCKET_HOME/api" cp -frp "$root"/api/* "$SOCKET_HOME/api" @@ -591,6 +606,19 @@ function _install { mkdir -p "$SOCKET_HOME/include" cp -rfp "$BUILD_DIR"/uv/include/* "$SOCKET_HOME/include" cp -rfp "$root"/include/* "$SOCKET_HOME/include" + rm -f "$SOCKET_HOME/include/socket/_user-config-bytes.hh" + + mkdir -p "$SOCKET_HOME/include/llama" + for header in $(find "$root/build/llama" -name *.h); do + if [[ "$header" =~ examples/ ]]; then continue; fi + if [[ "$header" =~ tests/ ]]; then continue; fi + + local llama_build_dir="$root/build/llama/" + local destination="$SOCKET_HOME/include/llama/${header/$llama_build_dir/}" + + mkdir -p "$(dirname "$destination")" + cp -f "$header" "$destination" + done if [[ -f "$root/$SSC_ENV_FILENAME" ]]; then if [[ -f "$SOCKET_HOME/$SSC_ENV_FILENAME" ]]; then @@ -698,7 +726,7 @@ function _compile_libuv_android { local max_concurrency=$CPU_CORES build_static=0 declare base_lib="libuv" - declare static_library="$root/build/$arch-$platform/lib$d/$base_lib$d.a" + declare static_library="$root/build/$arch-$platform/lib/$base_lib.a" for source in "${sources[@]}"; do if (( i++ > max_concurrency )); then @@ -708,7 +736,7 @@ function _compile_libuv_android { i=0 fi - declare object="${source/.c/$d.o}" + declare object="${source/.c/.o}" object="$(basename "$object")" objects+=("$output_directory/$object") @@ -763,6 +791,153 @@ function _compile_libuv_android { fi } +function _compile_llama_metal { + target=$1 + hosttarget=$1 + platform=$2 + + if [ -z "$target" ]; then + target="$(host_arch)" + platform="desktop" + fi + + echo "# building METAL for $platform ($target) on $host..." + STAGING_DIR="$BUILD_DIR/$target-$platform/llama" + + if [ ! -d "$STAGING_DIR" ]; then + mkdir -p "$STAGING_DIR" + cp -r "$BUILD_DIR"/llama/* "$STAGING_DIR" + cd "$STAGING_DIR" || exit 1 + else + cd "$STAGING_DIR" || exit 1 + fi + + local sdk="iphoneos" + [[ "$platform" == "iPhoneSimulator" ]] && sdk="iphonesimulator" + + mkdir -p "$STAGING_DIR/build/" + mkdir -p ../lib + + xcrun -sdk $sdk metal -O3 -c ggml-metal.metal -o ggml-metal.air + xcrun -sdk $sdk metallib ggml-metal.air -o ../lib/default.metallib + rm *.air + + echo "ok - metal built for $platform" +} + +function _compile_llama { + target=$1 + hosttarget=$1 + platform=$2 + + if [ -z "$target" ]; then + target="$(host_arch)" + platform="desktop" + fi + + echo "# building llama.cpp for $platform ($target) on $host..." + STAGING_DIR="$BUILD_DIR/$target-$platform/llama" + + if [ ! -d "$STAGING_DIR" ]; then + mkdir -p "$STAGING_DIR" + cp -r "$BUILD_DIR"/llama/* "$STAGING_DIR" + cd "$STAGING_DIR" || exit 1 + else + cd "$STAGING_DIR" || exit 1 + fi + + local sdk="iphoneos" + [[ "$platform" == "iPhoneSimulator" ]] && sdk="iphonesimulator" + + mkdir -p "$STAGING_DIR/build/" + mkdir -p ../bin + + declare cmake_args=( + -DLLAMA_BUILD_TESTS=OFF + -DLLAMA_BUILD_SERVER=OFF + -DLLAMA_BUILD_EXAMPLES=OFF + ) + + if [ "$platform" == "desktop" ]; then + if [[ "$host" != "Win32" ]]; then + quiet command -v cmake + die $? "not ok - missing cmake, \"$(advice 'cmake')\"" + + quiet cmake -S . -B build -DCMAKE_INSTALL_PREFIX="$BUILD_DIR/$target-$platform" ${cmake_args[@]} + die $? "not ok - libllama.a (desktop)" + + quiet cmake --build build && + quiet cmake --build build -- -j"$CPU_CORES" && + quiet cmake --install build + die $? "not ok - libllama.a (desktop)" + else + if ! test -f "$BUILD_DIR/$target-$platform/lib$d/llama.lib"; then + local config="Release" + if [[ -n "$DEBUG" ]]; then + config="Debug" + fi + cd "$STAGING_DIR/build/" || exit 1 + quiet command -v cmake + die $? "not ok - missing cmake, \"$(advice 'cmake')\"" + quiet cmake -S .. -B . ${cmake_args[@]} + quiet cmake --build . --config $config + mkdir -p "$BUILD_DIR/$target-$platform/lib$d" + quiet echo "cp -up $STAGING_DIR/build/$config/llama.lib "$BUILD_DIR/$target-$platform/lib$d/llama.lib"" + cp -up "$STAGING_DIR/build/$config/llama.lib" "$BUILD_DIR/$target-$platform/lib$d/llama.lib" + if [[ -n "$DEBUG" ]]; then + cp -up "$STAGING_DIR"/build/$config/llama_a.pdb "$BUILD_DIR/$target-$platform/lib$d/llama_a.pdb" + fi; + fi + fi + + rm -f "$root/build/$(host_arch)-desktop/lib$d"/*.{so,la,dylib}* + return + elif [ "$platform" == "iPhoneOS" ] || [ "$platform" == "iPhoneSimulator" ]; then + # https://github.com/ggerganov/llama.cpp/discussions/4508 + + local ar="$(xcrun -sdk $sdk -find ar)" + local cc="$(xcrun -sdk $sdk -find clang)" + local cxx="$(xcrun -sdk $sdk -find clang++)" + local cflags="--target=$target-apple-ios -isysroot $PLATFORMPATH/$platform.platform/Developer/SDKs/$platform$SDKVERSION.sdk -m$sdk-version-min=$SDKMINVERSION -DLLAMA_METAL_EMBED_LIBRARY=ON" + + AR="$ar" CFLAGS="$cflags" CXXFLAGS="$cflags" CXX="$cxx" CC="$cc" make libllama.a + + if [ ! $? = 0 ]; then + die $? "not ok - Unable to compile libllama for '$platform'" + return + fi + elif [ "$platform" == "android" ]; then + if [[ "$host" == "Win32" ]]; then + echo "WARN - Building libllama for Android on Windows is not yet supported" + return + else + local android_includes=$(android_arch_includes "$1") + local host_arch="$(host_arch)" + local cc="$(android_clang "$ANDROID_HOME" "$NDK_VERSION" "$host" "$host_arch")" + local cxx="$(android_clang "$ANDROID_HOME" "$NDK_VERSION" "$host" "$host_arch" "++")" + local clang_target="$(android_clang_target "$target")" + local ar="$(android_ar "$ANDROID_HOME" "$NDK_VERSION" "$host" "$host_arch")" + local cflags=("$clang_target" -std=c++2a -g -pedantic "${android_includes[*]}") + + AR="$ar" CFLAGS="$cflags" CXXFLAGS="$cflags" CXX="$cxx" CC="$cc" make UNAME_S="Android" UNAME_M=".." UNAME_P="$1" LLAMA_FAST=1 libllama.a + + if [ ! $? = 0 ]; then + die $? "not ok - Unable to compile libllama for '$platform'" + return + fi + fi + fi + + if [[ "$host" != "Win32" ]]; then + cp libllama.a ../lib + die $? "not ok - Unable to compile libllama for '$platform'" + fi + + cd "$BUILD_DIR" || exit 1 + rm -f "$root/build/$target-$platform/lib$d"/*.{so,la,dylib}* + echo "ok - built llama for $target-$platform" +} + function _compile_libuv { target=$1 hosttarget=$1 @@ -801,7 +976,7 @@ function _compile_libuv { die $? "not ok - desktop configure" fi - quiet make clean + quiet make quiet make "-j$CPU_CORES" quiet make install fi @@ -812,6 +987,8 @@ function _compile_libuv { config="Debug" fi cd "$STAGING_DIR/build/" || exit 1 + quiet command -v cmake + die $? "not ok - missing cmake, \"$(advice 'cmake')\"" quiet cmake .. -DBUILD_TESTING=OFF -DLIBUV_BUILD_SHARED=OFF cd "$STAGING_DIR" || exit 1 quiet cmake --build "$STAGING_DIR/build/" --config $config @@ -839,6 +1016,7 @@ function _compile_libuv { export PLATFORM=$platform export CC="$(xcrun -sdk $sdk -find clang)" + export CXX="$(xcrun -sdk $sdk -find clang++)" export STRIP="$(xcrun -sdk $sdk -find strip)" export LD="$(xcrun -sdk $sdk -find ld)" export CPP="$CC -E" @@ -862,7 +1040,7 @@ function _compile_libuv { cd "$BUILD_DIR" || exit 1 rm -f "$root/build/$target-$platform/lib$d"/*.{so,la,dylib}* - echo "ok - built for $target" + echo "ok - built libuv for $target" } function _check_compiler_features { @@ -911,6 +1089,20 @@ cd "$BUILD_DIR" || exit 1 trap onsignal INT TERM +if [[ "$(uname -s)" == "Darwin" ]] && [[ -z "$NO_IOS" ]]; then + quiet xcode-select -p + die $? "not ok - xcode needs to be installed from the mac app store: https://apps.apple.com/us/app/xcode/id497799835" + + _compile_llama_metal arm64 iPhoneOS + _compile_llama_metal x86_64 iPhoneSimulator + _compile_llama_metal arm64 iPhoneSimulator +fi + +{ + _compile_llama + echo "ok - built llama for $platform ($target)" +} & _compile_llama_pid=$! + # Although we're passing -j$CPU_CORES on non Win32, we still don't get max utiliztion on macos. Start this before fat libs. { _compile_libuv @@ -930,9 +1122,14 @@ if [[ "$(uname -s)" == "Darwin" ]] && [[ -z "$NO_IOS" ]]; then _setSDKVersion iPhoneOS _compile_libuv arm64 iPhoneOS & pids+=($!) + _compile_llama arm64 iPhoneOS & pids+=($!) + _compile_libuv x86_64 iPhoneSimulator & pids+=($!) + _compile_llama x86_64 iPhoneSimulator & pids+=($!) + if [[ "$arch" = "arm64" ]]; then _compile_libuv arm64 iPhoneSimulator & pids+=($!) + _compile_llama arm64 iPhoneSimulator & pids+=($!) fi for pid in "${pids[@]}"; do wait "$pid"; done @@ -949,16 +1146,19 @@ fi if [[ "$host" != "Win32" ]]; then # non windows hosts uses make -j$CPU_CORES, wait for them to finish. - wait $_compile_libuv_pid + # wait $_compile_libuv_pid + wait $_compile_llama_pid fi if [[ -n "$BUILD_ANDROID" ]]; then for abi in $(android_supported_abis); do _compile_libuv_android "$abi" & pids+=($!) + _compile_llama "$abi" android & pids+=($!) done fi mkdir -p "$SOCKET_HOME"/uv/{src/unix,include} +cp -fr "$BUILD_DIR"/uv/LICENSE "$SOCKET_HOME"/uv/LICENSE cp -fr "$BUILD_DIR"/uv/src/*.{c,h} "$SOCKET_HOME"/uv/src cp -fr "$BUILD_DIR"/uv/src/unix/*.{c,h} "$SOCKET_HOME"/uv/src/unix die $? "not ok - could not copy headers" @@ -971,7 +1171,8 @@ _get_web_view2 if [[ "$host" == "Win32" ]]; then # Wait for Win32 lib uv build - wait $_compile_libuv_pid + # wait $_compile_libuv_pid + wait $_compile_llama_pid fi _check_compiler_features diff --git a/bin/ldflags.sh b/bin/ldflags.sh index 40097f8e14..abfdb1d146 100755 --- a/bin/ldflags.sh +++ b/bin/ldflags.sh @@ -109,6 +109,8 @@ if [[ "$host" = "Darwin" ]]; then ldflags+=("-framework" "Network") ldflags+=("-framework" "UniformTypeIdentifiers") ldflags+=("-framework" "WebKit") + ldflags+=("-framework" "Metal") + ldflags+=("-framework" "Accelerate") ldflags+=("-framework" "UserNotifications") ldflags+=("-framework" "OSLog") ldflags+=("-ldl") @@ -118,7 +120,7 @@ elif [[ "$host" = "Linux" ]]; then elif [[ "$host" = "Win32" ]]; then if [[ -n "$DEBUG" ]]; then # https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features?view=msvc-170 - # TODO(@mribbons): Populate from vcvars64.bat + # TODO(@heapwolf): Populate from vcvars64.bat IFS=',' read -r -a libs <<< "$WIN_DEBUG_LIBS" for (( i = 0; i < ${#libs[@]}; ++i )); do ldflags+=("${libs[$i]}") diff --git a/bin/publish-npm-modules.sh b/bin/publish-npm-modules.sh index ceb2d16394..522ed95e80 100755 --- a/bin/publish-npm-modules.sh +++ b/bin/publish-npm-modules.sh @@ -111,7 +111,7 @@ declare android_abis=() if (( !only_platforms || only_top_level )); then - npm run gen + : #npm run gen elif [[ "arm64" == "$(host_arch)" ]] && [[ "linux" == "$platform" ]]; then echo "warn - Android not supported on $platform-"$(uname -m)"" else @@ -148,7 +148,13 @@ if (( !only_platforms || only_top_level )); then cp -rf "$root/npm/bin/ssc.js" "$SOCKET_HOME/packages/$package/bin/ssc.js" cp -f "$root/LICENSE.txt" "$SOCKET_HOME/packages/$package" cp -f "$root/README.md" "$SOCKET_HOME/packages/$package/README-RUNTIME.md" - cp -rf "$root/api"/* "$SOCKET_HOME/packages/$package" + if (( do_global_link )); then + for file in $(ls "$root/api"); do + ln -sf "$root/api/$file" "$SOCKET_HOME/packages/$package/$file" + done + else + cp -rf "$root/api"/* "$SOCKET_HOME/packages/$package" + fi rm "$SOCKET_HOME/packages/$package/global.d.ts" fi @@ -205,6 +211,12 @@ if (( !only_top_level )); then cp -rap "$SOCKET_HOME/bin"/.vs* "$SOCKET_HOME/packages/$package/bin" fi + if (( do_global_link )); then + for file in $(find "$root/src" -name *.kt); do + ln -sf "$file" "$SOCKET_HOME/packages/$package${file/$root/}" + done + fi + cd "$SOCKET_HOME/packages/$package" || exit $? echo "# in directory: '$SOCKET_HOME/packages/$package'" diff --git a/bin/update-network-protocol.sh b/bin/update-network-protocol.sh new file mode 100755 index 0000000000..3767ad3fcf --- /dev/null +++ b/bin/update-network-protocol.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +npm link @socketsupply/latica + +version="${1:-"1.0.23-0"}" + +rm -rf api/latica.js || exit $? +rm -rf api/latica || exit $? +cp -rf node_modules/@socketsupply/latica/src api/latica || exit $? +rm -rf node_modules/@socketsupply/{socket,socket-{darwin,linux,win32}*,latica} || exit $? + +for file in $(find api/latica -type f); do + sed -i '' -e "s/'socket:\(.*\)'/'..\/\1.js'/g" "$file" || exit $? +done + +{ + echo "import def from './latica/index.js'" + echo "export * from './latica/index.js'" + echo "export default def" +} >> api/latica.js + +tree api/latica diff --git a/bin/update-stream-relay-source.sh b/bin/update-stream-relay-source.sh deleted file mode 100755 index 2796bffa61..0000000000 --- a/bin/update-stream-relay-source.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -version="${1:-"1.0.23-0"}" - -rm -rf api/stream-relay.js || exit $? -rm -rf api/stream-relay || exit $? -cp -rf node_modules/@socketsupply/stream-relay/src api/stream-relay || exit $? -rm -rf node_modules/@socketsupply/{socket,socket-{darwin,linux,win32}*,stream-relay} || exit $? - -for file in $(find api/stream-relay -type f); do - sed -i '' -e "s/'socket:\(.*\)'/'..\/\1.js'/g" "$file" || exit $? -done - -{ - echo "import def from './stream-relay/index.js'" - echo "export * from './stream-relay/index.js'" - echo "export default def" -} >> api/stream-relay.js - -tree api/stream-relay diff --git a/clib.json b/clib.json index 35c68d2106..cd58ce7db1 100644 --- a/clib.json +++ b/clib.json @@ -1,12 +1,7 @@ { "name": "socket", "repo": "socketsupply/socket", - "version": "0.5.4", + "version": "0.6.0-next", "description": "Build and package lean, fast, native desktop and mobile applications using the web technologies you already know.", - "install": "install.sh", - "src": [ - "bin/install.sh", - "LICENSE.txt", - "README.md" - ] + "install": "install.sh" } diff --git a/include/socket/_user-config-bytes.hh b/include/socket/_user-config-bytes.hh new file mode 100644 index 0000000000..fdd5c09cbf --- /dev/null +++ b/include/socket/_user-config-bytes.hh @@ -0,0 +1 @@ +constexpr unsigned char __socket_runtime_user_config_bytes[0] = {}; diff --git a/include/socket/extension.h b/include/socket/extension.h index 59e11fd0b6..48d431205a 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -773,7 +773,7 @@ SOCKET_RUNTIME_EXTENSION_EXTERN_BEGIN // reserved for future ABI changes char __reserved__[1024]; - } __attribute__((packed)); + }; /** * Register a new extension. There is typically no need to call this directly. diff --git a/include/socket/platform.h b/include/socket/platform.h index 57fb925f19..7d2dceb4d2 100644 --- a/include/socket/platform.h +++ b/include/socket/platform.h @@ -1,29 +1,33 @@ #ifndef SOCKET_RUNTIME_PLATFORM_H #define SOCKET_RUNTIME_PLATFORM_H +#ifndef SOCKET_RUNTIME_PLATFORM +#define SOCKET_RUNTIME_PLATFORM 1 + // when this header is included, this is always `0` #define SOCKET_RUNTIME_PLATFORM_WASM 0 #if defined(__x86_64__) || defined(_M_X64) -# define SOCKET_RUNTIME_ARCH_x64 1 -# define SOCKET_RUNTIME_ARCH_ARM64 0 -# define SOCKET_RUNTIME_ARCH_UNKNOWN 0 +# define SOCKET_RUNTIME_PLATFORM_ARCH_x64 1 +# define SOCKET_RUNTIME_PLATFORM_ARCH_ARM64 0 +# define SOCKET_RUNTIME_PLATFORM_ARCH_UNKNOWN 0 #elif defined(__aarch64__) || defined(_M_ARM64) -# define SOCKET_RUNTIME_ARCH_x64 0 -# define SOCKET_RUNTIME_ARCH_ARM64 1 -# define SOCKET_RUNTIME_ARCH_UNKNOWN 0 +# define SOCKET_RUNTIME_PLATFORM_ARCH_x64 0 +# define SOCKET_RUNTIME_PLATFORM_ARCH_ARM64 1 +# define SOCKET_RUNTIME_PLATFORM_ARCH_UNKNOWN 0 #elif defined(__i386__) && !defined(__ANDROID__) #error Socket is not supported on i386. #else -# define SOCKET_RUNTIME_ARCH_x64 0 -# define SOCKET_RUNTIME_ARCH_ARM64 0 -# define SOCKET_RUNTIME_ARCH_UNKNOWN 1 +# define SOCKET_RUNTIME_PLATFORM_ARCH_x64 0 +# define SOCKET_RUNTIME_PLATFORM_ARCH_ARM64 0 +# define SOCKET_RUNTIME_PLATFORM_ARCH_UNKNOWN 1 #endif #if defined(_WIN32) # define SOCKET_RUNTIME_PLATFORM_NAME "win32" # define SOCKET_RUNTIME_PLATFORM_OS "win32" # define SOCKET_RUNTIME_PLATFORM_ANDROID 0 +# define SOCKET_RUNTIME_PLATFORM_APPLE 0 # define SOCKET_RUNTIME_PLATFORM_IOS 0 # define SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR 0 # define SOCKET_RUNTIME_PLATFORM_LINUX 0 @@ -32,6 +36,7 @@ # define SOCKET_RUNTIME_PLATFORM_WINDOWS 1 #elif defined(__APPLE__) # include <TargetConditionals.h> +# define SOCKET_RUNTIME_PLATFORM_APPLE 1 # define SOCKET_RUNTIME_PLATFORM_NAME "darwin" # define SOCKET_RUNTIME_PLATFORM_ANDROID 0 # define SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR 0 @@ -56,7 +61,7 @@ #endif #if defined(__unix__) || defined(unix) || defined(__unix) -# define SOCKET_RUNTIME_PLATFORM_UXIX 0 +# define SOCKET_RUNTIME_PLATFORM_UXIX 1 #else # define SOCKET_RUNTIME_PLATFORM_UXIX 0 #endif @@ -64,18 +69,20 @@ #elif defined(__linux__) # undef linux # define SOCKET_RUNTIME_PLATFORM_NAME "linux" +# define SOCKET_RUNTIME_PLATFORM_APPLE 0 # define SOCKET_RUNTIME_PLATFORM_IOS 0 # define SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR 0 -# define SOCKET_RUNTIME_PLATFORM_LINUX 1 # define SOCKET_RUNTIME_PLATFORM_MACOS 0 # define SOCKET_RUNTIME_PLATFORM_WINDOWS 0 #ifdef __ANDROID__ # define SOCKET_RUNTIME_PLATFORM_OS "android" # define SOCKET_RUNTIME_PLATFORM_ANDROID 1 +# define SOCKET_RUNTIME_PLATFORM_LINUX 0 #else # define SOCKET_RUNTIME_PLATFORM_OS "linux" # define SOCKET_RUNTIME_PLATFORM_ANDROID 0 +# define SOCKET_RUNTIME_PLATFORM_LINUX 1 #endif #if defined(__unix__) || defined(unix) || defined(__unix) @@ -104,6 +111,7 @@ # define SOCKET_RUNTIME_PLATFORM_NAME "openbsd" # define SOCKET_RUNTIME_PLATFORM_OS "openbsd" # define SOCKET_RUNTIME_PLATFORM_ANDROID 0 +# define SOCKET_RUNTIME_PLATFORM_APPLE 0 # define SOCKET_RUNTIME_PLATFORM_IOS 0 # define SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR 0 # define SOCKET_RUNTIME_PLATFORM_LINUX 0 @@ -117,4 +125,17 @@ #endif #endif +#if SOCKET_RUNTIME_PLATFORM_ANDROID || SOCKET_RUNTIME_PLATFORM_IOS +#define SOCKET_RUNTIME_PLATFORM_MOBILE 1 +#define SOCKET_RUNTIME_PLATFORM_DESKTOP 0 +#else +#define SOCKET_RUNTIME_PLATFORM_MOBILE 0 +#define SOCKET_RUNTIME_PLATFORM_DESKTOP 1 +#endif + +#if !defined(SOCKET_RUNTIME_PLATFORM_SANDBOXED) +#define SOCKET_RUNTIME_PLATFORM_SANDBOXED 0 +#endif + +#endif #endif diff --git a/include/socket/webassembly.h b/include/socket/webassembly.h index c66e4edd5f..b58a5299b6 100644 --- a/include/socket/webassembly.h +++ b/include/socket/webassembly.h @@ -24,8 +24,8 @@ #define SOCKET_RUNTIME_PLATFORM_WASM 1 // arch -#define SOCKET_RUNTIME_ARCH_x64 0 -#define SOCKET_RUNTIME_ARCH_ARM64 0 -#define SOCKET_RUNTIME_ARCH_UNKNOWN 1 +#define SOCKET_RUNTIME_PLATFORM_ARCH_x64 0 +#define SOCKET_RUNTIME_PLATFORM_ARCH_ARM64 0 +#define SOCKET_RUNTIME_PLATFORM_ARCH_UNKNOWN 1 #endif diff --git a/npm/packages/@socketsupply/socket-darwin-arm64/package.json b/npm/packages/@socketsupply/socket-darwin-arm64/package.json index b6f0a6527c..2fe3101af2 100644 --- a/npm/packages/@socketsupply/socket-darwin-arm64/package.json +++ b/npm/packages/@socketsupply/socket-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsupply/socket-darwin-arm64", - "version": "0.5.4", + "version": "0.6.0-next", "description": "A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.", "type": "module", "main": "src/index.js", diff --git a/npm/packages/@socketsupply/socket-darwin-x64/package.json b/npm/packages/@socketsupply/socket-darwin-x64/package.json index b6667aa521..1b65140cee 100644 --- a/npm/packages/@socketsupply/socket-darwin-x64/package.json +++ b/npm/packages/@socketsupply/socket-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsupply/socket-darwin-x64", - "version": "0.5.4", + "version": "0.6.0-next", "description": "A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.", "type": "module", "main": "src/index.js", diff --git a/npm/packages/@socketsupply/socket-linux-arm64/package.json b/npm/packages/@socketsupply/socket-linux-arm64/package.json index 56ceae122c..4757c57bbc 100644 --- a/npm/packages/@socketsupply/socket-linux-arm64/package.json +++ b/npm/packages/@socketsupply/socket-linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsupply/socket-linux-arm64", - "version": "0.5.4", + "version": "0.6.0-next", "description": "A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.", "type": "module", "main": "src/index.js", diff --git a/npm/packages/@socketsupply/socket-linux-x64/package.json b/npm/packages/@socketsupply/socket-linux-x64/package.json index 7dba08a92a..795ce92cc6 100644 --- a/npm/packages/@socketsupply/socket-linux-x64/package.json +++ b/npm/packages/@socketsupply/socket-linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsupply/socket-linux-x64", - "version": "0.5.4", + "version": "0.6.0-next", "description": "A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.", "type": "module", "main": "src/index.js", diff --git a/npm/packages/@socketsupply/socket-win32-x64/package.json b/npm/packages/@socketsupply/socket-win32-x64/package.json index eaed3aa19d..774ce119f2 100644 --- a/npm/packages/@socketsupply/socket-win32-x64/package.json +++ b/npm/packages/@socketsupply/socket-win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsupply/socket-win32-x64", - "version": "0.5.4", + "version": "0.6.0-next", "description": "A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.", "type": "module", "main": "src/index.js", diff --git a/npm/packages/@socketsupply/socket/package.json b/npm/packages/@socketsupply/socket/package.json index 0452499aaa..e9f46f3ff6 100644 --- a/npm/packages/@socketsupply/socket/package.json +++ b/npm/packages/@socketsupply/socket/package.json @@ -1,12 +1,12 @@ { "name": "@socketsupply/socket", - "version": "0.5.4", + "version": "0.6.0-next", "description": "A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.", "type": "module", - "main": "index.js", - "types": "index.d.ts", + "main": "./index.js", + "types": "./index.d.ts", "bin": { - "ssc": "bin/ssc.js" + "ssc": "./bin/ssc.js" }, "os": [ "linux", @@ -39,10 +39,10 @@ }, "homepage": "https://github.com/socketsupply/socket#readme", "optionalDependencies": { - "@socketsupply/socket-darwin-arm64": "0.5.4", - "@socketsupply/socket-darwin-x64": "0.5.4", - "@socketsupply/socket-linux-arm64": "0.5.4", - "@socketsupply/socket-linux-x64": "0.5.4", - "@socketsupply/socket-win32-x64": "0.5.4" + "@socketsupply/socket-darwin-arm64": "0.6.0-next", + "@socketsupply/socket-darwin-x64": "0.6.0-next", + "@socketsupply/socket-linux-arm64": "0.6.0-next", + "@socketsupply/socket-linux-x64": "0.6.0-next", + "@socketsupply/socket-win32-x64": "0.6.0-next" } } diff --git a/package.json b/package.json index 80251960c8..e10c83216b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "type": "module", "scripts": { + "clean": "./bin/clean.sh", "gen": "npm run gen:docs && npm run gen:tsc", "gen:docs": "node ./bin/generate-docs.js", "gen:tsc": "./bin/generate-typescript-typings.sh", @@ -11,21 +12,22 @@ "test:android": "cp -f .ssc.env test | echo && cd test && npm install --silent --no-audit && npm run test:android", "test:android-emulator": "cp -f .ssc.env test | echo && cd test && npm install --silent --no-audit && npm run test:android-emulator", "test:clean": "cd test && rm -rf dist", - "update-stream-relay-source": "./bin/update-stream-relay-source.sh", + "update-network-protocol": "./bin/update-network-protocol.sh", "relink": "./bin/publish-npm-modules.sh --link" }, "private": true, "devDependencies": { - "acorn": "8.10.0", - "acorn-walk": "8.2.0", + "acorn": "8.12.0", + "acorn-walk": "8.3.3", "standard": "^17.1.0", - "typescript": "5.3.2", - "urlpattern-polyfill": "^9.0.0", - "whatwg-fetch": "^3.6.17", - "whatwg-url": "^13.0.0" + "typescript": "5.5.2", + "urlpattern-polyfill": "^10.0.0", + "web-streams-polyfill": "^4.0.0", + "whatwg-fetch": "^3.6.20", + "whatwg-url": "^14.0.0" }, "optionalDependencies": { - "@socketsupply/stream-relay": "^1.0.23-0" + "@socketsupply/latica": "^0.1.0" }, "standard": { "ignore": [ @@ -35,12 +37,12 @@ "/api/url/urlpattern/urlpattern.js", "/api/url/url/url.js", "/api/fetch/fetch.js", + "/api/internal/streams/web.js", "/npm/packages/@socketsupply/socket-node/index.cjs" ] }, "workspaces": [ "npm/packages/@socketsupply/socket-node" ], - "version": "0.0.0", - "dependencies": {} + "version": "0.0.0" } diff --git a/socket-runtime.pc.in b/socket-runtime.pc.in index a608c699f2..fd473a5c97 100644 --- a/socket-runtime.pc.in +++ b/socket-runtime.pc.in @@ -3,5 +3,5 @@ Version: {{VERSION}} Description: Build and package lean, fast, native desktop and mobile applications using the web technologies you already know. URL: https://github.com/socketsupply/socket Requires: {{DEPENDENCIES}} -Libs: -L{{LIB_DIRECTORY}} -lsocket-runtime -luv {{LDFLAGS}} +Libs: -L{{LIB_DIRECTORY}} -lsocket-runtime -luv -lllama {{LDFLAGS}} Cflags: -I{{INCLUDE_DIRECTORY}} {{CFLAGS}} diff --git a/src/android/app.kt b/src/android/app.kt new file mode 100644 index 0000000000..e2dbce5af1 --- /dev/null +++ b/src/android/app.kt @@ -0,0 +1,10 @@ +// vim: set sw=2: +package __BUNDLE_IDENTIFIER__ + +open class App : socket.runtime.app.App() { + companion object { + init { + socket.runtime.app.App.loadSocketRuntime() + } + } +} diff --git a/src/android/bridge.cc b/src/android/bridge.cc deleted file mode 100644 index 62eccca7b6..0000000000 --- a/src/android/bridge.cc +++ /dev/null @@ -1,222 +0,0 @@ -#include "internal.hh" - -using namespace SSC::android; - -static auto onInternalRouteResponseSignature = - "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)V"; - -namespace SSC::android { - Bridge::Bridge (JNIEnv* env, jobject self, Runtime* runtime) - : IPC::Bridge(runtime) - { - this->env = env; - this->self = env->NewGlobalRef(self); - this->pointer = reinterpret_cast<jlong>(this); - this->runtime = runtime; - this->router.dispatchFunction = [this](auto callback) { - // TODO(@jwerle): get `JavaVM*` from `JNIEnv*` above and then - // use use `JNIEnvironmentAttachment` to execute `callback` - if (callback != nullptr) { - callback(); - } - }; - - this->isAndroidEmulator = this->runtime->isEmulator; - } - - Bridge::~Bridge () { - this->env->DeleteGlobalRef(this->self); - IPC::Bridge::~Bridge(); - } -} - -extern "C" { - jlong external(Bridge, alloc)( - JNIEnv *env, - jobject self, - jlong runtimePointer - ) { - auto runtime = Runtime::from(runtimePointer); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return 0; - } - - auto bridge = new Bridge(env, self, runtime); - - if (bridge == nullptr) { - Throw(env, BridgeNotInitializedException); - return 0; - } - - return bridge->pointer; - } - - jboolean external(Bridge, dealloc)( - JNIEnv *env, - jobject self - ) { - auto bridge = Bridge::from(env, self); - - if (bridge == nullptr) { - Throw(env, BridgeNotInitializedException); - return false; - } - - delete bridge; - return true; - } - - jboolean external(Bridge, route)( - JNIEnv *env, - jobject self, - jstring uriString, - jbyteArray byteArray, - jlong requestId - ) { - auto bridge = Bridge::from(env, self); - - if (bridge == nullptr) { - Throw(env, BridgeNotInitializedException); - return false; - } - - JavaVM* jvm = nullptr; - auto jniVersion = env->GetVersion(); - env->GetJavaVM(&jvm); - auto attachment = JNIEnvironmentAttachment { jvm, jniVersion }; - - if (attachment.hasException()) { - return false; - } - - auto uri = StringWrap(env, uriString); - auto size = byteArray != nullptr ? env->GetArrayLength(byteArray) : 0; - auto input = size > 0 ? new char[size]{0} : nullptr; - - if (size > 0 && input != nullptr) { - env->GetByteArrayRegion(byteArray, 0, size, (jbyte*) input); - } - - auto routed = bridge->route(uri.str(), input, size, [=](auto result) mutable { - if (result.seq == "-1") { - bridge->router.send(result.seq, result.str(), result.post); - return; - } - - auto attachment = JNIEnvironmentAttachment { jvm, jniVersion }; - auto self = bridge->self; - auto env = attachment.env; - - if (!attachment.hasException()) { - auto size = result.post.length; - auto body = result.post.body; - auto bytes = body ? env->NewByteArray(size) : nullptr; - - if (bytes != nullptr) { - env->SetByteArrayRegion(bytes, 0, size, (jbyte *) body); - } - - auto seq = env->NewStringUTF(result.seq.c_str()); - auto source = env->NewStringUTF(result.source.c_str()); - auto value = env->NewStringUTF(result.str().c_str()); - auto headers = env->NewStringUTF(result.post.headers.c_str()); - - CallVoidClassMethodFromEnvironment( - env, - self, - "onInternalRouteResponse", - onInternalRouteResponseSignature, - requestId, - seq, - source, - value, - headers, - bytes - ); - - env->DeleteLocalRef(seq); - env->DeleteLocalRef(source); - env->DeleteLocalRef(value); - env->DeleteLocalRef(headers); - - if (bytes != nullptr) { - env->DeleteLocalRef(bytes); - } - } - }); - - delete [] input; - - if (!routed) { - auto attachment = JNIEnvironmentAttachment { jvm, jniVersion }; - auto env = attachment.env; - - if (!attachment.hasException()) { - auto msg = SSC::IPC::Message{uri.str()}; - auto err = SSC::JSON::Object::Entries { - {"source", uri.str()}, - {"err", SSC::JSON::Object::Entries { - {"message", "Not found"}, - {"type", "NotFoundError"}, - {"url", uri.str()} - }} - }; - - auto seq = env->NewStringUTF(msg.seq.c_str()); - auto source =env->NewStringUTF(msg.name.c_str()); - auto value = env->NewStringUTF(SSC::JSON::Object(err).str().c_str()); - auto headers = env->NewStringUTF(""); - - CallVoidClassMethodFromEnvironment( - env, - self, - "onInternalRouteResponse", - onInternalRouteResponseSignature, - requestId, - seq, - source, - value, - headers, - nullptr - ); - - env->DeleteLocalRef(seq); - env->DeleteLocalRef(source); - env->DeleteLocalRef(value); - env->DeleteLocalRef(headers); - } - } - - return routed; - } - - jboolean external(Bridge, emit)( - JNIEnv *env, - jobject self, - jstring eventNameString, - jstring eventDataString - ) { - auto bridge = Bridge::from(env, self); - - if (bridge == nullptr) { - Throw(env, BridgeNotInitializedException); - return false; - } - - JavaVM* jvm = nullptr; - auto jniVersion = env->GetVersion(); - env->GetJavaVM(&jvm); - auto attachment = JNIEnvironmentAttachment { jvm, jniVersion }; - - if (attachment.hasException()) { - return false; - } - - auto event = StringWrap(env, eventNameString); - auto data = StringWrap(env, eventDataString); - - return bridge->router.emit(event.str(), data.str()); - } -} diff --git a/src/android/bridge.kt b/src/android/bridge.kt deleted file mode 100644 index e955464680..0000000000 --- a/src/android/bridge.kt +++ /dev/null @@ -1,1363 +0,0 @@ -// vim: set sw=2: -package __BUNDLE_IDENTIFIER__ - -interface IBridgeConfiguration { - val getRootDirectory: () -> String -} - -data class BridgeConfiguration ( - override val getRootDirectory: () -> String -) : IBridgeConfiguration - -data class Result ( - val id: Long, - val seq: String, - val source: String, - val value: String, - val bytes: ByteArray? = null, - val headers: Map<String, String> = emptyMap() -) - -typealias RouteCallback = (Result) -> Unit - -data class RouteRequest ( - val id: Long, - val callback: RouteCallback -) - -// container for a parseable IPC message (ipc://...) -class Message (message: String? = null) { - var uri: android.net.Uri? = - if (message != null) { - android.net.Uri.parse(message) - } else { - android.net.Uri.parse("ipc://") - } - - var command: String - get () = uri?.host ?: "" - set (command) { - uri = uri?.buildUpon()?.authority(command)?.build() - } - - var domain: String - get () { - val parts = command.split(".") - return parts.slice(0..(parts.size - 2)).joinToString(".") - } - set (_) {} - - var value: String - get () = get("value") - set (value) { - set("value", value) - } - - var seq: String - get () = get("seq") - set (seq) { - set("seq", seq) - } - - var bytes: ByteArray? = null - - fun get (key: String, defaultValue: String = ""): String { - val value = uri?.getQueryParameter(key) - - if (value != null && value.isNotEmpty()) { - return value - } - - return defaultValue - } - - fun has (key: String): Boolean { - return get(key).isNotEmpty() - } - - fun set (key: String, value: String): Boolean { - uri = uri?.buildUpon()?.appendQueryParameter(key, value)?.build() - return uri == null - } - - fun delete (key: String): Boolean { - if (uri?.getQueryParameter(key) == null) { - return false - } - - val params = uri?.queryParameterNames - val tmp = uri?.buildUpon()?.clearQuery() - - if (params != null) { - for (param: String in params) { - if (!param.equals(key)) { - val value = uri?.getQueryParameter(param) - tmp?.appendQueryParameter(param, value) - } - } - } - - uri = tmp?.build() - - return true - } - - override fun toString(): String { - return uri?.toString() ?: "" - } -} - -fun getPathFromContentDataColumn ( - activity: MainActivity, - uri: android.net.Uri, - id: String? = null -) : String? { - val context = activity.applicationContext - val column = android.provider.MediaStore.MediaColumns.DATA - var cursor: android.database.Cursor? = null - var result: String? = null - - try { - cursor = context.contentResolver.query(uri, arrayOf(column), null, null, null) - if (cursor != null) { - cursor.moveToFirst() - do { - if (id == null) { - result = cursor.getString(cursor.getColumnIndex(column)) - break - } else { - var index = cursor.getColumnIndex(android.provider.MediaStore.MediaColumns._ID) - var tmp: String? = null - - try { - tmp = cursor.getString(index) - } catch (e: Exception) {} - - if (tmp == id) { - index = cursor.getColumnIndex(column) - result = cursor.getString(index) - break - } - } - } while (cursor.moveToNext()) - } - } catch (err: Exception) { - return null - } finally { - if (cursor != null) { - cursor.close() - } - } - - return result -} - -fun isDocumentUri (activity: MainActivity, uri: android.net.Uri): Boolean { - return android.provider.DocumentsContract.isDocumentUri(activity.applicationContext, uri) -} - -fun isContentUri (uri: android.net.Uri): Boolean { - return uri.scheme == "content" -} - -fun isSocketUri (uri: android.net.Uri): Boolean { - return uri.scheme == "socket" -} - -fun isAssetBundleUri (uri: android.net.Uri): Boolean { - val path = uri.path - - if (path == null) { - return false - } - - return ( - (isContentUri(uri) || isSocketUri(uri)) && - uri.authority == "__BUNDLE_IDENTIFIER__" - ) -} - -fun isExternalStorageDocumentUri (uri: android.net.Uri): Boolean { - return uri.authority == "com.android.externalstorage.documents" -} - -fun isDownloadsDocumentUri (uri: android.net.Uri): Boolean { - return uri.authority == "com.android.providers.downloads.documents" -} - -fun isMediaDocumentUri (uri: android.net.Uri): Boolean { - return uri.authority == "com.android.providers.media.documents" -} - -fun getPathFromURI (activity: MainActivity, uri: android.net.Uri) : String? { - // just return the file path for `file://` based URIs - if (uri.scheme == "file") { - return uri.path - } - - if (isDocumentUri(activity, uri)) { - if (isExternalStorageDocumentUri(uri)) { - val externalStorage = android.os.Environment.getExternalStorageDirectory().absolutePath - val documentId = android.provider.DocumentsContract.getDocumentId(uri) - val parts = documentId.split(":") - val type = parts[0] - - if (type == "primary") { - return externalStorage + "/" + parts[1] - } - } - - if (isDownloadsDocumentUri(uri)) { - val documentId = android.provider.DocumentsContract.getDocumentId(uri) - val contentUri = android.content.ContentUris.withAppendedId( - android.net.Uri.parse("content://downloads/public_downloads"), - documentId.toLong() - ) - - return getPathFromContentDataColumn(activity, contentUri) - } - - if (isMediaDocumentUri(uri)) { - val documentId = android.provider.DocumentsContract.getDocumentId(uri) - val parts = documentId.split(":") - val type = parts[0] - val id = parts[1] - - val contentUri = ( - if (type == "image") { - android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } else if (type == "video") { - android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI - } else if (type == "audio") { - android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - } else { - android.provider.MediaStore.Files.getContentUri("external") - } - ) - - if (contentUri == null) { - return null - } - - return getPathFromContentDataColumn(activity, contentUri, id) - } - } else if (uri.scheme == "content") { - if (uri.authority == "com.google.android.apps.photos.content") { - return uri.lastPathSegment - } - - return getPathFromContentDataColumn(activity, uri) - } - - return null -} - -open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { - open protected val TAG = "Bridge" - var pointer = alloc(runtime.pointer) - var runtime = java.lang.ref.WeakReference(runtime) - val requests = mutableMapOf<Long, RouteRequest>() - val configuration = configuration - val buffers = mutableMapOf<String, ByteArray>() - val semaphore = java.util.concurrent.Semaphore( - java.lang.Runtime.getRuntime().availableProcessors() - ) - - val openedAssetDirectories = mutableMapOf<String, String>() - val openedAssetDirectoriesEntryCache: MutableMap<String, Array<String>> = mutableMapOf() - val fileDescriptors = mutableMapOf<String, android.content.res.AssetFileDescriptor>() - val uris = mutableMapOf<String, android.net.Uri>() - - protected var nextRequestId = 0L - - fun finalize () { - if (this.pointer > 0) { - this.dealloc() - } - - this.pointer = 0 - } - - fun call (command: String, callback: RouteCallback? = null): Boolean { - return this.call(command, emptyMap(), callback) - } - - fun call (command: String, options: Map<String, String> = emptyMap(), callback: RouteCallback? = null): Boolean { - val message = Message("ipc://$command") - - for (entry in options.entries.iterator()) { - message.set(entry.key, entry.value) - } - - if (callback != null) { - return this.route(message.toString(), null, callback) - } - - return this.route(message.toString(), null, {}) - } - - fun route ( - value: String, - bytes: ByteArray? = null, - callback: RouteCallback - ): Boolean { - val activity = this.runtime.get()?.activity?.get() ?: return false - val runtime = activity.runtime - val message = Message(value) - val contentResolver = activity.applicationContext.contentResolver - val assetManager = activity.applicationContext.resources.assets - - message.bytes = bytes - - if (buffers.contains(message.seq)) { - message.bytes = buffers[message.seq] - buffers.remove(message.seq) - } - - if (message.domain == "fs") { - if (message.has("path")) { - var path = message.get("path") - val uri = android.net.Uri.parse(path) - if (!isAssetBundleUri(uri)) { - if (path.startsWith("/")) { - path = path.substring(1) - } else if (path.startsWith("./")) { - path = path.substring(2) - } - - try { - val stream = assetManager.open(path, 0) - message.set("path", "socket://__BUNDLE_IDENTIFIER__/$path") - stream.close() - } catch (e: java.io.FileNotFoundException) { - // noop - } catch (e: Exception) { - // noop - } - } - } - } - - when (message.command) { - "application.getScreenSize", - "application.getWindows" -> { - val windowManager = activity.applicationContext.getSystemService( - android.content.Context.WINDOW_SERVICE - ) as android.view.WindowManager - - val metrics = windowManager.getCurrentWindowMetrics() - val windowInsets = metrics.windowInsets - val insets = windowInsets.getInsetsIgnoringVisibility( - android.view.WindowInsets.Type.navigationBars() or - android.view.WindowInsets.Type.displayCutout() - ) - - val width = insets.right + insets.left - val height = insets.top + insets.bottom - - if (message.command == "application.getScreenSize") { - callback(Result(0, message.seq, message.command, """{ - "data": { - "width": $width, - "height": $height - } - }""")) - } else { - activity.runOnUiThread { - val status = 31 // WINDOW_SHOWN" - val title = activity.webview?.title ?: "" - callback(Result(0, message.seq, message.command, """{ - "data": [{ - "index": 0, - "title": "$title", - "width": $width, - "height": $height, - "status": $status - }] - }""")) - } - } - return true - } - - // handle WASM extensions here - "extension.stats" -> { - val name = message.get("name") - if (name.length > 0) { - val path = "socket/extensions/$name/$name.wasm" - try { - val stream = assetManager.open(path, 0) - val abi = 1 // TODO(@jwerle): read this over JNI - stream.close() - callback(Result(0, message.seq, message.command, """{ - "data": { - "abi": $abi, - "name": "$name", - "type": "wasm32", - "path": "socket://__BUNDLE_IDENTIFIER__/$path" - } - }""")) - return true - } catch (e: java.io.FileNotFoundException) { - // noop - } catch (e: Exception) { - // nooop - } - } - } - - // handle WASM extensions here - "extension.type" -> { - val name = message.get("name") - if (name.length > 0) { - try { - val path = "socket/extensions/$name/$name.wasm" - val stream = assetManager.open(path, 0) - stream.close() - callback(Result(0, message.seq, message.command, """{ - "data": { - "name": "$name", - "type": "wasm32" - } - }""")) - return true - } catch (e: java.io.FileNotFoundException) { - // noop - } catch (e: Exception) { - // nooop - } - } - } - - "window.showFileSystemPicker" -> { - val options = WebViewFilePickerOptions( - null, - arrayOf<String>(), - message.get("allowMultiple") == "true", - message.get("allowFiles") == "true", - message.get("allowDirs") == "true" - ) - - activity.showFileSystemPicker(options, fun (uris: Array<android.net.Uri>) { - var paths: Array<String> = arrayOf() - - for (uri in uris) { - val path = getPathFromURI(activity, uri) ?: uri - paths += "\"$path\"" - } - - callback(Result(0, message.seq, message.command, """{ - "data": { - "paths": ${paths.joinToString(prefix = "[", postfix = "]")} - } - }""")) - }) - return true - } - - "os.paths" -> { - val storage = android.os.Environment.getExternalStorageDirectory().absolutePath - - val resources = "socket://__BUNDLE_IDENTIFIER__" - var downloads = "$storage/Downloads" - var documents = "$storage/Documents" - var pictures = "$storage/Pictures" - var desktop = activity.getExternalFilesDir(null)?.absolutePath ?: "$storage/Desktop" - var videos = "$storage/DCIM/Camera/" - var music = "$storage/Music" - var home = desktop - - callback(Result(0, message.seq, message.command, """{ - "data": { - "resources": "$resources", - "downloads": "$downloads", - "documents": "$documents", - "pictures": "$pictures", - "desktop": "$desktop", - "videos": "$videos", - "music": "$music", - "home": "$home" - } - }""")) - return true - } - - "permissions.request" -> { - if (!message.has("name")) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "Expecting 'name' in parameters" } - }""")) - return true - } - - val name = message.get("name") - val permissions = mutableListOf<String>() - - when (name) { - "geolocation" -> { - if ( - activity.checkPermission("android.permission.ACCESS_COARSE_LOCATION") && - activity.checkPermission("android.permission.ACCESS_FINE_LOCATION") - ) { - callback(Result(0, message.seq, message.command, "{}")) - return true - } - - permissions.add("android.permission.ACCESS_COARSE_LOCATION") - permissions.add("android.permission.ACCESS_FINE_LOCATION") - } - - "push", "notifications" -> { - if (activity.checkPermission("android.permission.POST_NOTIFICATIONS")) { - callback(Result(0, message.seq, message.command, "{}")) - return true - } - - permissions.add("android.permission.POST_NOTIFICATIONS") - } - - else -> { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Unknown permission requested: '$name'" - } - }""")) - return true - } - } - - activity.requestPermissions(permissions.toTypedArray(), fun (granted: Boolean) { - if (granted) { - callback(Result(0, message.seq, message.command, "{}")) - } else { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "User denied permission request for '$name'" - } - }""")) - } - }) - - return true - } - - "permissions.query" -> { - if (!message.has("name")) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "Expecting 'name' in parameters" } - }""")) - return true - } - - val name = message.get("name") - - if (name == "geolocation") { - if (!runtime.isPermissionAllowed("geolocation")) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "User denied permissions to access the device's location" - } - }""")) - } else if ( - activity.checkPermission("android.permission.ACCESS_COARSE_LOCATION") && - activity.checkPermission("android.permission.ACCESS_FINE_LOCATION") - ) { - callback(Result(0, message.seq, message.command, """{ - "data": { - "state": "granted" - } - }""")) - } else { - callback(Result(0, message.seq, message.command, """{ - "data": { - "state": "prompt" - } - }""")) - } - } - - if (name == "notifications" || name == "push") { - if (!runtime.isPermissionAllowed("notifications")) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "User denied permissions to show notifications" - } - }""")) - } else if ( - activity.checkPermission("android.permission.POST_NOTIFICATIONS") && - androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() - ) { - callback(Result(0, message.seq, message.command, """{ - "data": { - "state": "granted" - } - }""")) - } else { - callback(Result(0, message.seq, message.command, """{ - "data": { - "state": "prompt" - } - }""")) - } - } - - if (name == "persistent-storage" || name == "storage-access") { - if (!runtime.isPermissionAllowed("data_access")) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "User denied permissions for ${name.replace('-', ' ')}" - } - }""")) - } - } - - return true - } - - "notification.show" -> { - if ( - !activity.checkPermission("android.permission.POST_NOTIFICATIONS") || - !androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() - ) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "User denied permissions for 'notifications'" } - }""")) - return true - } - - if (!message.has("id")) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "Expecting 'id' in parameters" } - }""")) - return true - } - - if (!message.has("title")) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "Expecting 'title' in parameters" } - }""")) - return true - } - - val id = message.get("id") - val channel = message.get("channel", "default").replace("default", "__BUNDLE_IDENTIFIER__"); - val vibrate = message.get("vibrate") - .split(",") - .filter({ it.length > 0 }) - .map({ it.toInt().toLong() }) - .toTypedArray() - - val identifier = id.toLongOrNull()?.toInt() ?: (0..16384).random().toInt() - - val contentIntent = android.content.Intent(activity, MainActivity::class.java).apply { - flags = ( - android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP or - android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP - ) - } - - val deleteIntent = android.content.Intent(activity, MainActivity::class.java).apply { - flags = ( - android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP or - android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP - ) - } - - contentIntent.setAction("notification.response.default") - contentIntent.putExtra("id", id) - - deleteIntent.setAction("notification.response.dismiss") - deleteIntent.putExtra("id", id) - - val pendingContentIntent: android.app.PendingIntent = android.app.PendingIntent.getActivity( - activity, - identifier, - contentIntent, - ( - android.app.PendingIntent.FLAG_UPDATE_CURRENT or - android.app.PendingIntent.FLAG_IMMUTABLE or - android.app.PendingIntent.FLAG_ONE_SHOT - ) - ) - - val pendingDeleteIntent: android.app.PendingIntent = android.app.PendingIntent.getActivity( - activity, - identifier, - deleteIntent, - ( - android.app.PendingIntent.FLAG_UPDATE_CURRENT or - android.app.PendingIntent.FLAG_IMMUTABLE - ) - ) - - val builder = androidx.core.app.NotificationCompat.Builder( - activity, - channel - ) - - builder - .setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT) - .setContentTitle(message.get("title", "Notification")) - .setContentIntent(pendingContentIntent) - .setDeleteIntent(pendingDeleteIntent) - .setAutoCancel(true) - - if (message.has("body")) { - builder.setContentText(message.get("body")) - } - - if (message.has("icon")) { - val url = message.get("icon") - .replace("socket://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") - .replace("https://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") - - val icon = androidx.core.graphics.drawable.IconCompat.createWithContentUri(url) - builder.setSmallIcon(icon) - } else { - val icon = androidx.core.graphics.drawable.IconCompat.createWithResource( - activity, - R.mipmap.ic_launcher_round - ) - builder.setSmallIcon(icon) - } - - if (message.has("image")) { - val url = message.get("image") - .replace("socket://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") - .replace("https://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") - - val icon = android.graphics.drawable.Icon.createWithContentUri(url) - builder.setLargeIcon(icon) - } - - if (message.has("category")) { - var category = message.get("category") - .replace("msg", "message") - .replace("-", "_") - - builder.setCategory(category) - } - - if (message.get("silent") == "true") { - builder.setSilent(true) - } - - val notification = builder.build() - with (androidx.core.app.NotificationManagerCompat.from(activity)) { - notify( - message.get("tag"), - identifier, - notification - ) - } - - callback(Result(0, message.seq, message.command, """{ - "data": { - "id": "$id" - } - }""")) - - activity.runOnUiThread { - this.emit("notificationpresented", """{ - "id": "$id" - }""") - } - - return true - } - - "notification.close" -> { - if ( - !activity.checkPermission("android.permission.POST_NOTIFICATIONS") || - !androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() - ) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "User denied permissions for 'notifications'" } - }""")) - return true - } - - if (!message.has("id")) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "Expecting 'id' in parameters" } - }""")) - return true - } - - val id = message.get("id") - with (androidx.core.app.NotificationManagerCompat.from(activity)) { - cancel( - message.get("tag"), - id.toLongOrNull()?.toInt() ?: (0..16384).random().toInt() - ) - } - - callback(Result(0, message.seq, message.command, """{ - "data": { - "id": "$id" - } - }""")) - - activity.runOnUiThread { - this.emit("notificationresponse", """{ - "id": "$id", - "action": "dismiss" - }""") - } - - return true - } - - "notification.list" -> { - if ( - !activity.checkPermission("android.permission.POST_NOTIFICATIONS") || - !androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() - ) { - callback(Result(0, message.seq, message.command, """{ - "err": { "message": "User denied permissions for 'notifications'" } - }""")) - return true - } - - return true - } - - "buffer.map" -> { - if (bytes != null) { - buffers[message.seq] = bytes - } - callback(Result(0, message.seq, message.command, "{}")) - return true - } - - "log", "stdout" -> { - console.log(message.value) - callback(Result(0, message.seq, message.command, "{}")) - return true - } - - "stderr" -> { - console.error(message.value) - callback(Result(0, message.seq, message.command, "{}")) - return true - } - - "application.exit", "process.exit", "exit" -> { - val code = message.get("value", "0").toInt() - this.runtime.get()?.exit(code) - callback(Result(0, message.seq, message.command, "{}")) - return true - } - - "openExternal" -> { - this.runtime.get()?.openExternal(message.value) - callback(Result(0, message.seq, message.command, "{}")) - return true - } - - "fs.access" -> { - var mode = message.get("mode", "0").toInt() - val path = message.get("path") - val uri = android.net.Uri.parse(path) - if (isAssetBundleUri(uri) || isContentUri(uri) || isDocumentUri(activity, uri)) { - try { - val path = uri.path?.substring(1) - val stream = ( - if (path != null && isAssetBundleUri(uri)) assetManager.open(path, mode) - else contentResolver.openInputStream(uri) - ) - - if (stream == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Failed to open input stream for access check" - } - }""")) - return true; - } - - mode = 4 // R_OK - stream.close() - callback(Result(0, message.seq, message.command, """{ - "data": { - "mode": $mode - } - }""")) - } catch (e: java.io.FileNotFoundException) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "type": "NotFoundError", - "message": "${e.message}" - } - }""")) - } catch (e: Exception) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "${e.message}" - } - }""")) - } - - return true - } - } - - "fs.open" -> { - val path = message.get("path") - val uri = android.net.Uri.parse(path) - - if (isAssetBundleUri(uri) || isContentUri(uri) || isDocumentUri(activity, uri)) { - val id = message.get("id") - - if (id.length == 0) { - return false - } - - try { - val path = uri.path?.substring(1) - val fd = ( - if (path != null && isAssetBundleUri(uri)) assetManager.openFd(path) - else contentResolver.openTypedAssetFileDescriptor( - uri, - "*/*", - null - ) - ) - - if (fd == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Failed to open asset file descriptor" - } - }""")) - return true; - } - - this.fileDescriptors[id] = fd - this.uris[id] = uri - - callback(Result(0, message.seq, message.command, """{ - "data": { - "id": "$id", - "fd": ${fd.getParcelFileDescriptor().getFd()} - } - }""")) - } catch (e: java.io.FileNotFoundException) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "type": "NotFoundError", - "message": "${e.message}" - } - }""")) - } catch (e: Exception) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "${e.message}" - } - }""")) - } - - return true - } - } - - "fs.opendir" -> { - val path = message.get("path") - val uri = android.net.Uri.parse(path) - val id = message.get("id") - - if (isAssetBundleUri(uri)) { - var path = uri.path - if (path == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Missing pathspec in 'path'" - } - }""")) - } else { - if (path.length > 0) { - while (path != null && path.startsWith("/")) { - path = path.substring(1) - } - - if (path == null) { - path = "" - } - - if (path.length > 0 && !path.endsWith("/")) { - path += "/" - } - } - - val entries = assetManager.list(path) - if (entries == null || entries.size == 0) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "type": "NotFoundError", - "message": "Directory not found in asset manager" - } - }""")) - } else { - this.openedAssetDirectoriesEntryCache[id] = entries - this.openedAssetDirectories[id] = path - callback(Result(0, message.seq, message.command, """{ - "data": { - "id": "$id" - } - }""")) - } - } - - return true - } - } - - "fs.close" -> { - val id = message.get("id") - if (this.fileDescriptors.contains(id)) { - try { - val fd = this.fileDescriptors[id] - this.fileDescriptors.remove(id) - this.uris.remove(id) - - if (fd != null) { - fd.close() - } - - callback(Result(0, message.seq, message.command, """{ - "data": { - "id": "$id" - } - }""")) - } catch (e: Exception) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "${e.message}" - } - }""")) - } - - return true - } - } - - "fs.closedir" -> { - val id = message.get("id") - if (this.openedAssetDirectories.contains(id)) { - this.openedAssetDirectories.remove(id) - this.openedAssetDirectoriesEntryCache.remove(id) - callback(Result(0, message.seq, message.command, """{ - "data": { - "id": "$id" - } - }""")) - return true - } - } - - "fs.read" -> { - val id = message.get("id") - val size = message.get("size", "0").toInt() - val offset = message.get("offset", "0").toLong() - if (this.fileDescriptors.contains(id)) { - try { - val uri = this.uris[id] - - if (uri == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Could not determine URI for open asset file descriptor" - } - }""")) - return true - } - - val path = uri.path?.substring(1) - val stream = ( - if (path != null && isAssetBundleUri(uri)) assetManager.open(path, 2) - else contentResolver.openInputStream(uri) - ) - - if (stream == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Failed to open input stream for file read" - } - }""")) - return true; - } - - val bytes = ByteArray(size) - - if (offset > 0) { - stream.skip(offset) - } - - val bytesRead = stream.read(bytes, 0, size) - stream.close() - if (bytesRead > 0) { - callback(Result(0, message.seq, message.command, "{}", bytes.slice(0..(bytesRead - 1)).toByteArray())) - } else { - callback(Result(0, message.seq, message.command, "{}", ByteArray(0))) - } - } catch (e: Exception) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "${e.message}" - } - }""")) - } - - return true - } - } - - "fs.readdir" -> { - val id = message.get("id") - val max = message.get("entries", "8").toInt() - if (this.openedAssetDirectories.contains(id)) { - val path = this.openedAssetDirectories[id] - if (path != null) { - var entries = this.openedAssetDirectoriesEntryCache[id] - if (entries != null) { - var data: Array<String> = arrayOf() - var count = 0 - for (entry in entries) { - var type = 1 - - try { - val tmp = assetManager.list(path + entry) - if (entry.endsWith("/") || (tmp != null && tmp.size > 0)) { - type = 2 - } - } catch (e: Exception) {} - - data += """{ "name": "$entry", "type": $type }""" - - if (++count == max) { - break - } - } - - entries = entries.slice(count..(entries.size - 1)).toTypedArray() - this.openedAssetDirectoriesEntryCache[id] = entries - - callback(Result(0, message.seq, message.command, """{ - "data": ${data.joinToString(prefix = "[", postfix = "]")} - }""")) - return true; - } - } - callback(Result(0, message.seq, message.command, """{ "data": [] }""")) - return true; - } - } - - "fs.readFile" -> { - val path = message.get("path") - val uri = android.net.Uri.parse(path) - if (isAssetBundleUri(uri) || isContentUri(uri) || isDocumentUri(activity, uri)) { - try { - val path = uri.path?.substring(1) - val stream = ( - if (path != null && isAssetBundleUri(uri)) assetManager.open(path, 2) // ACCESS_STREAMING - else contentResolver.openInputStream(uri) - ) - - if (stream == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Failed to open input stream for file read" - } - }""")) - return true; - } - - val bytes = stream.readAllBytes() - stream.close() - callback(Result(0, message.seq, message.command, "{}", bytes)) - } catch (e: java.io.FileNotFoundException) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "type": "NotFoundError", - "message": "${e.message}" - } - }""")) - } catch (e: Exception) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "${e.message}" - } - }""")) - } - return true - } - } - - "fs.stat" -> { - val path = message.get("path") - val uri = android.net.Uri.parse(path) - if (isAssetBundleUri(uri) || isContentUri(uri) || isDocumentUri(activity, uri)) { - val path = uri.path?.substring(1) - val fd = ( - if (path != null && isAssetBundleUri(uri)) assetManager.openFd(path) - else contentResolver.openTypedAssetFileDescriptor( - uri, - "*/*", - null - ) - ) - - if (fd == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Failed to open asset file descriptor for stats" - } - }""")) - return true - } - - callback(Result(0, message.seq, message.command, """{ - "data": { - "st_mode": 4, - "st_size": ${fd.getParcelFileDescriptor().getStatSize()} - } - }""")) - - fd.close() - return true - } - } - - "fs.fstat" -> { - val path = message.get("path") - val id = message.get("id") - if (this.fileDescriptors.contains(id)) { - val fd = this.fileDescriptors[id] - - if (fd == null) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "message": "Failed to acquire open asset file descriptor for stats" - } - }""")) - return true - } - - callback(Result(0, message.seq, message.command, """{ - "data": { - "st_mode": 4, - "st_size": ${fd.getParcelFileDescriptor().getStatSize()} - } - }""")) - return true - } - } - } - - if (message.domain == "fs") { - if (message.has("id") || message.has("path")) { - val path = message.get("path") - val uri = android.net.Uri.parse(path) - val id = message.get("id") - if (isAssetBundleUri(uri) || isContentUri(uri) || isDocumentUri(activity, uri) || this.fileDescriptors.contains(id)) { - callback(Result(0, message.seq, message.command, """{ - "err": { - "type": "NotFoundError", - "message": "'${message.command}' is not supported for Android content URIs" - } - }""")) - return true - } - } - - val root = java.nio.file.Paths.get(configuration.getRootDirectory()) - if (message.has("path")) { - var path = message.get("path") - message.set("path", root.resolve(java.nio.file.Paths.get(path)).toString()) - } - - if (message.has("src")) { - var src = message.get("src") - message.set("src", root.resolve(java.nio.file.Paths.get(src)).toString()) - } - - if (message.has("dest")) { - var dest = message.get("dest") - message.set("dest", root.resolve(java.nio.file.Paths.get(dest)).toString()) - } - - if (message.has("dst")) { - var dest = message.get("dst") - message.set("dst", root.resolve(java.nio.file.Paths.get(dest)).toString()) - } - } - - val request = RouteRequest(this.nextRequestId++, callback) - this.requests[request.id] = request - - return this.route(message.toString(), message.bytes, request.id) - } - - fun onInternalRouteResponse ( - id: Long, - seq: String, - source: String, - value: String? = null, - headersString: String? = null, - bytes: ByteArray? = null - ) { - val headers = try { - headersString - ?.split("\n") - ?.map { it.split(":", limit=3) } - ?.map { it.elementAt(0) to it.elementAt(1) } - ?.toMap() - } catch (err: Exception) { - null - } - - val result = Result(id, seq, source, value ?: "", bytes, headers ?: emptyMap<String, String>()) - - this.onResult(result) - } - - open fun onResult (result: Result) { - val semaphore = this.semaphore - val activity = this.runtime.get()?.activity?.get() - - this.requests[result.id]?.apply { - kotlin.concurrent.thread { - semaphore.acquireUninterruptibly() - - if (activity != null) { - activity.runOnUiThread { - semaphore.release() - } - } - - callback(result) - - if (activity == null) { - semaphore.release() - } - } - } - - if (this.requests.contains(result.id)) { - this.requests.remove(result.id) - } - } - - @Throws(java.lang.Exception::class) - external fun alloc (runtimePointer: Long): Long; - - @Throws(java.lang.Exception::class) - external fun dealloc (): Boolean; - - @Throws(java.lang.Exception::class) - external fun route (msg: String, bytes: ByteArray?, requestId: Long): Boolean; - - @Throws(java.lang.Exception::class) - external fun emit (event: String, data: String = ""): Boolean; -} diff --git a/src/android/internal.hh b/src/android/internal.hh deleted file mode 100644 index 1e0dec25e3..0000000000 --- a/src/android/internal.hh +++ /dev/null @@ -1,278 +0,0 @@ -#ifndef SSC_ANDROID_INTERNAL_H -#define SSC_ANDROID_INTERNAL_H - -#include "../core/core.hh" -#include "../ipc/ipc.hh" -#include "../window/options.hh" - -/** - * Defined by the Socket preprocessor - */ -#define PACKAGE_NAME __BUNDLE_IDENTIFIER__ - -/** - * Creates a named native package export for the configured bundle suitable for - * for definition only. - * @param name The name of the package function to export - */ -#define external(namespace, name) \ - JNIEXPORT JNICALL Java___BUNDLE_IDENTIFIER___##namespace##_##name - -/** - * Gets class for object for `self` from `env`. - */ -#define GetObjectClassFromEnvironment(env, self) env->GetObjectClass(self) - -/** - * Get field on object `self` from `env`. - */ -#define GetObjectClassFieldFromEnvironment(env, self, Type, field, sig) \ - ({ \ - auto Class = GetObjectClassFromEnvironment(env, self); \ - auto id = env->GetFieldID(Class, field, sig); \ - env->Get##Type##Field(self, id); \ - }) - -/** - * Gets the JNI `Exception` class from environment. - */ -#define GetExceptionClassFromEnvironment(env) \ - env->FindClass("java/lang/Exception") - -/** - */ -#define CallObjectClassMethodFromEnvironment(env, object, method, sig, ...) \ - ({ \ - auto Class = env->GetObjectClass(object); \ - auto ID = env->GetMethodID(Class, method, sig); \ - env->CallObjectMethod(object, ID, ##__VA_ARGS__); \ - }) - -#define CallVoidClassMethodFromEnvironment(env, object, method, sig, ...) \ - ({ \ - auto Class = env->GetObjectClass(object); \ - auto ID = env->GetMethodID(Class, method, sig); \ - env->CallVoidMethod(object, ID, ##__VA_ARGS__); \ - }) - -/** - * Generic `Exception` throw helper - */ -#define Throw(env, E) \ - ({ \ - env->ThrowNew(GetExceptionClassFromEnvironment(env), E); \ - (void) 0; \ - }) - -/** - * Translate a libuv error to a message suitable for `Throw(...)` - */ -#define UVException(code) uv_strerror(code) - -/** - * Errors thrown from the JNI/NDK bindings - */ -#define AssetManagerIsNotReachableException \ - "AssetManager is not reachable through binding" -#define ExceptionCheckException "ExceptionCheck" -#define JavaScriptPreloadSourceNotInitializedException \ - "JavaScript preload source is not initialized" -#define BridgeNotInitializedException "Bridge is not initialized" -#define CoreJavaVMNotInitializedException "Core JavaVM is not initialized" -#define CoreNotInitializedException "Core is not initialized" -#define CoreRefsNotInitializedException "Core refs are not initialized" -#define RuntimeNotInitializedException "Runtime is not initialized" -#define RootDirectoryIsNotReachableException \ - "Root directory in file system is not reachable through binding" -#define UVLoopNotInitializedException "UVLoop is not initialized" -#define WindowNotInitializedException "Window is not initialized" - -namespace SSC::android { - struct JVMEnvironment { - JavaVM* jvm = nullptr; - int jniVersion = 0; - - JVMEnvironment (JNIEnv* env) { - this->jniVersion = env->GetVersion(); - env->GetJavaVM(&jvm); - } - - int version () { - return this->jniVersion; - } - - JavaVM* get () { - return this->jvm; - } - }; - - struct JNIEnvironmentAttachment { - JNIEnv *env = nullptr; - JavaVM *jvm = nullptr; - int status = 0; - int version = 0; - bool attached = false; - - JNIEnvironmentAttachment () = default; - JNIEnvironmentAttachment (JavaVM *jvm, int version) { - this->attach(jvm, version); - } - - ~JNIEnvironmentAttachment () { - this->detach(); - } - - void attach (JavaVM *jvm, int version) { - this->jvm = jvm; - this->version = version; - - if (jvm != nullptr) { - this->status = this->jvm->GetEnv((void **) &this->env, this->version); - - if (this->status == JNI_EDETACHED) { - this->attached = this->jvm->AttachCurrentThread(&this->env, 0); - } - } - } - - void detach () { - auto jvm = this->jvm; - auto attached = this->attached; - - if (this->hasException()) { - this->printException(); - } - - this->env = nullptr; - this->jvm = nullptr; - this->status = 0; - this->attached = false; - - if (attached && jvm != nullptr) { - jvm->DetachCurrentThread(); - } - } - - inline bool hasException () { - return this->env != nullptr && this->env->ExceptionCheck(); - } - - inline void printException () { - if (this->env != nullptr) { - this->env->ExceptionDescribe(); - } - } - }; - - /** - * A container for a JNI string (jstring). - */ - class StringWrap { - JNIEnv *env = nullptr; - jstring ref = nullptr; - const char *string = nullptr; - size_t length = 0; - jboolean needsRelease = false; - - public: - StringWrap (JNIEnv *env); - StringWrap (const StringWrap ©); - StringWrap (JNIEnv *env, jstring ref); - StringWrap (JNIEnv *env, String string); - StringWrap (JNIEnv *env, const char *string); - ~StringWrap (); - - void set (String string); - void set (const char *string); - void set (jstring ref); - void release (); - - const String str (); - const jstring j_str (); - const char * c_str (); - const size_t size (); - - const StringWrap & - operator= (const StringWrap &string) { - *this = string; - this->needsRelease = false; - return *this; - } - }; - - class Runtime : public Core { - public: - static auto from (JNIEnv* env, jobject self) { - auto pointer = GetObjectClassFieldFromEnvironment(env, self, Long, "pointer", "J"); - return reinterpret_cast<Runtime*>(pointer); - } - - static auto from (jlong pointer) { - return reinterpret_cast<Runtime*>(pointer); - } - - JNIEnv *env = nullptr; - jobject self = nullptr; - jlong pointer = 0; - String rootDirectory = ""; - bool isEmulator = false; - - Runtime (JNIEnv* env, jobject self, String rootDirectory); - ~Runtime (); - bool isPermissionAllowed (const String&) const; - }; - - class Bridge : public IPC::Bridge { - public: - static auto from (JNIEnv* env, jobject self) { - auto pointer = GetObjectClassFieldFromEnvironment(env, self, Long, "pointer", "J"); - return reinterpret_cast<Bridge*>(pointer); - } - - static auto from (jlong pointer) { - return reinterpret_cast<Bridge*>(pointer); - } - - JNIEnv *env = nullptr; - jobject self = nullptr; - jlong pointer = 0; - Runtime* runtime = nullptr; - - Bridge (JNIEnv* env, jobject self, Runtime* runtime); - ~Bridge (); - }; - - class Window { - public: - static auto from (JNIEnv* env, jobject self) { - auto pointer = GetObjectClassFieldFromEnvironment(env, self, Long, "pointer", "J"); - return reinterpret_cast<Window*>(pointer); - } - - static auto from (jlong pointer) { - return reinterpret_cast<Window*>(pointer); - } - - JNIEnv* env = nullptr; - jobject self = nullptr; - jlong pointer = 0; - Bridge* bridge = nullptr; - Map config; - String preloadSource; - WindowOptions options; - Map envvars; - - Window ( - JNIEnv* env, - jobject self, - Bridge* bridge, - WindowOptions options - ); - - ~Window (); - - void evaluateJavaScript (String source, JVMEnvironment& jvm); - }; -} - -#endif diff --git a/src/android/main.kt b/src/android/main.kt index 1528e3ae9a..92fb6e47f1 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -1,309 +1,6 @@ +// vim: set sw=2: package __BUNDLE_IDENTIFIER__ -object console { - val TAG = "Console" - fun log (string: String) { - android.util.Log.i(TAG, string) - } +import socket.runtime.app.AppActivity - fun info (string: String) { - android.util.Log.i(TAG, string) - } - - fun debug (string: String) { - android.util.Log.d(TAG, string) - } - - fun error (string: String) { - android.util.Log.e(TAG, string) - } -} - -class PermissionRequest (callback: (Boolean) -> Unit) { - val id: Int = (0..16384).random().toInt() - val callback = callback -} - -/** - * An entry point for the main activity specified in - * `AndroidManifest.xml` and which can be overloaded in `socket.ini` for - * advanced usage. - - * Main `android.app.Activity` class for the `WebViewClient`. - * @see https://developer.android.com/reference/kotlin/android/app/Activity - */ -open class MainActivity : WebViewActivity() { - override open protected val TAG = "Mainctivity" - open lateinit var notificationChannel: android.app.NotificationChannel - override open lateinit var runtime: Runtime - override open lateinit var window: Window - - val permissionRequests = mutableListOf<PermissionRequest>() - val filePicker = WebViewFilePicker(this) - - companion object { - init { - System.loadLibrary("socket-runtime") - } - } - - fun checkPermission (permission: String): Boolean { - val status = androidx.core.content.ContextCompat.checkSelfPermission( - this.applicationContext, - permission - ) - - if (status == android.content.pm.PackageManager.PERMISSION_GRANTED) { - return true - } - - return false - } - - fun requestPermissions (permissions: Array<String>, callback: (Boolean) -> Unit) { - val request = PermissionRequest(callback) - this.permissionRequests.add(request) - androidx.core.app.ActivityCompat.requestPermissions( - this, - permissions, - request.id - ) - } - - fun showFileSystemPicker ( - options: WebViewFilePickerOptions, - callback: (Array<android.net.Uri>) -> Unit - ) : Boolean { - val filePicker = this.filePicker - this.runOnUiThread { - filePicker.launch(options, callback) - } - return true - } - - override fun onCreate (state: android.os.Bundle?) { - // called before `super.onCreate()` - this.supportActionBar?.hide() - this.getWindow()?.statusBarColor = android.graphics.Color.TRANSPARENT - - super.onCreate(state) - - this.notificationChannel = android.app.NotificationChannel( - "__BUNDLE_IDENTIFIER__", - "__BUNDLE_IDENTIFIER__ Notifications", - android.app.NotificationManager.IMPORTANCE_DEFAULT - ) - - this.runtime = Runtime(this, RuntimeConfiguration( - assetManager = this.applicationContext.resources.assets, - rootDirectory = this.getRootDirectory(), - - exit = { code -> - console.log("__EXIT_SIGNAL__=${code}") - this.finishAndRemoveTask() - }, - - openExternal = { value -> - val uri = android.net.Uri.parse(value) - val action = android.content.Intent.ACTION_VIEW - val intent = android.content.Intent(action, uri) - this.startActivity(intent) - } - )) - - this.runtime.setIsEmulator( - ( - android.os.Build.BRAND.startsWith("generic") && - android.os.Build.DEVICE.startsWith("generic") - ) || - android.os.Build.FINGERPRINT.startsWith("generic") || - android.os.Build.FINGERPRINT.startsWith("unknown") || - android.os.Build.HARDWARE.contains("goldfish") || - android.os.Build.HARDWARE.contains("ranchu") || - android.os.Build.MODEL.contains("google_sdk") || - android.os.Build.MODEL.contains("Emulator") || - android.os.Build.MODEL.contains("Android SDK built for x86") || - android.os.Build.MANUFACTURER.contains("Genymotion") || - android.os.Build.PRODUCT.contains("sdk_google") || - android.os.Build.PRODUCT.contains("google_sdk") || - android.os.Build.PRODUCT.contains("sdk") || - android.os.Build.PRODUCT.contains("sdk_x86") || - android.os.Build.PRODUCT.contains("sdk_gphone64_arm64") || - android.os.Build.PRODUCT.contains("vbox86p") || - android.os.Build.PRODUCT.contains("emulator") || - android.os.Build.PRODUCT.contains("simulator") - ) - - this.window = Window(this.runtime, this) - - this.window.load() - this.runtime.start() - - if (this.runtime.isPermissionAllowed("notifications")) { - val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager - notificationManager.createNotificationChannel(this.notificationChannel) - } - } - - override fun onStart () { - this.runtime.start() - super.onStart() - - val window = this.window - val action: String? = this.intent?.action - val data: android.net.Uri? = this.intent?.data - - if (action != null && data != null) { - this.onNewIntent(this.intent) - } - } - - override fun onResume () { - this.runtime.start() - return super.onResume() - } - - override fun onPause () { - this.runtime.stop() - return super.onPause() - } - - override fun onStop () { - this.runtime.stop() - return super.onStop() - } - - override fun onDestroy () { - this.runtime.destroy() - return super.onDestroy() - } - - override fun onNewIntent (intent: android.content.Intent) { - super.onNewIntent(intent) - val window = this.window - val action = intent.action - val data = intent.data - val id = intent.extras?.getCharSequence("id")?.toString() - - if (action == null) { - return - } - - when (action) { - "android.intent.action.MAIN", - "android.intent.action.VIEW" -> { - val scheme = data?.scheme ?: return - val applicationProtocol = this.runtime.getConfigValue("meta_application_protocol") - if ( - applicationProtocol.length > 0 && - scheme.startsWith(applicationProtocol) - ) { - window.bridge.emit("applicationurl", """{ - "url": "$data" - }""") - } - } - - "notification.response.default" -> { - window.bridge.emit("notificationresponse", """{ - "id": "$id", - "action": "default" - }""") - } - - "notification.response.dismiss" -> { - window.bridge.emit("notificationresponse", """{ - "id": "$id", - "action": "dismiss" - }""") - } - } - } - - override fun onActivityResult ( - requestCode: Int, - resultCode: Int, - intent: android.content.Intent? - ) { - super.onActivityResult(requestCode, resultCode, intent) - } - - override fun onPageStarted ( - view: android.webkit.WebView, - url: String, - bitmap: android.graphics.Bitmap? - ) { - super.onPageStarted(view, url, bitmap) - this.window.onPageStarted(view, url, bitmap) - } - - override fun onPageFinished ( - view: android.webkit.WebView, - url: String - ) { - super.onPageFinished(view, url) - this.window.onPageFinished(view, url) - } - - override fun onSchemeRequest ( - request: android.webkit.WebResourceRequest, - response: android.webkit.WebResourceResponse, - stream: java.io.PipedOutputStream - ): Boolean { - return this.window.onSchemeRequest(request, response, stream) - } - - override fun onRequestPermissionsResult ( - requestCode: Int, - permissions: Array<String>, - grantResults: IntArray - ) { - for (request in this.permissionRequests) { - if (request.id == requestCode) { - this.permissionRequests.remove(request) - request.callback(grantResults.all { r -> - r == android.content.pm.PackageManager.PERMISSION_GRANTED - }) - break - } - } - - var i = 0 - val seen = mutableSetOf<String>() - for (permission in permissions) { - val granted = ( - grantResults[i++] == android.content.pm.PackageManager.PERMISSION_GRANTED - ) - - var name = "" - when (permission) { - "android.permission.ACCESS_COARSE_LOCATION", - "android.permission.ACCESS_FINE_LOCATION" -> { - name = "geolocation" - } - - "android.permission.POST_NOTIFICATIONS" -> { - name = "notifications" - } - } - - if (seen.contains(name)) { - continue - } - - if (name.length == 0) { - continue - } - - seen.add(name) - - this.runOnUiThread { - val state = if (granted) "granted" else "denied" - window.bridge.emit("permissionchange", """{ - "name": "$name", - "state": "$state" - }""") - } - } - } -} +open class MainActivity : AppActivity() {} diff --git a/src/android/runtime.cc b/src/android/runtime.cc deleted file mode 100644 index b3231675aa..0000000000 --- a/src/android/runtime.cc +++ /dev/null @@ -1,214 +0,0 @@ -#include "internal.hh" - -using namespace SSC::android; - -namespace SSC::android { - Runtime::Runtime (JNIEnv* env, jobject self, String rootDirectory) - : SSC::Core() - { - this->env = env; - this->self = env->NewGlobalRef(self); - this->pointer = reinterpret_cast<jlong>(this); - this->rootDirectory = rootDirectory; - } - - bool Runtime::isPermissionAllowed (const String& name) const { - static const auto config = SSC::getUserConfig(); - const auto permission = String("permissions_allow_") + replace(name, "-", "_"); - - // `true` by default - if (!config.contains(permission)) { - return true; - } - - return config.at(permission) != "false"; - } - - Runtime::~Runtime () { - this->env->DeleteGlobalRef(this->self); - } -} - -extern "C" { - jlong external(Runtime, alloc)( - JNIEnv *env, - jobject self, - jstring rootDirectory - ) { - auto runtime = new Runtime(env, self, StringWrap(env, rootDirectory).str()); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return 0; - } - - return runtime->pointer; - } - - jboolean external(Runtime, dealloc)( - JNIEnv *env, - jobject self - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - delete runtime; - return true; - } - - jboolean external(Runtime, isDebugEnabled) ( - JNIEnv* env, - jobject self - ) { - return SSC::isDebugEnabled(); - } - - jboolean external(Runtime, startEventLoop)( - JNIEnv *env, - jobject self - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - runtime->runEventLoop(); - return true; - } - - jboolean external(Runtime, stopEventLoop)( - JNIEnv *env, - jobject self - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - runtime->stopEventLoop(); - return true; - } - - jboolean external(Runtime, startTimers)( - JNIEnv *env, - jobject self - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - runtime->startTimers(); - return true; - } - - jboolean external(Runtime, stopTimers)( - JNIEnv *env, - jobject self - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - runtime->stopTimers(); - return true; - } - - jboolean external(Runtime, pause)( - JNIEnv *env, - jobject self - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - runtime->pauseAllPeers(); - runtime->stopTimers(); - runtime->stopEventLoop(); - - return true; - } - - jboolean external(Runtime, resume)( - JNIEnv *env, - jobject self - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - runtime->runEventLoop(); - runtime->resumeAllPeers(); - - return true; - } - - jboolean external(Runtime, isPermissionAllowed)( - JNIEnv *env, - jobject self, - jstring permission - ) { - auto runtime = Runtime::from(env, self); - auto name = StringWrap(env, permission).str(); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - return runtime->isPermissionAllowed(name); - } - - jboolean external(Runtime, setIsEmulator)( - JNIEnv *env, - jobject self, - jboolean value - ) { - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return false; - } - - runtime->isEmulator = value; - return true; - } - - jstring external(Runtime, getConfigValue)( - JNIEnv *env, - jobject self, - jstring keyString - ) { - static auto config = SSC::getUserConfig(); - auto runtime = Runtime::from(env, self); - - if (runtime == nullptr) { - Throw(env, RuntimeNotInitializedException); - return nullptr; - } - - auto key = StringWrap(env, keyString).str(); - auto value = config[key]; - return env->NewStringUTF(value.c_str()); - } -} diff --git a/src/android/runtime.kt b/src/android/runtime.kt deleted file mode 100644 index df07af0fa9..0000000000 --- a/src/android/runtime.kt +++ /dev/null @@ -1,97 +0,0 @@ -// vim: set sw=2: -package __BUNDLE_IDENTIFIER__ -import java.lang.ref.WeakReference - -interface IRuntimeConfiguration { - val rootDirectory: String - val assetManager: android.content.res.AssetManager - val exit: (Int) -> Unit - val openExternal: (String) -> Unit -} - -data class RuntimeConfiguration ( - override val rootDirectory: String, - override val assetManager: android.content.res.AssetManager, - override val exit: (Int) -> Unit, - override val openExternal: (String) -> Unit -) : IRuntimeConfiguration - -open class Runtime ( - activity: MainActivity, - configuration: RuntimeConfiguration -) { - var pointer = alloc(activity.getRootDirectory()) - var activity = WeakReference(activity) - val configuration = configuration; - var isRunning = false - - fun finalize () { - if (this.pointer > 0) { - this.dealloc() - } - - this.pointer = 0 - } - - fun exit (code: Int) { - this.configuration.exit(code) - } - - fun openExternal (value: String) { - this.configuration.openExternal(value) - } - - fun start () { - if (!this.isRunning) { - this.resume() - this.isRunning = true - } - } - - fun stop () { - if (this.isRunning) { - this.pause() - this.isRunning = false - } - } - - fun destroy () { - this.stop() - this.finalize() - } - - @Throws(java.lang.Exception::class) - external fun alloc (rootDirectory: String): Long; - - @Throws(java.lang.Exception::class) - external fun dealloc (): Boolean; - - external fun isDebugEnabled (): Boolean; - - @Throws(java.lang.Exception::class) - external fun pause (): Boolean; - - @Throws(java.lang.Exception::class) - external fun resume (): Boolean; - - @Throws(java.lang.Exception::class) - external fun startEventLoop (): Boolean; - - @Throws(java.lang.Exception::class) - external fun stopEventLoop (): Boolean; - - @Throws(java.lang.Exception::class) - external fun startTimers (): Boolean; - - @Throws(java.lang.Exception::class) - external fun stopTimers (): Boolean; - - @Throws(java.lang.Exception::class) - external fun isPermissionAllowed (permission: String): Boolean; - - @Throws(java.lang.Exception::class) - external fun setIsEmulator (value: Boolean): Boolean; - - @Throws(java.lang.Exception::class) - external fun getConfigValue (key: String): String; -} diff --git a/src/android/webview.kt b/src/android/webview.kt deleted file mode 100644 index eed7103164..0000000000 --- a/src/android/webview.kt +++ /dev/null @@ -1,752 +0,0 @@ -// vim: set sw=2: -package __BUNDLE_IDENTIFIER__ -import java.lang.ref.WeakReference - -fun decodeURIComponent (string: String): String { - val normalized = string.replace("+", "%2B") - return java.net.URLDecoder.decode(normalized, "UTF-8").replace("%2B", "+") -} - -fun isAndroidAssetsUri (uri: android.net.Uri): Boolean { - val scheme = uri.scheme - val host = uri.host - // handle no path segments, not currently required but future proofing - val path = uri.pathSegments?.get(0) - - if (host == "appassets.androidplatform.net") { - return true - } - - if (scheme == "file" && host == "" && path == "android_asset") { - return true - } - - return false -} - -/** - * @see https://developer.android.com/reference/kotlin/android/webkit/WebView - */ -open class WebView (context: android.content.Context) : android.webkit.WebView(context) - -/** - */ -open class WebViewFilePickerOptions ( - params: android.webkit.WebChromeClient.FileChooserParams? = null, - mimeTypes: Array<String> = arrayOf<String>(), - multiple: Boolean = false, - files: Boolean = true, - directories: Boolean = false -) { - val params = params - val multiple = multiple - val files = files - val directories = directories - var mimeTypes = mimeTypes - - init { - if (params != null && params.acceptTypes.size > 0) { - this.mimeTypes += params.acceptTypes - } - } -} - -open class WebViewFilePicker ( - activity: MainActivity -) { - protected val activity = WeakReference(activity) - var callback: ((Array<android.net.Uri>) -> Unit)? = null - - val launcherForSingleItem = activity.registerForActivityResult( - androidx.activity.result.contract.ActivityResultContracts.GetContent(), - { uri -> this.handleCallback(uri) } - ) - - val launcherForMulitpleItems = activity.registerForActivityResult( - androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents(), - { uris -> this.handleCallback(uris) } - ) - - val launcherForSingleDocument = activity.registerForActivityResult( - androidx.activity.result.contract.ActivityResultContracts.OpenDocument(), - { uri -> this.handleCallback(uri) } - ) - - val launcherForMulitpleDocuments = activity.registerForActivityResult( - androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments(), - { uris -> this.handleCallback(uris) } - ) - - val launcherForSingleVisualMedia = activity.registerForActivityResult( - androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia(), - { uri -> this.handleCallback(uri) } - ) - - val launcherForMultipleVisualMedia = activity.registerForActivityResult( - androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia(), - { uris -> this.handleCallback(uris) } - ) - - fun handleCallback (uris: Array<android.net.Uri>) { - val callback = this.callback - this.callback = null - if (callback != null) { - callback(uris) - } - } - - fun handleCallback (uris: List<android.net.Uri>) { - return this.handleCallback(uris.toTypedArray()) - } - - fun handleCallback (uri: android.net.Uri?) { - return this.handleCallback( - if (uri != null) { arrayOf(uri) } - else { arrayOf<android.net.Uri>() } - ) - } - - fun cancel () { - this.handleCallback(null) - } - - fun launch ( - options: WebViewFilePickerOptions, - callback: (Array<android.net.Uri>) -> Unit - ) { - this.cancel() - - var mimeType: String = "*/*" - - if (options.mimeTypes.size > 0) { - mimeType = options.mimeTypes[0] - } - - if (mimeType.length == 0) { - mimeType = "*/*" - } - - this.callback = callback - - if (options.multiple) { - this.launcherForMulitpleItems.launch(mimeType) - } else { - this.launcherForSingleItem.launch(mimeType) - } - } -} - -/** - * @see https://developer.android.com/reference/kotlin/android/webkit/WebViewClient - */ -open class WebChromeClient (activity: MainActivity) : android.webkit.WebChromeClient() { - protected val activity = WeakReference(activity) - - override fun onGeolocationPermissionsShowPrompt ( - origin: String, - callback: android.webkit.GeolocationPermissions.Callback - ) { - val runtime = this.activity.get()?.runtime ?: return callback(origin, false, false) - val allowed = runtime.isPermissionAllowed("geolocation") - - callback(origin, allowed, allowed) - } - - override fun onPermissionRequest (request: android.webkit.PermissionRequest) { - val runtime = this.activity.get()?.runtime ?: return request.deny() - val resources = request.resources - var grants = mutableListOf<String>() - for (resource in resources) { - when (resource) { - android.webkit.PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { - if (runtime.isPermissionAllowed("microphone") || runtime.isPermissionAllowed("user_media")) { - grants.add(android.webkit.PermissionRequest.RESOURCE_AUDIO_CAPTURE) - } - } - - android.webkit.PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { - if (runtime.isPermissionAllowed("camera") || runtime.isPermissionAllowed("user_media")) { - grants.add(android.webkit.PermissionRequest.RESOURCE_VIDEO_CAPTURE) - } - } - - // auto grant EME - android.webkit.PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> { - grants.add(android.webkit.PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID) - } - } - } - - if (grants.size > 0) { - request.grant(grants.toTypedArray()) - } else { - request.deny() - } - } - - override fun onProgressChanged ( - webview: android.webkit.WebView, - progress: Int - ) { - val activity = this.activity.get() ?: return; - activity.window.onProgressChanged(webview, progress) - } - - override fun onShowFileChooser ( - webview: android.webkit.WebView, - callback: android.webkit.ValueCallback<Array<android.net.Uri>>, - params: android.webkit.WebChromeClient.FileChooserParams - ): Boolean { - val activity = this.activity.get() ?: return false; - - super.onShowFileChooser(webview, callback, params) - - val options = WebViewFilePickerOptions(params) - activity.showFileSystemPicker(options, fun (uris: Array<android.net.Uri>) { - callback.onReceiveValue(uris) - }) - return true - } -} - -/** - * A container for a resolved URL path laoded in the WebView. - */ -final class WebViewURLPathResolution (path: String, redirect: Boolean = false) { - val path = path - val redirect = redirect -} - -/** - * @see https://developer.android.com/reference/kotlin/android/webkit/WebViewClient - */ -open class WebViewClient (activity: WebViewActivity) : android.webkit.WebViewClient() { - protected val activity = WeakReference(activity) - open protected val TAG = "WebViewClient" - open protected var rootDirectory = "" - - fun putRootDirectory(rootDirectory: String) { - this.rootDirectory = rootDirectory - } - - open protected var assetLoader: androidx.webkit.WebViewAssetLoader = androidx.webkit.WebViewAssetLoader.Builder() - .addPathHandler( - "/assets/", - androidx.webkit.WebViewAssetLoader.AssetsPathHandler(activity) - ) - .build() - - /** - * Handles URL loading overrides for various URI schemes. - */ - override fun shouldOverrideUrlLoading ( - view: android.webkit.WebView, - request: android.webkit.WebResourceRequest - ): Boolean { - val url = request.url - - if (url.scheme == "http" || url.scheme == "https") { - return false - } - - if (url.scheme == "ipc" || url.scheme == "file" || url.scheme == "socket" || isAndroidAssetsUri(url)) { - return true - } - - val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, url) - val activity = this.activity.get() - - if (activity == null) { - return false - } - - try { - activity.startActivity(intent) - } catch (err: Error) { - // @TODO(jwelre): handle this error gracefully - console.error(err.toString()) - return false - } - - return true - } - - fun resolveURLPathForWebView (input: String? = null): WebViewURLPathResolution? { - var path = input ?: return null - val activity = this.activity.get() ?: return null - val assetManager = activity.getAssetManager() - - if (path == "/") { - try { - val htmlPath = "index.html" - val stream = assetManager.open(htmlPath) - stream.close() - return WebViewURLPathResolution("/" + htmlPath) - } catch (_: Exception) {} - } - - if (path.startsWith("/")) { - path = path.substring(1, path.length) - } else if (path.startsWith("./")) { - path = path.substring(2, path.length) - } - - try { - val htmlPath = path - val stream = assetManager.open(htmlPath) - stream.close() - return WebViewURLPathResolution("/" + htmlPath) - } catch (_: Exception) {} - - if (path.endsWith("/")) { - try { - val list = assetManager.list(path) - if (list != null && list.size > 0) { - try { - val htmlPath = path + "index.html" - val stream = assetManager.open(htmlPath) - stream.close() - return WebViewURLPathResolution("/" + htmlPath) - } catch (_: Exception) {} - } - } catch (_: Exception) {} - - return null - } else { - try { - val htmlPath = path + "/index.html" - val stream = assetManager.open(htmlPath) - stream.close() - return WebViewURLPathResolution("/" + path + "/", true) - } catch (_: Exception) {} - } - - try { - val htmlPath = path + ".html" - val stream = assetManager.open(htmlPath) - stream.close() - return WebViewURLPathResolution("/" + htmlPath) - } catch (_: Exception) {} - - return null - } - - override fun shouldInterceptRequest ( - view: android.webkit.WebView, - request: android.webkit.WebResourceRequest - ): android.webkit.WebResourceResponse? { - val activity = this.activity.get() ?: return null - val runtime = activity.runtime - var url = request.url - - // should be set in window loader - assert(rootDirectory.length > 0) - - if ( - (url.scheme == "socket" && url.host == "__BUNDLE_IDENTIFIER__") || - (url.scheme == "https" && url.host == "__BUNDLE_IDENTIFIER__") - ) { - var path = url.path - var redirect = false - val resolved = resolveURLPathForWebView(path) - - if (resolved != null) { - path = resolved.path - } - - if (resolved != null && resolved.redirect) { - redirect = true - } - - if (path == null) { - return null - } - - if (redirect && resolved != null) { - val redirectURL = "${url.scheme}://${url.host}${resolved.path}" - val redirectSource = """ - <meta http-equiv="refresh" content="0; url='${resolved.path}'" /> - """ - - val stream = java.io.PipedOutputStream() - val response = android.webkit.WebResourceResponse( - "text/html", - "utf-8", - java.io.PipedInputStream(stream) - ) - - response.responseHeaders = mapOf( - "Location" to redirectURL, - "Content-Location" to redirectURL - ) - - response.setStatusCodeAndReasonPhrase(200, "OK") - // prevent piped streams blocking each other, have to write on a separate thread if data > 1024 bytes - kotlin.concurrent.thread { - stream.write(redirectSource.toByteArray(), 0, redirectSource.length) - stream.close() - } - - return response - } - - if (path.startsWith("/")) { - path = path.substring(1, path.length) - } - - url = android.net.Uri.Builder() - .scheme("https") - .authority("appassets.androidplatform.net") - .path("/assets/${path}") - .build() - } - - // look for updated resources in ${pwd}/files - // live update systems can write to /files (/assets is read only) - if (url.host == "appassets.androidplatform.net" && url.pathSegments.get(0) == "assets") { - var first = true - val filePath = StringBuilder(rootDirectory) - for (item in url.pathSegments) { - if (!first) { - filePath.append("/${item}") - } - first = false - } - - val file = java.io.File(filePath.toString()) - if (file.exists() && !file.isDirectory()) { - val response = android.webkit.WebResourceResponse( - if (filePath.toString().endsWith(".js")) "text/javascript" else "text/html", - "utf-8", - java.io.FileInputStream(file) - ) - - val webviewHeaders = runtime.getConfigValue("webview_headers").split("\n") - val headers = mutableMapOf( - "Access-Control-Allow-Origin" to "*", - "Access-Control-Allow-Headers" to "*", - "Access-Control-Allow-Methods" to "*" - ) - - for (line in webviewHeaders) { - val parts = line.split(":", limit = 2) - if (parts.size == 2) { - val key = parts[0].trim() - val value = parts[1].trim() - headers[key] = value - } - } - - response.responseHeaders = headers.toMap() - - return response - } else { - // default to normal asset loader behaviour - } - } - - if (url.scheme == "socket") { - var path = url.toString().replace("socket:", "") - - if (!path.endsWith(".js")) { - path += ".js" - } - - url = android.net.Uri.Builder() - .scheme("socket") - .authority("__BUNDLE_IDENTIFIER__") - .path("/socket/${path}") - .build() - - val moduleTemplate = """ -import module from '$url' -export * from '$url' -export default module - """ - - val stream = java.io.PipedOutputStream() - val response = android.webkit.WebResourceResponse( - "text/javascript", - "utf-8", - java.io.PipedInputStream(stream) - ) - - val webviewHeaders = runtime.getConfigValue("webview_headers").split("\n") - val headers = mutableMapOf( - "Access-Control-Allow-Origin" to "*", - "Access-Control-Allow-Headers" to "*", - "Access-Control-Allow-Methods" to "*" - ) - - for (line in webviewHeaders) { - val parts = line.split(":", limit = 2) - if (parts.size == 2) { - val key = parts[0].trim() - val value = parts[1].trim() - headers[key] = value - } - } - - response.responseHeaders = headers.toMap() - - // prevent piped streams blocking each other, - // have to write on a separate thread if data > 1024 bytes - kotlin.concurrent.thread { - stream.write(moduleTemplate.toByteArray(), 0, moduleTemplate.length) - stream.close() - } - - return response - } - - val assetManager = activity.getAssetManager() - var path = url.path - if (path != null && path.endsWith(".html") == true) { - path = path.replace("/assets/", "") - val preload = activity.window.getJavaScriptPreloadSource() - val assetStream = try { - assetManager.open(path, 2) - } catch (err: Error) { - return null - } - - var html = String(assetStream.readAllBytes()) - val script = """<script type="text/javascript">$preload</script>""" - val stream = java.io.PipedOutputStream() - val response = android.webkit.WebResourceResponse( - "text/html", - "utf-8", - java.io.PipedInputStream(stream) - ) - - val webviewHeaders = runtime.getConfigValue("webview_headers").split("\n") - val headers = mutableMapOf( - "Access-Control-Allow-Origin" to "*", - "Access-Control-Allow-Headers" to "*", - "Access-Control-Allow-Methods" to "*" - ) - - for (line in webviewHeaders) { - val parts = line.split(":", limit = 2) - if (parts.size == 2) { - val key = parts[0].trim() - val value = parts[1].trim() - headers[key] = value - } - } - - response.responseHeaders = headers.toMap() - - if (html.contains("<head>")) { - html = html.replace("<head>", """ - <head> - $script - """) - } else if (html.contains("<body>")) { - html = html.replace("<body>", """ - $script - <body> - """) - } else if (html.contains("<html>")){ - html = html.replace("<html>", """ - <html> - $script - """) - } else { - html = script + html - } - - kotlin.concurrent.thread { - stream.write(html.toByteArray(), 0, html.length) - stream.close() - } - - return response - } - - val assetLoaderResponse = this.assetLoader.shouldInterceptRequest(url) - - if (assetLoaderResponse != null) { - - val webviewHeaders = runtime.getConfigValue("webview_headers").split("\n") - val headers = mutableMapOf( - "Origin" to "${url.scheme}://${url.host}", - "Access-Control-Allow-Origin" to "*", - "Access-Control-Allow-Headers" to "*", - "Access-Control-Allow-Methods" to "*" - ) - - for (line in webviewHeaders) { - val parts = line.split(":", limit = 2) - if (parts.size == 2) { - val key = parts[0].trim() - val value = parts[1].trim() - headers[key] = value - } - } - - assetLoaderResponse.responseHeaders = headers.toMap() - - return assetLoaderResponse - } - - if (url.scheme != "ipc") { - return null - } - - when (url.host) { - "ping" -> { - val stream = java.io.ByteArrayInputStream("pong".toByteArray()) - val response = android.webkit.WebResourceResponse( - "text/plain", - "utf-8", - stream - ) - - response.responseHeaders = mapOf( - "Access-Control-Allow-Origin" to "*" - ) - - return response - } - - else -> { - if (request.method == "OPTIONS") { - val stream = java.io.PipedOutputStream() - val response = android.webkit.WebResourceResponse( - "text/plain", - "utf-8", - java.io.PipedInputStream(stream) - ) - - val webviewHeaders = runtime.getConfigValue("webview_headers").split("\n") - val headers = mutableMapOf( - "Access-Control-Allow-Origin" to "*", - "Access-Control-Allow-Headers" to "*", - "Access-Control-Allow-Methods" to "*" - ) - - for (line in webviewHeaders) { - val parts = line.split(":", limit = 2) - if (parts.size == 2) { - val key = parts[0].trim() - val value = parts[1].trim() - headers[key] = value - } - } - - response.responseHeaders = headers.toMap() - - stream.close() - return response - } - - val stream = java.io.PipedOutputStream() - val response = android.webkit.WebResourceResponse( - "application/octet-stream", - "utf-8", - java.io.PipedInputStream(stream) - ) - - response.responseHeaders = mapOf( - "Access-Control-Allow-Origin" to "*", - "Access-Control-Allow-Headers" to "*", - "Access-Control-Allow-Methods" to "*" - ) - - if (activity.onSchemeRequest(request, response, stream) == true) { - return response - } - - response.setStatusCodeAndReasonPhrase(404, "Not found") - stream.close() - - return response - } - } - } - - override fun onPageStarted ( - view: android.webkit.WebView, - url: String, - bitmap: android.graphics.Bitmap? - ) { - this.activity.get()?.onPageStarted(view, url, bitmap) - } - - override fun onPageFinished ( - view: android.webkit.WebView, - url: String - ) { - this.activity.get()?.onPageFinished(view, url) - } -} - -/** - * Main `android.app.Activity` class for the `WebViewClient`. - * @see https://developer.android.com/reference/kotlin/android/app/Activity - * @TODO(jwerle): look into `androidx.appcompat.app.AppCompatActivity` - */ -abstract class WebViewActivity : androidx.appcompat.app.AppCompatActivity() { - open protected val TAG = "WebViewActivity" - - open lateinit var client: WebViewClient - abstract var runtime: Runtime - abstract var window: Window - - open var webview: android.webkit.WebView? = null - - fun evaluateJavaScript ( - source: String, - callback: android.webkit.ValueCallback<String?>? = null - ) { - runOnUiThread { - webview?.evaluateJavascript(source, callback) - } - } - - fun getAssetManager (): android.content.res.AssetManager { - return this.applicationContext.resources.assets - } - - open fun getRootDirectory (): String { - return getExternalFilesDir(null)?.absolutePath - ?: "/sdcard/Android/data/__BUNDLE_IDENTIFIER__/files" - } - - /** - * Called when the `WebViewActivity` is first created - * @see https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle) - */ - override fun onCreate (state: android.os.Bundle?) { - super.onCreate(state) - - setContentView(R.layout.web_view_activity) - - this.client = WebViewClient(this) - this.webview = findViewById(R.id.webview) as android.webkit.WebView? - } - - open fun onPageStarted ( - view: android.webkit.WebView, - url: String, - bitmap: android.graphics.Bitmap? - ) { - console.log("WebViewActivity is loading: $url") - } - - open fun onPageFinished ( - view: android.webkit.WebView, - url: String - ) { - console.log("WebViewActivity finished loading: $url") - } - - open fun onSchemeRequest ( - request: android.webkit.WebResourceRequest, - response: android.webkit.WebResourceResponse, - stream: java.io.PipedOutputStream - ): Boolean { - return false - } -} diff --git a/src/android/window.cc b/src/android/window.cc deleted file mode 100644 index 8fa7f0b84c..0000000000 --- a/src/android/window.cc +++ /dev/null @@ -1,188 +0,0 @@ -#include "../core/core.hh" -#include "../ipc/ipc.hh" -#include "internal.hh" - -using namespace SSC::android; - -namespace SSC::android { - Window::Window ( - JNIEnv* env, - jobject self, - Bridge* bridge, - WindowOptions options - ) : options(options) { - this->env = env; - this->self = env->NewGlobalRef(self); - this->bridge = bridge; - this->config = SSC::getUserConfig(); - this->pointer = reinterpret_cast<jlong>(this); - - StringStream stream; - - for (auto const &var : parseStringList(this->config["build_env"])) { - auto key = trim(var); - - if (!Env::has(key)) { - continue; - } - - auto value = Env::get(key.c_str()); - - if (value.size() > 0) { - stream << key << "=" << encodeURIComponent(value) << "&"; - envvars[key] = value; - } - } - - StringWrap rootDirectory(env, (jstring) CallObjectClassMethodFromEnvironment( - env, - self, - "getRootDirectory", - "()Ljava/lang/String;" - )); - - const auto argv = this->config["ssc_argv"]; - - options.headless = this->config["build_headless"] == "true"; - options.debug = isDebugEnabled() ? true : false; - options.env = stream.str(); - options.cwd = rootDirectory.str(); - options.appData = this->config; - options.argv = argv; - options.isTest = argv.find("--test") != -1; - - preloadSource = createPreload(options, PreloadOptions { - .module = false - }); - } - - Window::~Window () { - this->env->DeleteGlobalRef(this->self); - } - - void Window::evaluateJavaScript (String source, JVMEnvironment& jvm) { - auto attachment = JNIEnvironmentAttachment { jvm.get(), jvm.version() }; - auto env = attachment.env; - if (!attachment.hasException()) { - auto sourceString = env->NewStringUTF(source.c_str()); - CallVoidClassMethodFromEnvironment( - env, - self, - "evaluateJavaScript", - "(Ljava/lang/String;)V", - sourceString - ); - - env->DeleteLocalRef(sourceString); - } - } -} - -extern "C" { - jlong external(Window, alloc)( - JNIEnv *env, - jobject self, - jlong bridgePointer - ) { - auto bridge = Bridge::from(bridgePointer); - - if (bridge == nullptr) { - Throw(env, BridgeNotInitializedException); - return 0; - } - - auto options = SSC::WindowOptions {}; - auto window = new Window(env, self, bridge, options); - auto jvm = JVMEnvironment(env); - - if (window == nullptr) { - Throw(env, WindowNotInitializedException); - return 0; - } - - bridge->router.evaluateJavaScriptFunction = [window, jvm](auto source) mutable { - window->evaluateJavaScript(source, jvm); - }; - - return window->pointer; - } - - jboolean external(Window, dealloc)( - JNIEnv *env, - jobject self - ) { - auto window = Window::from(env, self); - - if (window == nullptr) { - Throw(env, WindowNotInitializedException); - return false; - } - - delete window; - return true; - } - - jstring external(Window, getPathToFileToLoad)( - JNIEnv *env, - jobject self - ) { - auto window = Window::from(env, self); - - if (window == nullptr) { - Throw(env, WindowNotInitializedException); - return nullptr; - } - - auto filename = window->config["webview_root"]; - - if (filename.size() > 0) { - if (filename.ends_with("/")) { - return env->NewStringUTF((filename + "index.html").c_str()); - } else { - return env->NewStringUTF(filename.c_str()); - } - } - - return env->NewStringUTF("/index.html"); - } - - jstring external(Window, getJavaScriptPreloadSource)( - JNIEnv *env, - jobject self - ) { - auto window = Window::from(env, self); - - if (window == nullptr) { - Throw(env, WindowNotInitializedException); - return nullptr; - } - - auto source = window->preloadSource.c_str(); - - return env->NewStringUTF(source); - } - - jstring external(Window, getResolveToRenderProcessJavaScript)( - JNIEnv *env, - jobject self, - jstring seq, - jstring state, - jstring value - ) { - auto window = Window::from(env, self); - - if (window == nullptr) { - Throw(env, WindowNotInitializedException); - return nullptr; - } - - auto resolved = SSC::encodeURIComponent(StringWrap(env, value).str()); - auto source = SSC::getResolveToRenderProcessJavaScript( - StringWrap(env, seq).str(), - StringWrap(env, state).str(), - resolved - ); - - return env->NewStringUTF(source.c_str()); - } -} diff --git a/src/android/window.kt b/src/android/window.kt deleted file mode 100644 index 5f275d92f0..0000000000 --- a/src/android/window.kt +++ /dev/null @@ -1,188 +0,0 @@ -// vim: set sw=2: -package __BUNDLE_IDENTIFIER__ -import java.lang.ref.WeakReference - -open class Window (runtime: Runtime, activity: MainActivity) { - open protected val TAG = "Window" - - val bridge = Bridge(runtime, BridgeConfiguration( - getRootDirectory = { -> - this.getRootDirectory() - } - )) - - val userMessageHandler = UserMessageHandler(this) - val activity = WeakReference(activity) - val runtime = WeakReference(runtime) - val pointer = alloc(bridge.pointer) - var isLoading = false - - fun evaluateJavaScript (source: String) { - this.activity.get()?.evaluateJavaScript(source) - } - - fun getRootDirectory (): String { - return this.activity.get()?.getRootDirectory() ?: "" - } - - fun load () { - val runtime = this.runtime.get() ?: return - val isDebugEnabled = this.runtime.get()?.isDebugEnabled() ?: false - val filename = this.getPathToFileToLoad() - val activity = this.activity.get() ?: return - - val rootDirectory = this.getRootDirectory() - val preload = this.getJavaScriptPreloadSource() - - this.bridge.route("ipc://internal.setcwd?value=${rootDirectory}", null, fun (_: Result) { - activity.applicationContext - .getSharedPreferences("WebSettings", android.app.Activity.MODE_PRIVATE) - .edit() - .apply { - putString("scheme", "socket") - putString("hostname", "__BUNDLE_IDENTIFIER__") - apply() - } - - activity.runOnUiThread { - // enable/disable debug module in webview - android.webkit.WebView.setWebContentsDebuggingEnabled(isDebugEnabled) - - activity.webview?.apply { - // features - settings.javaScriptEnabled = true - - settings.domStorageEnabled = runtime.isPermissionAllowed("data_access") - settings.databaseEnabled = runtime.isPermissionAllowed("data_access") - - settings.setGeolocationEnabled(runtime.isPermissionAllowed("geolocation")) - settings.javaScriptCanOpenWindowsAutomatically = true - - // allow list - settings.allowFileAccess = true - settings.allowContentAccess = true - - // allow mixed content - settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW - - activity.client.putRootDirectory(rootDirectory) - - // clients - webViewClient = activity.client - webChromeClient = WebChromeClient(activity) - - addJavascriptInterface(userMessageHandler, "external") - - var baseUrl = "https://__BUNDLE_IDENTIFIER__$filename" - loadUrl(baseUrl) - } - } - }) - } - - open fun onSchemeRequest ( - request: android.webkit.WebResourceRequest, - response: android.webkit.WebResourceResponse, - stream: java.io.PipedOutputStream - ): Boolean { - return bridge.route(request.url.toString(), null, fun (result: Result) { - var bytes = result.value.toByteArray() - var contentType = "application/json" - - if (result.bytes != null) { - bytes = result.bytes - contentType = "application/octet-stream" - } - - response.apply { - setStatusCodeAndReasonPhrase(200, "OK") - setResponseHeaders(responseHeaders + result.headers) - setMimeType(contentType) - } - - kotlin.concurrent.thread { - try { - stream.write(bytes, 0, bytes.size) - } catch (err: Exception) { - if (err.message != "Pipe closed") { - console.error("onSchemeRequest(): ${err.toString()}") - } - } - - stream.close() - } - }) - } - - open fun onPageStarted ( - view: android.webkit.WebView, - url: String, - bitmap: android.graphics.Bitmap? - ) { - this.isLoading = true - } - - open fun onPageFinished ( - view: android.webkit.WebView, - url: String - ) { - this.isLoading = false - } - - open fun onProgressChanged ( - view: android.webkit.WebView, - progress: Int - ) { - } - - @Throws(java.lang.Exception::class) - external fun alloc (bridgePointer: Long): Long; - - @Throws(java.lang.Exception::class) - external fun dealloc (): Boolean; - - @Throws(java.lang.Exception::class) - external fun getPathToFileToLoad (): String; - - @Throws(java.lang.Exception::class) - external fun getJavaScriptPreloadSource (): String; - - @Throws(java.lang.Exception::class) - external fun getResolveToRenderProcessJavaScript ( - seq: String, - state: String, - value: String - ): String; -} - -/** - * External JavaScript interface attached to the webview at - * `window.external` - */ -open class UserMessageHandler (window: Window) { - val TAG = "UserMessageHandler" - - val namespace = "external" - val activity = window.bridge.runtime.get()?.activity - val runtime = window.bridge.runtime - val window = WeakReference(window) - - @android.webkit.JavascriptInterface - open fun postMessage (value: String): Boolean { - return this.postMessage(value, null) - } - - /** - * Low level external message handler - */ - @android.webkit.JavascriptInterface - open fun postMessage (value: String, bytes: ByteArray? = null): Boolean { - val bridge = this.window.get()?.bridge ?: return false - - return bridge.route(value, bytes, fun (result: Result) { - val window = this.window.get() ?: return - val javascript = window.getResolveToRenderProcessJavaScript(result.seq, "1", result.value) - this.window.get()?.evaluateJavaScript(javascript) - }) - } -} diff --git a/src/app/app.cc b/src/app/app.cc index d321f3b9dc..6d635ddc97 100644 --- a/src/app/app.cc +++ b/src/app/app.cc @@ -1,16 +1,8 @@ -#include "../window/window.hh" -#include "../ipc/ipc.hh" #include "app.hh" -#if defined(_WIN32) -#include <uxtheme.h> -#include <thread> -#pragma comment(lib, "UxTheme.lib") -#pragma comment(lib, "Dwmapi.lib") -#pragma comment(lib, "Gdi32.lib") -#endif +using namespace SSC; -#if defined(__APPLE__) +#if SOCKET_RUNTIME_PLATFORM_APPLE static dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class( DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, @@ -22,38 +14,48 @@ static dispatch_queue_t queue = dispatch_queue_create( qos ); -#if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR @implementation SSCApplicationDelegate +#if SOCKET_RUNTIME_PLATFORM_MACOS - (void) applicationDidFinishLaunching: (NSNotification*) notification { - self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; + self.statusItem = [NSStatusBar.systemStatusBar statusItemWithLength: NSVariableStatusItemLength]; +} + +- (void) applicationWillBecomeActive: (NSNotification*) notification { + dispatch_async(queue, ^{ + self.app->resume(); + }); +} + +- (void) applicationWillResignActive: (NSNotification*) notification { + dispatch_async(queue, ^{ + // self.app->pause(); + }); } - (void) menuWillOpen: (NSMenu*) menu { auto app = self.app; - auto w = app->windowManager->getWindow(0)->window; + auto window = app->windowManager.getWindow(0); + if (!window) return; + auto w = window->window; if (!w) return; [w makeKeyAndOrderFront: nil]; [NSApp activateIgnoringOtherApps:YES]; - if (app != nullptr && app->windowManager != nullptr) { - for (auto window : self.app->windowManager->windows) { - if (window != nullptr) window->bridge->router.emit("tray", "true"); + if (app != nullptr) { + for (auto window : self.app->windowManager.windows) { + if (window != nullptr) window->bridge.emit("tray", JSON::Object {}); } } } - (void) application: (NSApplication*) application openURLs: (NSArray<NSURL*>*) urls { auto app = self.app; - if (app != nullptr && app->windowManager != nullptr) { + if (app != nullptr) { for (NSURL* url in urls) { - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ - "url", [url.absoluteString UTF8String] - }}; - - for (auto window : self.app->windowManager->windows) { + for (auto& window : self.app->windowManager.windows) { if (window != nullptr) { - window->bridge->router.emit("applicationurl", json.str()); + window->handleApplicationURL(url.absoluteString.UTF8String); } } } @@ -73,8 +75,8 @@ continueUserActivity: (NSUserActivity*) userActivity - (BOOL) application: (NSApplication*) application willContinueUserActivityWithType: (NSString*) userActivityType { - static auto userConfig = SSC::getUserConfig(); - auto webpageURL = application.userActivity.webpageURL; + static auto userConfig = getUserConfig(); + const auto webpageURL = application.userActivity.webpageURL; if (userActivityType == nullptr) { return NO; @@ -88,14 +90,14 @@ continueUserActivity: (NSUserActivity*) userActivity return NO; } - auto activityType = SSC::String(userActivityType.UTF8String); + const auto activityType = String(userActivityType.UTF8String); - if (activityType != SSC::String(NSUserActivityTypeBrowsingWeb.UTF8String)) { + if (activityType != String(NSUserActivityTypeBrowsingWeb.UTF8String)) { return NO; } - auto host = SSC::String(webpageURL.host.UTF8String); - auto links = SSC::parseStringList(userConfig["meta_application_links"], ' '); + const auto host = String(webpageURL.host.UTF8String); + const auto links = parseStringList(userConfig["meta_application_links"], ' '); if (links.size() == 0) { return NO; @@ -104,7 +106,7 @@ continueUserActivity: (NSUserActivity*) userActivity bool exists = false; for (const auto& link : links) { - const auto parts = SSC::split(link, '?'); + const auto parts = split(link, '?'); if (host == parts[0]) { exists = true; break; @@ -115,15 +117,12 @@ continueUserActivity: (NSUserActivity*) userActivity return NO; } - auto url = SSC::String(webpageURL.absoluteString.UTF8String); - - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ "url", url }}; - + const auto url = String(webpageURL.absoluteString.UTF8String); bool emitted = false; - for (auto window : self.app->windowManager->windows) { + for (auto& window : self.app->windowManager.windows) { if (window != nullptr) { - window->bridge->router.emit("applicationurl", json.str()); + window->handleApplicationURL(url); emitted = true; } } @@ -140,38 +139,648 @@ didFailToContinueUserActivityWithType: (NSString*) userActivityType error: (NSError*) error { debug("application:didFailToContinueUserActivityWithType:error: %@", error); } -@end + +#elif SOCKET_RUNTIME_PLATFORM_IOS +- (BOOL) application: (UIApplication*) application + didFinishLaunchingWithOptions: (NSDictionary*) launchOptions +{ + auto const notificationCenter = NSNotificationCenter.defaultCenter; + const auto frame = UIScreen.mainScreen.bounds; + + Vector<String> argv; + bool isTest = false; + + self.app = App::sharedApplication(); + self.app->applicationDelegate = self; + + [notificationCenter + addObserver: self + selector: @selector(keyboardDidShow:) + name: UIKeyboardDidShowNotification + object: nil + ]; + + [notificationCenter + addObserver: self + selector: @selector(keyboardDidHide:) + name: UIKeyboardDidHideNotification + object: nil + ]; + + [notificationCenter + addObserver: self + selector: @selector(keyboardWillShow:) + name: UIKeyboardWillShowNotification + object: nil + ]; + + [notificationCenter + addObserver: self + selector: @selector(keyboardWillHide:) + name: UIKeyboardWillHideNotification + object: nil + ]; + + [notificationCenter + addObserver: self + selector: @selector(keyboardWillChange:) + name: UIKeyboardWillChangeFrameNotification + object: nil + ]; + + for (const auto& arg : split(self.app->userConfig["ssc_argv"], ',')) { + if (arg.find("--test") == 0) { + isTest = true; + } + + argv.push_back("'" + trim(arg) + "'"); + } + + auto windowManagerOptions = WindowManagerOptions {}; + + for (const auto& arg : split(self.app->userConfig["ssc_argv"], ',')) { + if (arg.find("--test") == 0) { + windowManagerOptions.features.useTestScript = true; + } + + windowManagerOptions.argv.push_back("'" + trim(arg) + "'"); + } + + + windowManagerOptions.userConfig = self.app->userConfig; + + self.app->windowManager.configure(windowManagerOptions); + + static const auto port = getDevPort(); + static const auto host = getDevHost(); + + if ( + self.app->userConfig["webview_service_worker_mode"] != "hybrid" && + self.app->userConfig["permissions_allow_service_worker"] != "false" + ) { + auto serviceWorkerWindowOptions = Window::Options {}; + auto serviceWorkerUserConfig = self.app->userConfig; + + serviceWorkerUserConfig["webview_watch_reload"] = "false"; + serviceWorkerWindowOptions.shouldExitApplicationOnClose = false; + serviceWorkerWindowOptions.index = SOCKET_RUNTIME_SERVICE_WORKER_CONTAINER_WINDOW_INDEX; + serviceWorkerWindowOptions.headless = Env::get("SOCKET_RUNTIME_SERVICE_WORKER_DEBUG").size() == 0; + serviceWorkerWindowOptions.userConfig = serviceWorkerUserConfig; + serviceWorkerWindowOptions.features.useGlobalCommonJS = false; + serviceWorkerWindowOptions.features.useGlobalNodeJS = false; + + auto serviceWorkerWindow = self.app->windowManager.createWindow(serviceWorkerWindowOptions); + self.app->serviceWorkerContainer.init(&serviceWorkerWindow->bridge); + + serviceWorkerWindow->navigate( + "socket://" + self.app->userConfig["meta_bundle_identifier"] + "/socket/service-worker/index.html" + ); + } + + auto defaultWindow = self.app->windowManager.createDefaultWindow(Window::Options { + .shouldExitApplicationOnClose = true + }); + + if (self.app->userConfig["webview_service_worker_mode"] == "hybrid") { + self.app->serviceWorkerContainer.init(&defaultWindow->bridge); + } + + defaultWindow->setTitle(self.app->userConfig["meta_title"]); + + if (isDebugEnabled() && port > 0 && host.size() > 0) { + defaultWindow->navigate(host + ":" + std::to_string(port)); + } else if (self.app->userConfig["webview_root"].size() != 0) { + defaultWindow->navigate( + "socket://" + self.app->userConfig["meta_bundle_identifier"] + self.app->userConfig["webview_root"] + ); + } else { + defaultWindow->navigate( + "socket://" + self.app->userConfig["meta_bundle_identifier"] + "/index.html" + ); + } + + defaultWindow->show(); + + return YES; +} + +- (void) applicationDidEnterBackground: (UIApplication*) application { + for (const auto& window : self.app->windowManager.windows) { + if (window != nullptr) { + window->eval("window.blur()"); + } + } +} + +- (void) applicationWillEnterForeground: (UIApplication*) application { + for (const auto& window : self.app->windowManager.windows) { + if (window != nullptr) { + if (!window->webview.isHidden) { + window->eval("window.focus()"); + } + } + } + + for (const auto& window : self.app->windowManager.windows) { + if (window != nullptr) { + // XXX(@jwerle): maybe bluetooth should be a singleton instance with observers + window->bridge.bluetooth.startScanning(); + } + } +} + +- (void) applicationWillTerminate: (UIApplication*) application { + dispatch_async(queue, ^{ + // TODO(@jwerle): what should we do here? + self.app->stop(); + }); +} + +- (void) applicationDidBecomeActive: (UIApplication*) application { + dispatch_async(queue, ^{ + self.app->resume(); + }); +} + +- (void) applicationWillResignActive: (UIApplication*) application { + dispatch_async(queue, ^{ + self.app->pause(); + }); +} + +- (BOOL) application: (UIApplication*) application + continueUserActivity: (NSUserActivity*) userActivity + restorationHandler: (void (^)(NSArray<id<UIUserActivityRestoring>>*)) restorationHandler +{ + return [self + application: application + willContinueUserActivityWithType: userActivity.activityType + ]; +} + + - (BOOL) application: (UIApplication*) application + willContinueUserActivityWithType: (NSString*) userActivityType +{ + static auto userConfig = getUserConfig(); + const auto webpageURL = application.userActivity.webpageURL; + + if (userActivityType == nullptr) { + return NO; + } + + if (webpageURL == nullptr) { + return NO; + } + + if (webpageURL.host == nullptr) { + return NO; + } + + const auto activityType = String(userActivityType.UTF8String); + + if (activityType != String(NSUserActivityTypeBrowsingWeb.UTF8String)) { + return NO; + } + + const auto host = String(webpageURL.host.UTF8String); + const auto links = parseStringList(userConfig["meta_application_links"], ' '); + + if (links.size() == 0) { + return NO; + } + + bool exists = false; + + for (const auto& link : links) { + const auto parts = split(link, '?'); + if (host == parts[0]) { + exists = true; + break; + } + } + + if (!exists) { + return NO; + } + + bool emitted = false; + for (const auto& window : self.app->windowManager.windows) { + if (window != nullptr) { + window->handleApplicationURL(webpageURL.absoluteString.UTF8String); + emitted = true; + } + } + + return emitted; +} + +- (void) keyboardWillHide: (NSNotification*) notification { + const auto info = notification.userInfo; + const auto keyboardFrameBegin = (NSValue*) [info valueForKey: UIKeyboardFrameEndUserInfoKey]; + const auto rect = [keyboardFrameBegin CGRectValue]; + const auto height = rect.size.height; + + const auto window = self.app->windowManager.getWindow(0); + window->webview.scrollView.scrollEnabled = YES; + + for (const auto window : self.app->windowManager.windows) { + if (window) { + window->bridge.emit("keyboard", JSON::Object::Entries { + {"value", JSON::Object::Entries { + {"event", "will-hide"}, + {"height", height} + }} + }); + } + } +} + +- (void) keyboardDidHide: (NSNotification*) notification { + for (const auto window : self.app->windowManager.windows) { + if (window) { + window->bridge.emit("keyboard", JSON::Object::Entries { + {"value", JSON::Object::Entries { + {"event", "did-hide"} + }} + }); + } + } +} + +- (void)keyboardWillShow: (NSNotification*) notification { + const auto info = notification.userInfo; + const auto keyboardFrameBegin = (NSValue*) [info valueForKey: UIKeyboardFrameEndUserInfoKey]; + const auto rect = [keyboardFrameBegin CGRectValue]; + const auto height = rect.size.height; + + const auto window = self.app->windowManager.getWindow(0); + window->webview.scrollView.scrollEnabled = NO; + + for (const auto window : self.app->windowManager.windows) { + if (window && !window->window.isHidden) { + window->bridge.emit("keyboard", JSON::Object::Entries { + {"value", JSON::Object::Entries { + {"event", "will-show"}, + {"height", height} + }} + }); + } + } +} + +- (void) keyboardDidShow: (NSNotification*) notification { + for (const auto window : self.app->windowManager.windows) { + if (window && !window->window.isHidden) { + window->bridge.emit("keyboard", JSON::Object::Entries { + {"value", JSON::Object::Entries { + {"event", "did-show"} + }} + }); + } + } +} + +- (void) keyboardWillChange: (NSNotification*) notification { + const auto keyboardInfo = notification.userInfo; + const auto keyboardFrameBegin = (NSValue* )[keyboardInfo valueForKey: UIKeyboardFrameEndUserInfoKey]; + const auto rect = [keyboardFrameBegin CGRectValue]; + const auto width = rect.size.width; + const auto height = rect.size.height; + + for (const auto window : self.app->windowManager.windows) { + if (window && !window->window.isHidden) { + window->bridge.emit("keyboard", JSON::Object::Entries { + {"value", JSON::Object::Entries { + {"event", "will-change"}, + {"width", width}, + {"height", height}, + }} + }); + } + } +} + +- (BOOL) application: (UIApplication*) app + openURL: (NSURL*) url + options: (NSDictionary<UIApplicationOpenURLOptionsKey, id>*) options +{ + for (const auto window : self.app->windowManager.windows) { + if (window) { + window->handleApplicationURL(url.absoluteString.UTF8String); + return YES; + } + } + + return NO; +} #endif +@end #endif namespace SSC { -#if defined(_WIN32) - FILE* console; -#endif static App* applicationInstance = nullptr; - App* App::instance () { - return SSC::applicationInstance; +#if SOCKET_RUNTIME_PLATFORM_WINDOWS + static Atomic<bool> isConsoleVisible = false; + static FILE* console = nullptr; + + static inline void alert (const WString &ws) { + MessageBoxA(nullptr, convertWStringToString(ws).c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); } - App::App () { + static inline void alert (const String &s) { + MessageBoxA(nullptr, s.c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); + } + + static inline void alert (const char* s) { + MessageBoxA(nullptr, s, _TEXT("Alert"), MB_OK | MB_ICONSTOP); + } + + static void showWindowsConsole () { + if (!isConsoleVisible) { + isConsoleVisible = true; + AllocConsole(); + freopen_s(&console, "CONOUT$", "w", stdout); + } + } + + static void hideWindowsConsole () { + if (isConsoleVisible) { + isConsoleVisible = false; + fclose(console); + FreeConsole(); + } + } + + // message is defined in WinUser.h + // https://raw.githubusercontent.com/tpn/winsdk-10/master/Include/10.0.10240.0/um/WinUser.h + static LRESULT CALLBACK onWindowProcMessage ( + HWND hWnd, + UINT message, + WPARAM wParam, + LPARAM lParam + ) { + auto app = SSC::App::sharedApplication(); + + if (app == nullptr) { + return 0; + } + + auto window = reinterpret_cast<WindowManager::ManagedWindow*>( + GetWindowLongPtr(hWnd, GWLP_USERDATA) + ); + + // invalidate `window` pointer that potentially is leaked + if (window != nullptr && app->windowManager.getWindow(window->index).get() != window) { + window = nullptr; + } + + auto userConfig = window != nullptr + ? reinterpret_cast<Window*>(window)->bridge.userConfig + : getUserConfig(); + + if (message == WM_COPYDATA) { + auto copyData = reinterpret_cast<PCOPYDATASTRUCT>(lParam); + message = (UINT) copyData->dwData; + wParam = (WPARAM) copyData->cbData; + lParam = (LPARAM) copyData->lpData; + } + + switch (message) { + case WM_SIZE: { + if (window == nullptr || window->controller == nullptr) { + break; + } + + RECT bounds; + GetClientRect(hWnd, &bounds); + window->size.height = bounds.bottom - bounds.top; + window->size.width = bounds.right - bounds.left; + window->controller->put_Bounds(bounds); + break; + } + + case WM_SOCKET_TRAY: { + // XXX(@jwerle, @heapwolf): is this a correct for an `isAgent` predicate? + auto isAgent = userConfig.count("tray_icon") != 0; + + if (window != nullptr && lParam == WM_LBUTTONDOWN) { + SetForegroundWindow(hWnd); + if (isAgent) { + POINT point; + GetCursorPos(&point); + TrackPopupMenu( + window->menutray, + TPM_BOTTOMALIGN | TPM_LEFTALIGN, + point.x, + point.y, + 0, + hWnd, + NULL + ); + } + + PostMessage(hWnd, WM_NULL, 0, 0); + + // broadcast an event to all the windows that the tray icon was clicked + for (auto window : app->windowManager.windows) { + if (window != nullptr) { + window->bridge.emit("tray", JSON::Object {}); + } + } + } + + // XXX: falls through to `WM_COMMAND` below + } + + case WM_COMMAND: { + if (window == nullptr) { + break; + } + + if (window->menuMap.contains(wParam)) { + String meta(window->menuMap[wParam]); + auto parts = split(meta, '\t'); + + if (parts.size() > 1) { + auto title = parts[0]; + auto parent = parts[1]; + + if (title.find("About") == 0) { + reinterpret_cast<Window*>(window)->about(); + break; + } + + if (title.find("Quit") == 0) { + window->exit(0); + break; + } + + window->eval(getResolveMenuSelectionJavaScript("0", title, parent, "system")); + } + } else if (window->menuTrayMap.contains(wParam)) { + String meta(window->menuTrayMap[wParam]); + auto parts = split(meta, ':'); + + if (parts.size() > 0) { + auto title = trim(parts[0]); + auto tag = parts.size() > 1 ? trim(parts[1]) : ""; + window->eval(getResolveMenuSelectionJavaScript("0", title, tag, "tray")); + } + } + + break; + } + + case WM_SETTINGCHANGE: { + // TODO(heapwolf): Dark mode + break; + } + + case WM_CREATE: { + // TODO(heapwolf): Dark mode + SetWindowTheme(hWnd, L"Explorer", NULL); + SetMenu(hWnd, CreateMenu()); + break; + } + + case WM_CLOSE: { + if (!window || !window->options.closable) { + break; + } + + auto index = window->index; + const JSON::Object json = JSON::Object::Entries { + {"data", index} + }; + + for (auto window : app->windowManager.windows) { + if (window != nullptr && window->index != index) { + window->eval(getEmitToRenderProcessJavaScript("window-closed", json.str())); + } + } + + app->windowManager.destroyWindow(index); + break; + } + + case WM_HOTKEY: { + if (window != nullptr) { + window->hotkey.onHotKeyBindingCallback((HotKeyBinding::ID) wParam); + } + break; + } + + case WM_HANDLE_DEEP_LINK: { + const auto url = String(reinterpret_cast<const char*>(lParam), wParam); + + for (auto window : app->windowManager.windows) { + if (window != nullptr) { + window->handleApplicationURL(url); + } + } + break; + } + + default: + return DefWindowProc(hWnd, message, wParam, lParam); + } + + return 0; + } +#endif + + App* App::sharedApplication () { + return applicationInstance; + } + +#if SOCKET_RUNTIME_PLATFORM_ANDROID + App::App ( + JNIEnv* env, + jobject self, + SharedPointer<Core> core + ) + : userConfig(getUserConfig()), + core(core), + windowManager(core), + serviceWorkerContainer(core), + jvm(env), + androidLooper(env) + { if (applicationInstance == nullptr) { - SSC::applicationInstance = this; + applicationInstance = this; } - this->core = new Core(); - auto cwd = getcwd(); - uv_chdir(cwd.c_str()); + this->jni = env; + this->self = env->NewGlobalRef(self); + this->init(); } +#else + App::App ( + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + HINSTANCE instanceId, + #else + int instanceId, + #endif + SharedPointer<Core> core + ) : userConfig(getUserConfig()), + core(core), + windowManager(core), + serviceWorkerContainer(core) + { + if (applicationInstance == nullptr) { + applicationInstance = this; + } - App::App (int) : App() { - #if defined(__linux__) && !defined(__ANDROID__) - gtk_init_check(0, nullptr); - #elif defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR - this->delegate.app = this; - NSApplication.sharedApplication.delegate = this->delegate; + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + this->hInstance = instanceId; + + // this fixes bad default quality DPI. + SetProcessDPIAware(); + + if (userConfig["win_logo"].size() == 0 && userConfig["win_icon"].size() > 0) { + userConfig["win_logo"] = fs::path(userConfig["win_icon"]).filename().string(); + } + + auto iconPath = fs::path { getcwd() / fs::path { userConfig["win_logo"] } }; + + HICON icon = (HICON) LoadImageA( + NULL, + iconPath.string().c_str(), + IMAGE_ICON, + GetSystemMetrics(SM_CXICON), + GetSystemMetrics(SM_CXICON), + LR_LOADFROMFILE + ); + + auto windowClassName = userConfig["meta_bundle_identifier"]; + + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = CreateSolidBrush(RGB(0, 0, 0)); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = windowClassName.c_str(); + wcex.hIconSm = icon; // ico doesn't auto scale, needs 16x16 icon lol fuck you bill + wcex.hIcon = icon; + wcex.lpfnWndProc = onWindowProcMessage; + + if (!RegisterClassEx(&wcex)) { + alert("Application could not launch, possible missing resources."); + } + #endif + + #if !SOCKET_RUNTIME_DESKTOP_EXTENSION + const auto cwd = getcwd(); + uv_chdir(cwd.c_str()); #endif + this->init(); } +#endif App::~App () { if (applicationInstance == this) { @@ -179,12 +788,39 @@ namespace SSC { } } - int App::run () { - #if defined(__linux__) && !defined(__ANDROID__) + void App::init () { + #if SOCKET_RUNTIME_PLATFORM_LINUX && !SOCKET_RUNTIME_PLATFORM_LINUX + gtk_init_check(0, nullptr); + #elif SOCKET_RUNTIME_PLATFORM_MACOS + this->applicationDelegate = [SSCApplicationDelegate new]; + this->applicationDelegate.app = this; + NSApplication.sharedApplication.delegate = this->applicationDelegate; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + OleInitialize(nullptr); + #endif + } + + int App::run (int argc, char** argv) { + #if SOCKET_RUNTIME_PLATFORM_LINUX && !SOCKET_RUNTIME_DESKTOP_EXTENSION gtk_main(); - #elif defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + // MUST be acquired on "main" thread + // `run()` should called when the main activity is created + if (!this->androidLooper.isAcquired()) { + this->androidLooper.acquire(); + } + #elif SOCKET_RUNTIME_PLATFORM_MACOS [NSApp run]; - #elif defined(_WIN32) + #elif SOCKET_RUNTIME_PLATFORM_IOS + @autoreleasepool { + return UIApplicationMain( + argc, + argv, + nullptr, + NSStringFromClass(SSCApplicationDelegate.class) + ); + } + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS MSG msg; if (!GetMessage(&msg, nullptr, 0, 0)) { @@ -199,7 +835,7 @@ namespace SSC { if (msg.message == WM_APP) { // from PostThreadMessage - auto callback = (std::function<void()> *)(msg.lParam); + auto callback = (Function<void()> *)(msg.lParam); (*callback)(); delete callback; } @@ -209,61 +845,77 @@ namespace SSC { } #endif + if (shouldExit) { + this->core->isShuttingDown = true; + } + return shouldExit ? 1 : 0; } - void App::kill () { - // Distinguish window closing with app exiting - shouldExit = true; - #if defined(__linux__) && !defined(__ANDROID__) + void App::resume () { + if (this->core != nullptr && this->paused) { + this->paused = false; + this->windowManager.emit("applicationresume"); + + this->dispatch([this]() { + this->core->resume(); + }); + } + } + + void App::pause () { + if (this->core != nullptr && !this->paused) { + this->paused = true; + this->windowManager.emit("applicationpause"); + + this->dispatch([this]() { + this->core->pause(); + }); + } + } + + void App::stop () { + if (this->stopped) { + return; + } + + this->stopped = true; + this->windowManager.emit("applicationstop"); + this->pause(); + + SSC::applicationInstance = nullptr; + + this->shouldExit = true; + + this->core->shutdown(); + #if SOCKET_RUNTIME_PLATFORM_LINUX && !SOCKET_RUNTIME_DESKTOP_EXTENSION gtk_main_quit(); - #elif defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + #elif SOCKET_RUNTIME_PLATFORM_MACOS // if not launched from the cli, just use `terminate()` // exit code status will not be captured - if (!fromSSC) { - [NSApp terminate:nil]; - } - #elif defined(_WIN32) - if (isDebugEnabled()) { - if (w32ShowConsole) { - HideConsole(); - } + if (!wasLaunchedFromCli) { + [NSApp terminate: nil]; } + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS PostQuitMessage(0); #endif } - void App::restart () { - #if defined(__linux__) && !defined(__ANDROID__) - // @TODO - #elif defined(__APPLE__) - // @TODO - #elif defined(_WIN32) - char filename[MAX_PATH] = ""; - PROCESS_INFORMATION pi; - STARTUPINFO si = { sizeof(STARTUPINFO) }; - GetModuleFileName(NULL, filename, MAX_PATH); - CreateProcess(NULL, filename, NULL, NULL, NULL, NULL, NULL, NULL, &si, &pi); - std::exit(0); - #endif - } - - void App::dispatch (std::function<void()> callback) { - #if defined(__linux__) && !defined(__ANDROID__) - auto threadCallback = new std::function<void()>(callback); - - g_idle_add_full( - G_PRIORITY_HIGH_IDLE, - (GSourceFunc)([](void* callback) -> int { - (*static_cast<std::function<void()>*>(callback))(); + void App::dispatch (Function<void()> callback) { + #if SOCKET_RUNTIME_PLATFORM_LINUX + g_main_context_invoke( + nullptr, + +[](gpointer userData) -> gboolean { + auto callback = reinterpret_cast<Function<void()>*>(userData); + if (*callback != nullptr) { + (*callback)(); + delete callback; + } return G_SOURCE_REMOVE; - }), - threadCallback, - [](void* callback) { - delete static_cast<std::function<void()>*>(callback); - } + }, + new Function<void()>(callback) ); - #elif defined(__APPLE__) + #elif SOCKET_RUNTIME_PLATFORM_APPLE auto priority = DISPATCH_QUEUE_PRIORITY_DEFAULT; auto queue = dispatch_get_global_queue(priority, 0); @@ -272,135 +924,489 @@ namespace SSC { callback(); }); }); - #elif defined(_WIN32) + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS static auto mainThread = GetCurrentThreadId(); - auto threadCallback = (LPARAM) new std::function<void()>(callback); + auto threadCallback = (LPARAM) new Function<void()>(callback); + if (this->isReady) { PostThreadMessage(mainThread, WM_APP, 0, threadCallback); return; } - std::thread t([&, threadCallback] { + + Thread t([&, threadCallback] { // TODO(trevnorris): Need to also check a shouldExit so this doesn't run forever in case // the rest of the application needs to exit before isReady is set. while (!this->isReady) { - std::this_thread::sleep_for(std::chrono::milliseconds(16)); + msleep(16); } PostThreadMessage(mainThread, WM_APP, 0, threadCallback); }); + t.detach(); + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + auto attachment = Android::JNIEnvironmentAttachment(this->jvm); + this->androidLooper.dispatch([=, this] () { + const auto attachment = Android::JNIEnvironmentAttachment(this->jvm); + callback(); + }); #endif } String App::getcwd () { - static String cwd = ""; + return SSC::getcwd(); + } + + bool App::hasRuntimePermission (const String& permission) const { + static const auto userConfig = getUserConfig(); + const auto key = String("permissions_allow_") + replace(permission, "-", "_"); + + if (!userConfig.contains(key)) { + return true; + } - #if defined(__linux__) && !defined(__ANDROID__) - try { - auto canonical = fs::canonical("/proc/self/exe"); - cwd = fs::path(canonical).parent_path().string(); - } catch (...) {} - #elif defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR - NSString *bundlePath = [[NSBundle mainBundle] resourcePath]; - cwd = [bundlePath UTF8String]; - #elif defined(_WIN32) - wchar_t filename[MAX_PATH]; - GetModuleFileNameW(NULL, filename, MAX_PATH); - auto path = fs::path { filename }.remove_filename(); - cwd = path.string(); - #endif + return userConfig.at(key) != "false"; + } - return cwd; +#if SOCKET_RUNTIME_PLATFORM_WINDOWS + LRESULT App::forwardWindowProcMessage ( + HWND hWnd, + UINT message, + WPARAM wParam, + LPARAM lParam + ) { + return onWindowProcMessage(hWnd, message, wParam, lParam); } +#endif +} - void App::setWindowManager (WindowManager* windowManager) { - this->windowManager = windowManager; +#if SOCKET_RUNTIME_PLATFORM_ANDROID +extern "C" { + jlong ANDROID_EXTERNAL(app, App, alloc)(JNIEnv *env, jobject self) { + if (App::sharedApplication() == nullptr) { + auto app = new App(env, self); + } + return reinterpret_cast<jlong>(App::sharedApplication()); } - WindowManager* App::getWindowManager () const { - return this->windowManager; + jboolean ANDROID_EXTERNAL(app, App, setRootDirectory)( + JNIEnv *env, + jobject self, + jstring rootDirectoryString + ) { + const auto rootDirectory = Android::StringWrap(env, rootDirectoryString).str(); + setcwd(rootDirectory); + uv_chdir(rootDirectory.c_str()); + return true; } - void App::exit (int code) { - if (this->onExit != nullptr) { - this->onExit(code); + jboolean ANDROID_EXTERNAL(app, App, setExternalStorageDirectory)( + JNIEnv *env, + jobject self, + jstring externalStorageDirectoryString + ) { + const auto directory = Android::StringWrap(env, externalStorageDirectoryString).str(); + FileResource::setExternalAndroidStorageDirectory(Path(directory)); + return true; + } + + jboolean ANDROID_EXTERNAL(app, App, setExternalFilesDirectory)( + JNIEnv *env, + jobject self, + jstring externalFilesDirectoryString + ) { + const auto directory = Android::StringWrap(env, externalFilesDirectoryString).str(); + FileResource::setExternalAndroidFilesDirectory(Path(directory)); + return true; + } + + jboolean ANDROID_EXTERNAL(app, App, setExternalCacheDirectory)( + JNIEnv *env, + jobject self, + jstring externalCacheDirectoryString + ) { + const auto directory = Android::StringWrap(env, externalCacheDirectoryString).str(); + FileResource::setExternalAndroidCacheDirectory(Path(directory)); + return true; + } + + jboolean ANDROID_EXTERNAL(app, App, setAssetManager)( + JNIEnv *env, + jobject self, + jobject assetManager + ) { + auto app = App::sharedApplication(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return false; } + + if (!assetManager) { + ANDROID_THROW(env, "'assetManager' object is null"); + return false; + } + + // `Core::FS` and `FileResource` will use the asset manager + // when looking for file resources, the asset manager will + // take precedence + FileResource::setSharedAndroidAssetManager(AAssetManager_fromJava(env, assetManager)); + return true; } -} -#if defined(_WIN32) + jboolean ANDROID_EXTERNAL(app, App, setMimeTypeMap)( + JNIEnv *env, + jobject self, + jobject mimeTypeMap + ) { + auto app = App::sharedApplication(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return false; + } -namespace SSC { - static inline void alert (const SSC::WString &ws) { - MessageBoxA(nullptr, SSC::convertWStringToString(ws).c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); + if (!mimeTypeMap) { + ANDROID_THROW(env, "'mimeTypeMap' object is null"); + return false; + } + + Android::initializeMimeTypeMap(env->NewGlobalRef(mimeTypeMap), app->jvm); + return true; } - static inline void alert (const SSC::String &s) { - MessageBoxA(nullptr, s.c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); + void ANDROID_EXTERNAL(app, App, setBuildInformation)( + JNIEnv *env, + jobject self, + jstring brandString, + jstring deviceString, + jstring fingerprintString, + jstring hardwareString, + jstring modelString, + jstring manufacturerString, + jstring productString + ) { + const auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + app->androidBuildInformation.brand = Android::StringWrap(env, brandString).str(); + app->androidBuildInformation.device = Android::StringWrap(env, deviceString).str(); + app->androidBuildInformation.fingerprint = Android::StringWrap(env, fingerprintString).str(); + app->androidBuildInformation.hardware = Android::StringWrap(env, hardwareString).str(); + app->androidBuildInformation.model = Android::StringWrap(env, modelString).str(); + app->androidBuildInformation.manufacturer = Android::StringWrap(env, manufacturerString).str(); + app->androidBuildInformation.product = Android::StringWrap(env, productString).str(); + app->isAndroidEmulator = ( + ( + app->androidBuildInformation.brand.starts_with("generic") && + app->androidBuildInformation.device.starts_with("generic") + ) || + app->androidBuildInformation.fingerprint.starts_with("generic") || + app->androidBuildInformation.fingerprint.starts_with("unknown") || + app->androidBuildInformation.hardware.find("goldfish") != String::npos || + app->androidBuildInformation.hardware.find("ranchu") != String::npos || + app->androidBuildInformation.model.find("google_sdk") != String::npos || + app->androidBuildInformation.model.find("Emulator") != String::npos || + app->androidBuildInformation.model.find("Android SDK built for x86") != String::npos || + app->androidBuildInformation.manufacturer.find("Genymotion") != String::npos || + app->androidBuildInformation.product.find("sdk_google") != String::npos || + app->androidBuildInformation.product.find("google_sdk") != String::npos || + app->androidBuildInformation.product.find("sdk") != String::npos || + app->androidBuildInformation.product.find("sdk_x86") != String::npos || + app->androidBuildInformation.product.find("sdk_gphone64_arm64") != String::npos || + app->androidBuildInformation.product.find("vbox86p") != String::npos || + app->androidBuildInformation.product.find("emulator") != String::npos || + app->androidBuildInformation.product.find("simulator") != String::npos + ); } - static inline void alert (const char* s) { - MessageBoxA(nullptr, s, _TEXT("Alert"), MB_OK | MB_ICONSTOP); + void ANDROID_EXTERNAL(app, App, setWellKnownDirectories)( + JNIEnv *env, + jobject self, + jstring downloadsString, + jstring documentsString, + jstring picturesString, + jstring desktopString, + jstring videosString, + jstring configString, + jstring mediaString, + jstring musicString, + jstring homeString, + jstring dataString, + jstring logString, + jstring tmpString + ) { + const auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + FileResource::WellKnownPaths defaults; + defaults.downloads = Path(Android::StringWrap(env, downloadsString).str()); + defaults.documents = Path(Android::StringWrap(env, documentsString).str()); + defaults.pictures = Path(Android::StringWrap(env, picturesString).str()); + defaults.desktop = Path(Android::StringWrap(env, desktopString).str()); + defaults.videos = Path(Android::StringWrap(env, videosString).str()); + defaults.config = Path(Android::StringWrap(env, configString).str()); + defaults.media = Path(Android::StringWrap(env, mediaString).str()); + defaults.music = Path(Android::StringWrap(env, musicString).str()); + defaults.home = Path(Android::StringWrap(env, homeString).str()); + defaults.data = Path(Android::StringWrap(env, dataString).str()); + defaults.log = Path(Android::StringWrap(env, logString).str()); + defaults.tmp = Path(Android::StringWrap(env, tmpString).str()); + FileResource::WellKnownPaths::setDefaults(defaults); } - void App::ShowConsole() { - if (consoleVisible) - return; - consoleVisible = true; - AllocConsole(); - freopen_s(&console, "CONOUT$", "w", stdout); + jstring ANDROID_EXTERNAL(app, App, getUserConfigValue)( + JNIEnv *env, + jobject self, + jstring keyString + ) { + const auto app = App::sharedApplication(); + const auto key = Android::StringWrap(env, keyString).str(); + const auto value = env->NewStringUTF( + app->userConfig.contains(key) + ? app->userConfig.at(key).c_str() + : "" + ); + + return value; } - void App::HideConsole() { - if (!consoleVisible) - return; - consoleVisible = false; - fclose(console); - FreeConsole(); + jboolean ANDROID_EXTERNAL(app, App, hasRuntimePermission)( + JNIEnv *env, + jobject self, + jstring permissionString + ) { + const auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + const auto permission = Android::StringWrap(env, permissionString).str(); + return app->hasRuntimePermission(permission); } - App::App (void* h) : App() { - static auto userConfig = SSC::getUserConfig(); - this->hInstance = (HINSTANCE) h; + jboolean ANDROID_EXTERNAL(app, App, isDebugEnabled)( + JNIEnv *env, + jobject self + ) { + const auto app = App::sharedApplication(); - // this fixes bad default quality DPI. - SetProcessDPIAware(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } - if (userConfig["win_logo"].size() == 0 && userConfig["win_icon"].size() > 0) { - userConfig["win_logo"] = fs::path(userConfig["win_icon"]).filename().string(); + return isDebugEnabled(); + } + + void ANDROID_EXTERNAL(app, App, onCreateAppActivity)( + JNIEnv *env, + jobject self, + jobject activity + ) { + auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); } - auto iconPath = fs::path { getcwd() / fs::path { userConfig["win_logo"] } }; + if (app->activity) { + app->jni->DeleteGlobalRef(app->activity); + } - HICON icon = (HICON) LoadImageA( - NULL, - iconPath.string().c_str(), - IMAGE_ICON, - GetSystemMetrics(SM_CXICON), - GetSystemMetrics(SM_CXICON), - LR_LOADFROMFILE + app->activity = env->NewGlobalRef(activity); + app->core->platform.configureAndroidContext( + Android::JVMEnvironment(app->jni), + app->activity ); - auto windowClassName = userConfig["meta_bundle_identifier"]; + app->run(); - wcex.cbSize = sizeof(WNDCLASSEX); - wcex.style = CS_HREDRAW | CS_VREDRAW; - wcex.cbClsExtra = 0; - wcex.cbWndExtra = 0; - wcex.hInstance = hInstance; - wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - wcex.hbrBackground = CreateSolidBrush(RGB(0, 0, 0)); - wcex.lpszMenuName = NULL; - wcex.lpszClassName = windowClassName.c_str(); - wcex.hIconSm = icon; // ico doesn't auto scale, needs 16x16 icon lol fuck you bill - wcex.hIcon = icon; - wcex.lpfnWndProc = Window::WndProc; + if (app->windowManager.getWindowStatus(0) == WindowManager::WINDOW_NONE) { + auto windowManagerOptions = WindowManagerOptions {}; - if (!RegisterClassEx(&wcex)) { - alert("Application could not launch, possible missing resources."); + for (const auto& arg : split(app->userConfig["ssc_argv"], ',')) { + if (arg.find("--test") == 0) { + windowManagerOptions.features.useTestScript = true; + } + + windowManagerOptions.argv.push_back("'" + trim(arg) + "'"); + } + + windowManagerOptions.userConfig = app->userConfig; + + app->windowManager.configure(windowManagerOptions); + + app->dispatch([=]() { + auto defaultWindow = app->windowManager.createDefaultWindow(Window::Options { + .shouldExitApplicationOnClose = true + }); + + if ( + app->userConfig["webview_service_worker_mode"] != "hybrid" && + app->userConfig["permissions_allow_service_worker"] != "false" + ) { + if (app->windowManager.getWindowStatus(SOCKET_RUNTIME_SERVICE_WORKER_CONTAINER_WINDOW_INDEX) == WindowManager::WINDOW_NONE) { + auto serviceWorkerWindowOptions = Window::Options {}; + auto serviceWorkerUserConfig = app->userConfig; + + serviceWorkerUserConfig["webview_watch_reload"] = "false"; + serviceWorkerWindowOptions.shouldExitApplicationOnClose = false; + serviceWorkerWindowOptions.index = SOCKET_RUNTIME_SERVICE_WORKER_CONTAINER_WINDOW_INDEX; + serviceWorkerWindowOptions.headless = Env::get("SOCKET_RUNTIME_SERVICE_WORKER_DEBUG").size() == 0; + serviceWorkerWindowOptions.userConfig = serviceWorkerUserConfig; + serviceWorkerWindowOptions.features.useGlobalCommonJS = false; + serviceWorkerWindowOptions.features.useGlobalNodeJS = false; + + auto serviceWorkerWindow = app->windowManager.createWindow(serviceWorkerWindowOptions); + app->serviceWorkerContainer.init(&serviceWorkerWindow->bridge); + serviceWorkerWindow->navigate( + "https://" + app->userConfig["meta_bundle_identifier"] + "/socket/service-worker/index.html" + ); + + app->core->setTimeout(256, [=](){ + serviceWorkerWindow->hide(); + }); + } + } + + if ( + app->userConfig["permissions_allow_service_worker"] != "false" && + app->userConfig["webview_service_worker_mode"] == "hybrid" + ) { + app->serviceWorkerContainer.init(&defaultWindow->bridge); + } + + defaultWindow->setTitle(app->userConfig["meta_title"]); + + static const auto port = getDevPort(); + static const auto host = getDevHost(); + + if (isDebugEnabled() && port > 0 && host.size() > 0) { + defaultWindow->navigate(host + ":" + std::to_string(port)); + } else if (app->userConfig["webview_root"].size() != 0) { + defaultWindow->navigate( + "https://" + app->userConfig["meta_bundle_identifier"] + app->userConfig["webview_root"] + ); + } else { + defaultWindow->navigate( + "https://" + app->userConfig["meta_bundle_identifier"] + "/index.html" + ); + } + }); + } + } + + void ANDROID_EXTERNAL(app, App, onDestroyAppActivity)( + JNIEnv *env, + jobject self, + jobject activity + ) { + auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + if (app->activity) { + env->DeleteGlobalRef(app->activity); + app->activity = nullptr; + app->core->platform.activity = nullptr; } - }; + } + + void ANDROID_EXTERNAL(app, App, onDestroy)(JNIEnv *env, jobject self) { + auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + if (app->self) { + env->DeleteGlobalRef(app->self); + } + + app->jni = nullptr; + app->self = nullptr; + + app->pause(); + } + + void ANDROID_EXTERNAL(app, App, onStart)(JNIEnv *env, jobject self) { + auto app = App::sharedApplication(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + app->resume(); + } + + void ANDROID_EXTERNAL(app, App, onStop)(JNIEnv *env, jobject self) { + auto app = App::sharedApplication(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + app->pause(); + } + + void ANDROID_EXTERNAL(app, App, onResume)(JNIEnv *env, jobject self) { + auto app = App::sharedApplication(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + app->resume(); + } + + void ANDROID_EXTERNAL(app, App, onPause)(JNIEnv *env, jobject self) { + auto app = App::sharedApplication(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + app->pause(); + } + + void ANDROID_EXTERNAL(app, App, onPermissionChange)( + JNIEnv *env, + jobject self, + jstring nameString, + jstring stateString + ) { + auto app = App::sharedApplication(); + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + } + + const auto name = Android::StringWrap(env, nameString).str(); + const auto state = Android::StringWrap(env, stateString).str(); + + if (name == "geolocation") { + app->core->geolocation.permissionChangeObservers.dispatch(JSON::Object::Entries { + {"name", name}, + {"state", state} + }); + } + + if (name == "notification") { + app->core->notifications.permissionChangeObservers.dispatch(JSON::Object::Entries { + {"name", name}, + {"state", state} + }); + } + + if (name == "camera" || name == "microphone") { + app->core->mediaDevices.permissionChangeObservers.dispatch(JSON::Object::Entries { + {"name", name}, + {"state", state} + }); + } + } } -#endif // _WIN32 +#endif diff --git a/src/app/app.hh b/src/app/app.hh index 1dfc331a68..52df6a8ce8 100644 --- a/src/app/app.hh +++ b/src/app/app.hh @@ -1,79 +1,147 @@ -#ifndef SSC_APP_APP_H -#define SSC_APP_APP_H +#ifndef SOCKET_RUNTIME_APP_APP_H +#define SOCKET_RUNTIME_APP_APP_H #include "../core/core.hh" -#include "../ipc/ipc.hh" +#include "../serviceworker/container.hh" +#include "../window/window.hh" + +#if SOCKET_RUNTIME_PLATFORM_IOS +#import <QuartzCore/QuartzCore.h> +#import <objc/runtime.h> +#endif namespace SSC { class App; } -#if defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR -@interface SSCApplicationDelegate : NSObject<NSApplicationDelegate> -@property (nonatomic) SSC::App* app; -@property (strong, nonatomic) NSStatusItem *statusItem; -- (void) applicationDidFinishLaunching: (NSNotification*) notification; -- (void) application: (NSApplication*) application openURLs: (NSArray<NSURL*>*) urls; +#if SOCKET_RUNTIME_PLATFORM_APPLE +@interface SSCApplicationDelegate : +#if SOCKET_RUNTIME_PLATFORM_MACOS + NSObject<NSApplicationDelegate> + @property (strong, nonatomic) NSStatusItem *statusItem; + - (void) applicationDidFinishLaunching: (NSNotification*) notification; + - (void) application: (NSApplication*) application openURLs: (NSArray<NSURL*>*) urls; -- (BOOL) application: (NSApplication*) application -continueUserActivity: (NSUserActivity*) userActivity - restorationHandler: (void (^)(NSArray*)) restorationHandler; + - (BOOL) application: (NSApplication*) application + continueUserActivity: (NSUserActivity*) userActivity + restorationHandler: (void (^)(NSArray*)) restorationHandler; - (BOOL) application: (NSApplication*) application willContinueUserActivityWithType: (NSString*) userActivityType; - - (void) application: (NSApplication*) application -didFailToContinueUserActivityWithType: (NSString*) userActivityType - error: (NSError*) error; + - (void) application: (NSApplication*) application + didFailToContinueUserActivityWithType: (NSString*) userActivityType + error: (NSError*) error; + +#elif SOCKET_RUNTIME_PLATFORM_IOS + UIResponder <UIApplicationDelegate> + @property (nonatomic, strong) CADisplayLink *displayLink; + @property (nonatomic, assign) CGFloat keyboardHeight; + @property (nonatomic, assign) BOOL inMotion; + @property (nonatomic, assign) CGFloat progress; + @property (nonatomic, assign) NSTimeInterval animationDuration; + + - (BOOL) application: (UIApplication*) application + continueUserActivity: (NSUserActivity*) userActivity + restorationHandler: (void (^)(NSArray<id<UIUserActivityRestoring>>*)) restorationHandler; + + - (BOOL) application: (UIApplication*) application + willContinueUserActivityWithType: (NSString*) userActivityType; +#endif + +@property (nonatomic, assign) SSC::App* app; @end #endif namespace SSC { - class WindowManager; - class App { - // an opaque pointer to the configured `WindowManager<Window, App>` public: - static inline std::atomic<bool> isReady = false; - static App* instance (); + static inline Atomic<bool> isReady = false; + static App* sharedApplication (); + static constexpr int DEFAULT_INSTANCE_ID = 0; - #if defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR - NSAutoreleasePool* pool = [NSAutoreleasePool new]; - SSCApplicationDelegate* delegate = [SSCApplicationDelegate new]; - #elif defined(_WIN32) - MSG msg; - WNDCLASSEX wcex; + #if SOCKET_RUNTIME_PLATFORM_APPLE + // created and set in `App::App()` on macOS or created by + // `UIApplicationMain` and set in the + // `application:didFinishLaunchingWithOptions:` delegate methdo on iOS + // TODO(@jwerle): remove this field + SSCApplicationDelegate* applicationDelegate = nullptr; + #if SOCKET_RUNTIME_PLATFORM_MACOS + NSAutoreleasePool* pool = [NSAutoreleasePool new]; + #endif + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS _In_ HINSTANCE hInstance; + WNDCLASSEX wcex; + MSG msg; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + Android::BuildInformation androidBuildInformation; + Android::Looper androidLooper; + Android::JVMEnvironment jvm; + Android::Activity activity = nullptr; + Android::Application self = nullptr; + JNIEnv* jni = nullptr; + bool isAndroidEmulator = false; #endif - WindowManager *windowManager = nullptr; ExitCallback onExit = nullptr; AtomicBool shouldExit = false; - bool fromSSC = false; - bool w32ShowConsole = false; - Map appData; - Core *core; - - #ifdef _WIN32 - App (void *); - void ShowConsole(); - void HideConsole(); - bool consoleVisible = false; + AtomicBool stopped = false; + AtomicBool paused = false; + bool wasLaunchedFromCli = false; + + WindowManager windowManager; + ServiceWorkerContainer serviceWorkerContainer; + SharedPointer<Core> core = nullptr; + Map userConfig; + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + /** + * `App` class constructor for Android. + * The `App` instance is constructed from the context of + * the shared `Application` singleton on Android. This is a + * special case constructor. + */ + App ( + JNIEnv* env, + jobject self, + SharedPointer<Core> core = SharedPointer<Core>(new Core()) + ); + #else + /** + * `App` class constructor for desktop (Linux, macOS, Windows) and + * iOS (iPhoneOS, iPhoneSimulator, iPad) where `instanceId` can be + * a `HINSTANCE` on Windows or an empty value (`0`) on other platforms. + */ + App ( + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + HINSTANCE instanceId = 0, + #else + int instanceId = 0, + #endif + SharedPointer<Core> core = SharedPointer<Core>(new Core()) + ); #endif - App (int); - App (); + App () = delete; + App (const App&) = delete; + App (App&&) = delete; ~App (); - int run (); - void kill (); - void exit (int code); - void restart (); - void dispatch (std::function<void()>); + App& operator = (App&) = delete; + App& operator = (App&&) = delete; + + int run (int argc = 0, char** argv = nullptr); + void init (); + void stop (); + void resume (); + void pause (); + void dispatch (Function<void()>); String getcwd (); - void setWindowManager (WindowManager*); - WindowManager* getWindowManager () const; - }; + bool hasRuntimePermission (const String& permission) const; + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + LRESULT forwardWindowProcMessage (HWND, UINT, WPARAM, LPARAM); + #endif + }; } #endif diff --git a/src/app/app.kt b/src/app/app.kt new file mode 100644 index 0000000000..9d83aac292 --- /dev/null +++ b/src/app/app.kt @@ -0,0 +1,796 @@ +// vim: set sw=2: +package socket.runtime.app + +import java.lang.Exception +import java.lang.ref.WeakReference + +import android.app.Activity +import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.AssetManager +import android.content.res.AssetFileDescriptor +import android.content.ContentResolver +import android.database.Cursor +import android.graphics.Insets +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import android.view.WindowInsets +import android.view.WindowManager +import android.webkit.MimeTypeMap +import android.webkit.WebView + +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner + +import socket.runtime.core.console +import socket.runtime.window.WindowManagerActivity + +import __BUNDLE_IDENTIFIER__.R + +open class AppPermissionRequest (callback: (Boolean) -> Unit) { + val id: Int = (0..16384).random().toInt() + val callback = callback +} + +open class AppPlatform (val activity: AppActivity) { + fun isDocumentURI (url: String): Boolean { + return DocumentsContract.isDocumentUri( + this.activity.applicationContext, + Uri.parse(url) + ) + } + + fun getDocumentID (url: String): String { + return DocumentsContract.getDocumentId(Uri.parse(url)) + } + + fun getContentURI (baseURL: String, documentID: Long): String { + val uri = ContentUris.withAppendedId( + Uri.parse(baseURL), + documentID + ) + + return uri.toString() + } + + fun getContentMimeType (url: String): String { + return this.activity.applicationContext.contentResolver.getType(Uri.parse(url)) ?: "" + } + + fun getPathnameEntriesFromContentURI (url: String): Array<String> { + val context = this.activity.applicationContext + val column = MediaStore.MediaColumns.DATA + var cursor: Cursor? = null + val uri = Uri.parse(url) + val results = mutableListOf<String>() + + try { + cursor = context.contentResolver.query( + uri, + arrayOf(column), + null, + null, + null + ) + + if (cursor != null) { + cursor.moveToFirst() + + do { + val result = cursor.getString(cursor.getColumnIndex(column)) + + if (result != null && result.length > 0) { + results += result + } else { + val tmp = try { + cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns._ID)) + } catch (e: Exception) { + null + } + + if (tmp != null) { + results += cursor.getString(cursor.getColumnIndex(column)) + } + } + } while (cursor.moveToNext()) + } + } catch (_: Exception) { + // not handled + } finally { + if (cursor != null) { + cursor.close() + } + } + + return results.toTypedArray() + } + + fun getPathnameFromContentURIDataColumn ( + url: String, + id: String + ): String { + val context = this.activity.applicationContext + val column = MediaStore.MediaColumns.DATA + var cursor: Cursor? = null + var result: String? = null + val uri = Uri.parse(url) + + try { + cursor = context.contentResolver.query( + uri, + arrayOf(column), + null, + null, + null + ) + + if (cursor != null) { + cursor.moveToFirst() + do { + if (id.length > 0 && (result == null || result.length == 0)) { + result = cursor.getString(cursor.getColumnIndex(column)) + break + } else { + var index = cursor.getColumnIndex(MediaStore.MediaColumns._ID) + var tmp: String? = null + + try { + tmp = cursor.getString(index) + } catch (e: Exception) {} + + if (tmp == id) { + index = cursor.getColumnIndex(column) + result = cursor.getString(index) + break + } + } + } while (cursor.moveToNext()) + } + } catch (err: Exception) { + return "" + } finally { + if (cursor != null) { + cursor.close() + } + } + + return result ?: "" + } + + fun getExternalContentURIForType (type: String): String { + val contentURI = ( + if (type == "image") { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } else if (type == "video") { + MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } else if (type == "audio") { + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } else { + MediaStore.Files.getContentUri("external") + } + ) + + if (contentURI == null) { + return "" + } + + return contentURI.toString() + } + + fun getContentResolver (): ContentResolver { + return this.activity.getContentResolver() + } + + fun openContentResolverFileDescriptor (url: String): AssetFileDescriptor? { + val contentResolver = this.activity.applicationContext.contentResolver + console.log("url: $url") + try { + return contentResolver.openAssetFileDescriptor(Uri.parse(url), "r") + } catch (err: Exception) { + console.log("error: $err") + return null + } + } + + fun hasContentResolverAccess (url: String): Boolean { + val contentResolver = this.activity.applicationContext.contentResolver + + try { + val fd = contentResolver.openAssetFileDescriptor(Uri.parse(url), "r") + + if (fd == null) { + return false + } + + fd.close() + } catch (_: Exception) { + return false + } + + return true + } +} + +/** + * The `AppActivity` represents the root activity for the application. + * It is an extended `WindowManagerActivity` that considers application + * and platform details like notifications, incoming intents, platform + * permissions, and more. + */ +open class AppActivity : WindowManagerActivity() { + open protected val TAG = "AppActivity" + open lateinit var notificationChannel: NotificationChannel + open lateinit var notificationManager: NotificationManager + + open val platform = AppPlatform(this) + open val permissionRequests = mutableListOf<AppPermissionRequest>() + + open fun getRootDirectory (): String { + return this.getExternalFilesDir(null)?.absolutePath + ?: "/sdcard/Android/data/__BUNDLE_IDENTIFIER__/files" + } + + fun getAppPlatform (): AppPlatform { + return this.platform + } + + fun checkPermission (permission: String): Boolean { + val status = ContextCompat.checkSelfPermission(this.applicationContext, permission) + return status == PackageManager.PERMISSION_GRANTED + } + + fun requestPermissions (permissions: Array<String>) { + return this.requestPermissions(permissions, fun (_: Boolean) {}) + } + + fun requestPermissions (permissions: Array<String>, callback: (Boolean) -> Unit) { + val request = AppPermissionRequest(callback) + this.permissionRequests.add(request) + ActivityCompat.requestPermissions( + this, + permissions, + request.id + ) + } + + fun openExternal (value: String) { + val uri = Uri.parse(value) + val action = Intent.ACTION_VIEW + val intent = Intent(action, uri) + this.startActivity(intent) + } + + fun getScreenInsets (): Insets { + val windowManager = this.applicationContext.getSystemService( + Context.WINDOW_SERVICE + ) as WindowManager + val metrics = windowManager.getCurrentWindowMetrics() + val windowInsets = metrics.windowInsets + return windowInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars() or + WindowInsets.Type.displayCutout() + ) + } + + fun getScreenSizeWidth (): Int { + val insets = this.getScreenInsets() + return insets.right + insets.left + } + + fun getScreenSizeHeight (): Int { + val insets = this.getScreenInsets() + return insets.top + insets.bottom + } + + fun getAssetManager (): AssetManager { + return this.applicationContext.resources.assets + } + + fun isNotificationManagerEnabled (): Boolean { + return NotificationManagerCompat.from(this).areNotificationsEnabled() + } + + fun showNotification ( + id: String, + title: String, + body: String, + tag: String, + channel: String, + category: String, + silent: Boolean, + iconURL: String, + imageURL: String, + vibratePattern: String + ): Boolean { + // paramters + val identifier = id.toLongOrNull()?.toInt() ?: (0..16384).random().toInt() + val vibration = vibratePattern + .split(",") + .filter({ it.length > 0 }) + .map({ it.toInt().toLong() }) + .toTypedArray() + + // intents + val intentFlags = ( + Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + + val contentIntent = Intent(this, __BUNDLE_IDENTIFIER__.MainActivity::class.java) + val deleteIntent = Intent(this, __BUNDLE_IDENTIFIER__.MainActivity::class.java) + + // TODO(@jwerle): move 'action' to a constant + contentIntent.addCategory(Intent.CATEGORY_LAUNCHER) + contentIntent.setAction("notification.response.default") + contentIntent.putExtra("id", id) + contentIntent.flags = intentFlags + + // TODO(@jwerle): move 'action' to a constant + deleteIntent.setAction("notification.response.dismiss") + deleteIntent.putExtra("id", id) + deleteIntent.flags = intentFlags + + // pending intents + val pendingContentIntent: PendingIntent = PendingIntent.getActivity( + this, + identifier, + contentIntent, + ( + PendingIntent.FLAG_UPDATE_CURRENT or + PendingIntent.FLAG_IMMUTABLE or + PendingIntent.FLAG_ONE_SHOT + ) + ) + + val pendingDeleteIntent: PendingIntent = PendingIntent.getActivity( + this, + identifier, + deleteIntent, + ( + PendingIntent.FLAG_UPDATE_CURRENT or + PendingIntent.FLAG_IMMUTABLE + ) + ) + + // build notification + val builder = NotificationCompat.Builder(this, channel).apply { + setPriority(NotificationCompat.PRIORITY_DEFAULT) + setContentTitle(title) + setContentIntent(pendingContentIntent) + setDeleteIntent(pendingDeleteIntent) + setAutoCancel(true) + setSilent(silent) + if (vibration.size > 0) { + setVibrate(vibration.toLongArray()) + } + + if (body.length > 0) { + setContentText(body) + } + } + + if (iconURL.length > 0) { + val icon = IconCompat.createWithContentUri(iconURL) + builder.setSmallIcon(icon) + } else { + val icon = IconCompat.createWithResource( + this, + R.mipmap.ic_launcher_round + ) + + builder.setSmallIcon(icon) + } + + if (imageURL.length > 0) { + val icon = Icon.createWithContentUri(imageURL) + builder.setLargeIcon(icon) + } else { + val icon = IconCompat.createWithResource( + this, + R.mipmap.ic_launcher + ) + + builder.setSmallIcon(icon) + } + + if (category.length > 0) { + builder.setCategory( + category + .replace("msg", "message") + .replace("-", "_") + ) + } + + val notification = builder.build() + with (NotificationManagerCompat.from(this)) { + notify( + tag, + identifier, + notification + ) + } + + return true + } + + fun closeNotification (id: String, tag: String): Boolean { + val identifier = id.toLongOrNull()?.toInt() ?: (0..16384).random().toInt() + with (NotificationManagerCompat.from(this)) { + cancel(tag, identifier) + } + return true + } + + override fun onCreate (savedInstanceState: Bundle?) { + this.supportActionBar?.hide() + this.getWindow()?.statusBarColor = android.graphics.Color.TRANSPARENT + + super.onCreate(savedInstanceState) + + val externalStorageDirectory = Environment.getExternalStorageDirectory().absolutePath + val externalFilesDirectory = this.getExternalFilesDir(null)?.absolutePath ?: "$externalStorageDirectory/Desktop" + val cacheDirectory = this.applicationContext.getCacheDir().absolutePath + val rootDirectory = this.getRootDirectory() + val assetManager = this.applicationContext.resources.assets + val app = App.getInstance() + + this.notificationChannel = NotificationChannel( + "__BUNDLE_IDENTIFIER__", + "__BUNDLE_IDENTIFIER__ Notifications", + NotificationManager.IMPORTANCE_DEFAULT + ) + + this.notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + app.apply { + setMimeTypeMap(MimeTypeMap.getSingleton()) + setAssetManager(assetManager) + + // directories + setRootDirectory(rootDirectory) + setExternalCacheDirectory(cacheDirectory) + setExternalFilesDirectory(externalFilesDirectory) + setExternalStorageDirectory(externalStorageDirectory) + + // build info + setBuildInformation( + Build.BRAND, + Build.DEVICE, + Build.FINGERPRINT, + Build.HARDWARE, + Build.MODEL, + Build.MANUFACTURER, + Build.PRODUCT + ) + + setWellKnownDirectories( + // 'Downloads/' + this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath ?: "", + // 'Documents/' + this.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.absolutePath ?: "", + // 'Pictures/' + this.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath ?: "", + // 'Desktop/' + externalFilesDirectory, + // 'Videos/' + this.getExternalFilesDir(Environment.DIRECTORY_DCIM)?.absolutePath ?: "", + // "configuration directory" + externalFilesDirectory, + // "media directory" + externalStorageDirectory + "Android/media/__BUNDLE_IDENTIFIER__", + // 'Music/' + this.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath ?: "", + // '~/' + externalStorageDirectory, + // "data directory" + externalFilesDirectory, + // "logs directory" + externalFilesDirectory, + // 'tmp/' (likely the cache directory will be used instead of this value) + externalStorageDirectory + "/tmp/__BUNDLE_IDENTIFIER__" + ) + } + + app.onCreateAppActivity(this) + + if (app.hasRuntimePermission("notifications")) { + this.notificationManager.createNotificationChannel(this.notificationChannel) + } + + if (savedInstanceState == null) { + WebView.setWebContentsDebuggingEnabled(app.isDebugEnabled()) + + this.applicationContext + .getSharedPreferences("WebSettings", Activity.MODE_PRIVATE) + .edit() + .apply { + putString("scheme", "socket") + putString("hostname", "__BUNDLE_IDENTIFIER__") + apply() + } + } + } + + override fun onStart () { + super.onStart() + val action: String? = this.intent?.action + val data: android.net.Uri? = this.intent?.data + + if (action != null && data != null) { + this.onNewIntent(this.intent) + } + } + + override fun onResume () { + super.onResume() + } + + override fun onPause () { + super.onPause() + } + + override fun onStop () { + super.onStop() + } + + override fun onDestroy () { + super.onDestroy() + App.getInstance().onDestroyAppActivity(this) + } + + override fun onNewIntent (intent: Intent) { + super.onNewIntent(intent) + val action = intent.action + val data = intent.data + val id = intent.extras?.getCharSequence("id")?.toString() + + when (action) { + "android.intent.action.MAIN", + "android.intent.action.VIEW" -> { + val scheme = data?.scheme ?: return + val applicationProtocol = App.getInstance().getUserConfigValue("meta_application_protocol") + if ( + applicationProtocol.length > 0 && + scheme.startsWith(applicationProtocol) + ) { + for (fragment in this.windowFragmentManager.fragments) { + val window = fragment.window ?: continue + window.handleApplicationURL(data.toString()) + } + } + } + + "notification.response.default" -> { + for (fragment in this.windowFragmentManager.fragments) { + val bridge = fragment.window?.bridge ?: continue + bridge.emit("notificationresponse", """{ + "id": "$id", + "action": "default" + }""") + } + } + + "notification.response.dismiss" -> { + for (fragment in this.windowFragmentManager.fragments) { + val bridge = fragment.window?.bridge ?: continue + bridge.emit("notificationresponse", """{ + "id": "$id", + "action": "dismiss" + }""") + } + } + } + } + + override fun onRequestPermissionsResult ( + requestCode: Int, + permissions: Array<String>, + grantResults: IntArray + ) { + var i = 0 + val seen = mutableSetOf<String>() + for (permission in permissions) { + val granted = ( + grantResults[i++] == PackageManager.PERMISSION_GRANTED + ) + + var name = "" + when (permission) { + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.ACCESS_FINE_LOCATION" -> { + name = "geolocation" + } + + "android.permission.POST_NOTIFICATIONS" -> { + name = "notifications" + } + + "android.permission.CAMERA" -> { + name = "camera" + } + + "android.permission.RECORD_AUDIO" -> { + name = "microphone" + } + } + + if (seen.contains(name)) { + continue + } + + if (name.length == 0) { + continue + } + + seen.add(name) + + val state = if (granted) "granted" else "denied" + App.getInstance().onPermissionChange(name, state) + } + + for (request in this.permissionRequests) { + if (request.id == requestCode) { + this.permissionRequests.remove(request) + request.callback(grantResults.all { r -> + r == PackageManager.PERMISSION_GRANTED + }) + return + } + } + + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } +} + +open class AppLifecycleObserver : DefaultLifecycleObserver { + override fun onDestroy (owner: LifecycleOwner) { + } + + override fun onStart (owner: LifecycleOwner) { + App.getInstance().onStart() + } + + override fun onStop (owner: LifecycleOwner) { + App.getInstance().onStop() + } + + override fun onPause (owner: LifecycleOwner) { + App.getInstance().onPause() + } + + override fun onResume (owner: LifecycleOwner) { + App.getInstance().onResume() + } +} + +open class App : Application() { + val pointer = alloc() + + protected val lifecycleListener: AppLifecycleObserver by lazy { + AppLifecycleObserver() + } + + companion object { + lateinit var appInstance: App + + fun getInstance () : App { + return appInstance + } + + fun loadSocketRuntime () { + System.loadLibrary("socket-runtime") + } + } + + init { + App.Companion.appInstance = this + } + + override fun onCreate () { + super.onCreate() + + val lifecycleListener = this.lifecycleListener + + ProcessLifecycleOwner.get().lifecycle.apply { + addObserver(lifecycleListener) + } + } + + @Throws(Exception::class) + protected external fun alloc (): Long + + @Throws(Exception::class) + external fun setRootDirectory (rootDirectory: String): Boolean + + @Throws(Exception::class) + external fun setExternalCacheDirectory (cacheDirectory: String): Boolean + + @Throws(Exception::class) + external fun setExternalStorageDirectory (directory: String): Boolean + + @Throws(Exception::class) + external fun setExternalFilesDirectory (directory: String): Boolean + + @Throws(Exception::class) + external fun setAssetManager (assetManager: AssetManager): Boolean + + @Throws(Exception::class) + external fun setMimeTypeMap (mimeTypeMap: MimeTypeMap): Boolean + + @Throws(Exception::class) + external fun setBuildInformation ( + brand: String, + device: String, + fingerprint: String, + hardware: String, + model: String, + manufacturer: String, + product: String + ): Unit + + @Throws(Exception::class) + external fun setWellKnownDirectories ( + downloads: String = "", + documents: String = "", + pictures: String = "", + desktop: String = "", + videos: String = "", + config: String = "", + media: String = "", + music: String = "", + home: String = "", + data: String = "", + log: String = "", + tmp: String = "" + ): Unit + + @Throws(Exception::class) + external fun getUserConfigValue (key: String): String + + @Throws(Exception::class) + external fun hasRuntimePermission (permission: String): Boolean + + @Throws(Exception::class) + external fun isDebugEnabled (): Boolean + + @Throws(Exception::class) + external fun onCreateAppActivity (activity: AppActivity): Unit + + @Throws(Exception::class) + external fun onDestroyAppActivity (activity: AppActivity): Unit + + @Throws(Exception::class) + external fun onStart (): Unit + + @Throws(Exception::class) + external fun onStop (): Unit + + @Throws(Exception::class) + external fun onResume (): Unit + + @Throws(Exception::class) + external fun onPause (): Unit + + @Throws(Exception::class) + external fun onDestroy (): Unit + + @Throws(Exception::class) + external fun onPermissionChange (name: String, state: String): Unit +} diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 2e79efd3e1..c546ae45af 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -25,6 +25,7 @@ #pragma comment(lib, "userenv.lib") #pragma comment(lib, "uuid.lib") #pragma comment(lib, "libuv.lib") +#pragma comment(lib, "llama.lib") #pragma comment(lib, "winspool.lib") #pragma comment(lib, "ws2_32.lib") #endif @@ -37,23 +38,27 @@ #include <filesystem> #include <iostream> #include <fstream> +#include <future> #include <regex> #include <span> #include <unordered_set> -#include "../core/core.hh" +#ifndef CMD_RUNNER +#define CMD_RUNNER +#endif + +#ifndef SSC_CLI +#define SSC_CLI 1 +#endif + #include "../extension/extension.hh" -#include "../process/process.hh" +#include "../core/core.hh" #include "templates.hh" #include "cli.hh" -#ifndef CMD_RUNNER -#define CMD_RUNNER -#endif - -#ifndef SSC_BUILD_TIME -#define SSC_BUILD_TIME 0 +#ifndef SOCKET_RUNTIME_BUILD_TIME +#define SOCKET_RUNTIME_BUILD_TIME 0 #endif using namespace SSC; @@ -66,30 +71,20 @@ Process* buildAfterScriptProcess = nullptr; FileSystemWatcher* sourcesWatcher = nullptr; Thread* sourcesWatcherSupportThread = nullptr; -Mutex signalHandlerMutex; - -String _settings; Path targetPath; +String settingsSource = ""; Map settings; Map rc; auto start = system_clock::now(); bool flagDebugMode = true; -bool flagVerboseMode = true; +bool flagVerboseMode = false; bool flagQuietMode = false; Map defaultTemplateAttrs; -#if defined(__APPLE__) -std::atomic<bool> checkLogStore = true; -static dispatch_queue_t queue = dispatch_queue_create( - "socket.runtime.cli.queue", - dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_CONCURRENT, - QOS_CLASS_USER_INITIATED, - -1 - ) -); +#if SOCKET_RUNTIME_PLATFORM_APPLE +Atomic<bool> checkLogStore = true; #endif void log (const String s) { @@ -150,24 +145,30 @@ bool equal (const String& s1, const String& s2) { return s1.compare(s2) == 0; }; -const Map SSC::getUserConfig () { - return settings; -} +extern "C" { + const unsigned char* socket_runtime_init_get_user_config_bytes () { + return reinterpret_cast<const unsigned char*>(settingsSource.c_str()); + } -const String SSC::getDevHost () { - return settings["host"]; -} + unsigned int socket_runtime_init_get_user_config_bytes_size () { + return settingsSource.size(); + } -int SSC::getDevPort () { - if (settings.contains("port")) { - return std::stoi(settings["port"].c_str()); + bool socket_runtime_init_is_debug_enabled () { + return DEBUG == 1; } - return 0; -} + const char* socket_runtime_init_get_dev_host () { + return settings["host"].c_str(); + } -bool SSC::isDebugEnabled () { - return DEBUG == 1; + int socket_runtime_init_get_dev_port () { + if (settings.contains("port")) { + return std::stoi(settings["port"].c_str()); + } + + return 0; + } } void printHelp (const String& command) { @@ -302,204 +303,112 @@ inline String prefixFile () { return socketHome; } -static Process::id_type appPid = 0; -static Process* appProcess = nullptr; +static Process::PID appPid = 0; +static SharedPointer<Process> appProcess = nullptr; static std::atomic<int> appStatus = -1; static std::mutex appMutex; +static uv_loop_t *loop = nullptr; + +static uv_udp_t logsocket; +static int lastLogSequence = 0; + +#if SOCKET_RUNTIME_PLATFORM_APPLE +static NSDate* lastLogTime = [NSDate now]; +unsigned short createLogSocket() { + std::promise<int> p; + std::future<int> future = p.get_future(); + int port = 0; + + auto t = std::thread([](std::promise<int>&& p) { + loop = uv_default_loop(); + + uv_udp_init(loop, &logsocket); + struct sockaddr_in addr; + int port; + + uv_ip4_addr("0.0.0.0", 0, &addr); + uv_udp_bind(&logsocket, (const struct sockaddr*)&addr, UV_UDP_REUSEADDR); + + uv_udp_recv_start( + &logsocket, + [](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { + *buf = uv_buf_init(new char[suggested_size], suggested_size); + }, + [](uv_udp_t* req, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags) { + if (nread > 0) { + std::string data(buf->base, nread); + data = trim(data); // Assuming trim is defined elsewhere + if (data[0] != '+') return; + + @autoreleasepool { + NSError* err = nil; + auto logs = [OSLogStore storeWithScope: OSLogStoreSystem error: &err]; // get snapshot + + if (err) { + std::cerr << "ERROR: Failed to open OSLogStore" << std::endl; + return; + } -#if defined(__APPLE__) -void pollOSLogStream (bool isForDesktop, String bundleIdentifier, int processIdentifier) { - // It appears there is a bug with `:predicateWithFormat:` as the - // following does not appear to work: - // - // [NSPredicate - // predicateWithFormat: @"processIdentifier == %d AND subsystem == '%s'", - // app.processIdentifier, - // bundle.bundleIdentifier // or even a literal string "co.socketsupply.socket.tests" - // ]; - // - // We can build the predicate query string manually, instead. - auto queryStream = StringStream {}; - auto pid = std::to_string(processIdentifier); - auto bid = bundleIdentifier.c_str(); - queryStream - << "(" - << (isForDesktop - ? "category == 'socket.runtime.desktop'" - : "category == 'socket.runtime.mobile'") - << " OR category == 'socket.runtime.debug'" - << ") AND "; - - if (processIdentifier > 0) { - queryStream << "processIdentifier == " << pid << " AND "; - } - - queryStream << "subsystem == '" << bid << "'"; - // log store query and predicate for filtering logs based on the currently - // running application that was just launched and those of a subsystem - // directly related to the application's bundle identifier which allows us - // to just get logs that came from the application (not foundation/cocoa/webkit) - const auto query = [NSString stringWithUTF8String: queryStream.str().c_str()]; - const auto predicate = [NSPredicate predicateWithFormat: query]; - - // use the launch date as the initial marker - const auto now = [NSDate now]; - // and offset it by 1 second in the past as the initial position in the eumeration - auto offset = [now dateByAddingTimeInterval: -1]; - - // tracks the latest log entry date so we ignore older ones - NSDate* latest = nil; - NSError* error = nil; - - int pollsAfterTermination = 16; - int backoffIndex = 0; - - // lucas series of backoffs - static int backoffs[] = { - 16*2, - 16*1, - 16*3, - 16*4, - 16*7, - 16*11, - 16*18, - 16*29, - 32*2, - 32*1, - 32*3, - 32*4, - 32*7, - 32*11, - 32*18, - 32*29, - 64*2, - 64*1, - 64*3, - 64*4, - 64*7, - 64*11, - 64*18, - 64*29, - }; - - if (processIdentifier > 0) { - dispatch_async(queue, ^{ - while (kill(processIdentifier, 0) == 0) { - msleep(256); - } - }); - } - - while (appStatus < 0 || pollsAfterTermination > 0) { - if (appStatus >= 0) { - pollsAfterTermination = pollsAfterTermination - 1; - checkLogStore = true; - } - - if (!checkLogStore) { - auto backoff = backoffs[backoffIndex]; - backoffIndex = ( - (backoffIndex + 1) % - (sizeof(backoffs) / sizeof(backoffs[0])) - ); - - msleep(backoff); - if (processIdentifier > 0) { - continue; - } - } - - // this is may be set to `true` from a `SIGUSR1` signal - checkLogStore = false; - @autoreleasepool { - // We need a new `OSLogStore` in each so we can keep enumeratoring - // the logs until the application terminates. This is required because - // each `OSLogStore` instance is a snapshot of the system's universal - // log archive at the time of instantiation. - auto logs = [OSLogStore - storeWithScope: OSLogStoreSystem - error: &error - ]; + NSDate* adjustedLogTime = [lastLogTime dateByAddingTimeInterval: -1]; // adjust by subtracting 1 second + auto position = [logs positionWithDate: adjustedLogTime]; + auto bid = settings["meta_bundle_identifier"]; + auto query = String("(category == 'socket.runtime') AND (subsystem == '" + bid + "')"); + auto predicate = [NSPredicate predicateWithFormat: [NSString stringWithUTF8String: query.c_str()]]; + auto enumerator = [logs entriesEnumeratorWithOptions: 0 position: position predicate: predicate error: &err]; - if (error) { - appStatus = 1; - debug( - "ERROR: OSLogStore: (code=%lu, domain=%@) %@", - error.code, - error.domain, - error.localizedDescription - ); - break; - } + if (err) { + std::cerr << "ERROR: Failed to open OSLogStore" << std::endl; + return; + } - auto position = [logs positionWithDate: offset]; - auto enumerator = [logs - entriesEnumeratorWithOptions: 0 - position: position - predicate: predicate - error: &error - ]; + id logEntry; + + while ((logEntry = [enumerator nextObject]) != nil) { + OSLogEntryLog* entry = (OSLogEntryLog*)logEntry; + String message = entry.composedMessage.UTF8String; + String body; + int seq = 0; + Vector<String> parts = split(message, "::::"); + if (parts.size() < 2) continue; + + try { + seq = std::stoi(parts[0]); + body = parts[1]; + if (body.size() == 0) continue; + } catch (...) { + continue; + } - if (error) { - appStatus = 1; - debug( - "ERROR: OSLogEnumerator: (code=%lu, domain=%@) %@", - error.code, - error.domain, - error.localizedDescription - ); - break; - } + if (seq <= lastLogSequence && lastLogSequence > 0) continue; + lastLogSequence = seq; - // Enumerate all the logs in this loop and print unredacted and most - // recently log entries to stdout - for (OSLogEntryLog* entry in enumerator) { - if ( - entry.composedMessage && - (processIdentifier == 0 || entry.processIdentifier == processIdentifier) - ) { - // visit latest log - if (!latest || [latest compare: entry.date] == NSOrderedAscending) { - auto message = entry.composedMessage.UTF8String; - - // the OSLogStore may redact log messages the user does not - // have access to, filter them out - if (String(message) != "<private>") { - if (String(message).starts_with("__EXIT_SIGNAL__")) { - if (appStatus == -1) { - appStatus = std::stoi(replace(String(message), "__EXIT_SIGNAL__=", "")); - } - } else if ( - entry.level == OSLogEntryLogLevelDebug || - entry.level == OSLogEntryLogLevelError || - entry.level == OSLogEntryLogLevelFault - ) { - if (entry.level == OSLogEntryLogLevelDebug) { - if (flagDebugMode) { - std::cerr << message << std::endl; - } - } else { - std::cerr << message << std::endl; - } - } else { - std::cout << message << std::endl; + std::cout << body << std::endl; } } - - backoffIndex = 0; - latest = entry.date; - offset = latest; } + + if (buf->base) delete[] buf->base; } - } + ); + + int len = sizeof(addr); + + if (uv_udp_getsockname(&logsocket, (struct sockaddr *)&addr, &len) == 0) { + auto port = ntohs(addr.sin_port); + p.set_value(port); } - } - appMutex.unlock(); + uv_run(loop, UV_RUN_DEFAULT); + }, std::move(p)); + + port = future.get(); + t.detach(); + + return port; } #endif -void handleBuildPhaseForUserScript ( +void handleBuildPhaseForUser ( const Map settings, const String& targetPlatform, const Path pathResourcesRelativeToUserBuild, @@ -537,10 +446,13 @@ void handleBuildPhaseForUserScript ( buildScript = "cmd.exe"; } + auto scriptPath = (cwd / targetPath).string(); + log("running build script (cmd='" + buildScript + "', args='" + scriptArgs + "', pwd='" + scriptPath + "')"); + auto process = new SSC::Process( buildScript, scriptArgs, - (cwd / targetPath).string(), + scriptPath, [](SSC::String const &out) { IO::write(out, false); }, [](SSC::String const &out) { IO::write(out, true); } ); @@ -655,10 +567,11 @@ Vector<Path> handleBuildPhaseForCopyMappedFiles ( dst = fs::absolute(dst); if (!fs::exists(fs::status(src))) { - log("WARNING: [copy-map] entry '" + src.string() + "' does not exist"); + log("WARNING: [build.copy-map] entry '" + convertWStringToString(src.string()) + "' does not exist"); continue; } + // ensure 'dst' parent directories exist if (!fs::exists(fs::status(dst.parent_path()))) { fs::create_directories(dst.parent_path()); } @@ -677,6 +590,14 @@ Vector<Path> handleBuildPhaseForCopyMappedFiles ( } } + if (flagVerboseMode) { + debug( + "copy %s ~> %s", + fs::relative(src, targetPath).c_str(), + fs::relative(dst, targetPath).c_str() + ); + } + fs::copy( src, dst, @@ -833,75 +754,70 @@ Vector<Path> handleBuildPhaseForCopyMappedFiles ( return copyMapFiles; } -void signalHandler (int signal) { -#if !defined(_WIN32) - if (signal == SIGUSR1) { - #if defined(__APPLE__) +void signalHandler (int signum) { +#if !SOCKET_RUNTIME_PLATFORM_WINDOWS + if (signum == SIGUSR1) { + #if SOCKET_RUNTIME_PLATFORM_APPLE checkLogStore = true; #endif return; } #endif - Lock lock(signalHandlerMutex); - -#if !defined(_WIN32) +#if !SOCKET_RUNTIME_PLATFORM_WINDOWS if (appPid > 0) { - kill(appPid, signal); + kill(appPid, signum); } #endif - if (appProcess != nullptr) { - appProcess->kill(); - } - - appPid = 0; - -#if defined(__linux__) && !defined(__ANDROID__) - if (gtk_main_level() > 0) { - gtk_main_quit(); - } -#endif + if (signum == SIGINT || signum == SIGTERM) { + if (appProcess != nullptr) { + appProcess->kill(signum); + appProcess = nullptr; + } - if (sourcesWatcher != nullptr) { - sourcesWatcher->stop(); - delete sourcesWatcher; - sourcesWatcher = nullptr; - } + appPid = 0; - if (sourcesWatcherSupportThread != nullptr) { - if (sourcesWatcherSupportThread->joinable()) { - sourcesWatcherSupportThread->join(); + if (sourcesWatcherSupportThread != nullptr) { + if (sourcesWatcherSupportThread->joinable()) { + sourcesWatcherSupportThread->join(); + } + delete sourcesWatcherSupportThread; + sourcesWatcherSupportThread = nullptr; } - delete sourcesWatcherSupportThread; - sourcesWatcherSupportThread = nullptr; - } - if (buildAfterScriptProcess != nullptr) { - buildAfterScriptProcess->kill(); - buildAfterScriptProcess->wait(); - delete buildAfterScriptProcess; - buildAfterScriptProcess = nullptr; - } + if (buildAfterScriptProcess != nullptr) { + buildAfterScriptProcess->kill(); + buildAfterScriptProcess->wait(); + delete buildAfterScriptProcess; + buildAfterScriptProcess = nullptr; + } - if (appStatus == -1) { - appStatus = signal; - log("App result: " + std::to_string(signal)); - } + if (appStatus == -1) { + appStatus = signum; + log("App result: " + std::to_string(signum)); + } - if (signal == SIGTERM) { - exit(1); - return; - } + if (signum == SIGTERM || signum == SIGINT) { + signal(signum, SIG_DFL); + raise(signum); + } -#if !defined(_WIN32) - if (signal == SIGUSR2) { - exit(0); - return; + #if SOCKET_RUNTIME_PLATFORM_LINUX + msleep(500); + if (gtk_main_level() > 0) { + g_main_context_invoke( + nullptr, + +[](gpointer userData) -> gboolean { + msleep(16); + gtk_main_quit(); + return true; + }, + nullptr + ); + } + #endif } -#endif - - exit(signal); } void checkIosSimulatorDeviceAvailability (const String& device) { @@ -968,97 +884,108 @@ int runApp (const Path& path, const String& args, bool headless) { } } -#if defined(__APPLE__) - if (platform.mac) { - auto sharedWorkspace = [NSWorkspace sharedWorkspace]; - auto configuration = [NSWorkspaceOpenConfiguration configuration]; - auto string = path.string(); - auto slice = string.substr(0, string.find(".app") + 4); - auto url = [NSURL - fileURLWithPath: [NSString stringWithUTF8String: slice.c_str()] - ]; + #if defined(__APPLE__) + if (platform.mac) { + auto sharedWorkspace = [NSWorkspace sharedWorkspace]; + auto configuration = [NSWorkspaceOpenConfiguration configuration]; + auto stringPath = path.string(); + auto slice = stringPath.substr(0, stringPath.rfind(".app") + 4); - auto bundle = [NSBundle bundleWithURL: url]; - auto env = [[NSMutableDictionary alloc] init]; + auto url = [NSURL + fileURLWithPath: [NSString stringWithUTF8String: slice.c_str()] + ]; - env[@"SSC_CLI_PID"] = [NSString stringWithFormat: @"%d", getpid()]; + auto bundle = [NSBundle bundleWithURL: url]; + auto env = [[NSMutableDictionary alloc] init]; - for (auto const &envKey : parseStringList(settings["build_env"])) { - auto cleanKey = trim(envKey); - auto envValue = Env::get(cleanKey.c_str()); - auto key = [NSString stringWithUTF8String: cleanKey.c_str()]; - auto value = [NSString stringWithUTF8String: envValue.c_str()]; + env[@"SSC_CLI_PID"] = [NSString stringWithFormat: @"%d", getpid()]; - env[key] = value; - } + for (auto const &envKey : parseStringList(settings["build_env"])) { + auto cleanKey = trim(envKey); - auto splitArgs = split(args, ' '); - auto arguments = [[NSMutableArray alloc] init]; + cleanKey.erase(0, cleanKey.find_first_not_of(",")); + cleanKey.erase(cleanKey.find_last_not_of(",") + 1); - for (auto arg : splitArgs) { - [arguments addObject: [NSString stringWithUTF8String: arg.c_str()]]; - } + auto envValue = Env::get(cleanKey.c_str()); + auto key = [NSString stringWithUTF8String: cleanKey.c_str()]; + auto value = [NSString stringWithUTF8String: envValue.c_str()]; - [arguments addObject: @"--from-ssc"]; + env[key] = value; + } - configuration.createsNewApplicationInstance = YES; - configuration.promptsUserIfNeeded = YES; - configuration.environment = env; - configuration.arguments = arguments; - configuration.activates = headless ? NO : YES; + auto splitArgs = split(args, ' '); + auto arguments = [[NSMutableArray alloc] init]; - log(String("Running App: " + String(bundle.bundlePath.UTF8String))); + for (auto arg : splitArgs) { + [arguments addObject: [NSString stringWithUTF8String: arg.c_str()]]; + } - appMutex.lock(); + [arguments addObject: @"--from-ssc"]; - [sharedWorkspace - openApplicationAtURL: bundle.bundleURL - configuration: configuration - completionHandler: ^(NSRunningApplication* app, NSError* error) - { - if (error) { - appMutex.unlock(); - appStatus = 1; - debug( - "ERROR: NSWorkspace: (code=%lu, domain=%@) %@", - error.code, - error.domain, - error.localizedDescription - ); - return; + auto port = std::to_string(createLogSocket()); + env[@"SSC_LOG_SOCKET"] = @(port.c_str()); + + auto parentLogSocket = Env::get("SSC_PARENT_LOG_SOCKET"); + if (parentLogSocket.size() > 0) { + env[@"SSC_PARENT_LOG_SOCKET"] = @(parentLogSocket.c_str()); } - appPid = app.processIdentifier; + configuration.createsNewApplicationInstance = YES; + configuration.promptsUserIfNeeded = YES; + configuration.environment = env; + configuration.arguments = arguments; + configuration.activates = headless ? NO : YES; - pollOSLogStream( - true, - String(bundle.bundleIdentifier.UTF8String), - app.processIdentifier - ); - }]; + if (!bundle) { + log("Unable to find the application bundle"); + return 1; + } - // wait for `NSRunningApplication` to terminate - std::lock_guard<std::mutex> lock(appMutex); - if (appStatus != -1) { - log("App result: " + std::to_string(appStatus.load())); - return appStatus.load(); - } + log(String("Running App: " + String(bundle.bundlePath.UTF8String))); - return 0; - } -#endif + appMutex.lock(); + + [sharedWorkspace + openApplicationAtURL: bundle.bundleURL + configuration: configuration + completionHandler: ^(NSRunningApplication* app, NSError* error) + { + if (error) { + appMutex.unlock(); + appStatus = 1; + debug( + "ERROR: NSWorkspace: (code=%lu, domain=%@) %@", + error.code, + error.domain, + error.localizedDescription + ); + } else { + appPid = app.processIdentifier; + } + }]; + + std::lock_guard<std::mutex> lock(appMutex); + + if (appStatus != -1) { + log("App result: " + std::to_string(appStatus.load())); + return appStatus.load(); + } + + return 0; + } + #endif log(String("Running App: " + headlessCommand + prefix + cmd + args + " --from-ssc")); -#if defined(__linux__) + #if defined(__linux__) // unlink lock file that may existk static const auto bundleIdentifier = settings["meta_bundle_identifier"]; static const auto TMPDIR = Env::get("TMPDIR", "/tmp"); static const auto appInstanceLock = fs::path(TMPDIR) / (bundleIdentifier + ".lock"); unlink(appInstanceLock.c_str()); -#endif + #endif - appProcess = new SSC::Process( + appProcess = std::make_shared<Process>( headlessCommand + prefix + cmd, args + " --from-ssc", fs::current_path().string(), @@ -1067,17 +994,16 @@ int runApp (const Path& path, const String& args, bool headless) { [](const auto& output) { signalHandler(std::atoi(output.c_str())); } ); - appPid = appProcess->open(); - const auto status = appProcess->wait(); + auto p = appProcess; + appPid = p->open(); + const auto status = p->wait(); + if (appStatus == -1) { appStatus = status; log("App result: " + std::to_string(appStatus.load())); } - if (appProcess != nullptr) { - delete appProcess; - appProcess = nullptr; - } + p = nullptr; return appStatus; } @@ -1088,125 +1014,133 @@ int runApp (const Path& path, const String& args) { void runIOSSimulator (const Path& path, Map& settings) { #ifndef _WIN32 - checkIosSimulatorDeviceAvailability(settings["ios_simulator_device"]); + String uuid; + bool booted = false; - String deviceType; - StringStream listDeviceTypesCommand; - listDeviceTypesCommand - << "xcrun" - << " simctl" - << " list devicetypes"; + if (settings.count("ios_simulator_uuid") > 0) { + uuid = settings["ios_simulator_uuid"]; + } else { + checkIosSimulatorDeviceAvailability(settings["ios_simulator_device"]); - auto rListDeviceTypes = exec(listDeviceTypesCommand.str().c_str()); - if (rListDeviceTypes.exitCode != 0) { - log("failed to list device types using \"" + listDeviceTypesCommand.str() + "\""); - if (rListDeviceTypes.output.size() > 0) { - log(rListDeviceTypes.output); + String deviceType; + StringStream listDeviceTypesCommand; + listDeviceTypesCommand + << "xcrun" + << " simctl" + << " list devicetypes"; + + auto rListDeviceTypes = exec(listDeviceTypesCommand.str().c_str()); + if (rListDeviceTypes.exitCode != 0) { + log("failed to list device types using \"" + listDeviceTypesCommand.str() + "\""); + if (rListDeviceTypes.output.size() > 0) { + log(rListDeviceTypes.output); + } + exit(rListDeviceTypes.exitCode); } - exit(rListDeviceTypes.exitCode); - } - std::regex reDeviceType(settings["ios_simulator_device"] + "\\s\\((com.apple.CoreSimulator.SimDeviceType.(?:.+))\\)"); - std::smatch match; + std::regex reDeviceType(settings["ios_simulator_device"] + "\\s\\((com.apple.CoreSimulator.SimDeviceType.(?:.+))\\)"); + std::smatch match; - if (std::regex_search(rListDeviceTypes.output, match, reDeviceType)) { - deviceType = match.str(1); - log("simulator device type: " + deviceType); - } else { - auto rListDevices = exec("xcrun simctl list devicetypes | grep iPhone"); - log( - "failed to find device type: " + settings["ios_simulator_device"] + ". " - "Please provide correct device name for the \"ios_simulator_device\". " - "The list of available devices:\n" + rListDevices.output - ); - if (rListDevices.output.size() > 0) { - log(rListDevices.output); + if (std::regex_search(rListDeviceTypes.output, match, reDeviceType)) { + deviceType = match.str(1); + log("simulator device type: " + deviceType); + } else { + auto rListDevices = exec("xcrun simctl list devicetypes"); + log( + "failed to find device type: " + settings["ios_simulator_device"] + ". " + "Please provide correct device name for the \"ios_simulator_device\". " + "The list of available devices:\n" + rListDevices.output + ); + if (rListDevices.output.size() > 0) { + log(rListDevices.output); + } + exit(rListDevices.exitCode); } - exit(rListDevices.exitCode); - } - StringStream listDevicesCommand; - listDevicesCommand - << "xcrun" - << " simctl" - << " list devices available"; - auto rListDevices = exec(listDevicesCommand.str().c_str()); - if (rListDevices.exitCode != 0) { - log("failed to list available devices using \"" + listDevicesCommand.str() + "\""); - if (rListDevices.output.size() > 0) { - log(rListDevices.output); - } - exit(rListDevices.exitCode); - } + StringStream listDevicesCommand; - auto iosSimulatorDeviceSuffix = settings["ios_simulator_device"]; - std::replace(iosSimulatorDeviceSuffix.begin(), iosSimulatorDeviceSuffix.end(), ' ', '_'); - std::regex reSocketSDKDevice("SocketSimulator_" + iosSimulatorDeviceSuffix + "\\s\\((.+)\\)\\s\\((.+)\\)"); + listDevicesCommand + << "xcrun" + << " simctl" + << " list devices available"; - String uuid; - bool booted = false; + auto rListDevices = exec(listDevicesCommand.str().c_str()); - if (std::regex_search(rListDevices.output, match, reSocketSDKDevice)) { - uuid = match.str(1); - booted = match.str(2).find("Booted") != String::npos; + if (rListDevices.exitCode != 0) { + log("failed to list available devices using \"" + listDevicesCommand.str() + "\""); - log("found Socket simulator VM for " + settings["ios_simulator_device"] + " with uuid: " + uuid); - if (booted) { - log("Socket simulator VM is booted"); - } else { - log("Socket simulator VM is not booted"); + if (rListDevices.output.size() > 0) { + log(rListDevices.output); + } + exit(rListDevices.exitCode); } - } else { - log("creating a new iOS simulator VM for " + settings["ios_simulator_device"]); - StringStream listRuntimesCommand; - listRuntimesCommand - << "xcrun" - << " simctl" - << " list runtimes available"; - auto rListRuntimes = exec(listRuntimesCommand.str().c_str()); - if (rListRuntimes.exitCode != 0) { - log("failed to list available runtimes using \"" + listRuntimesCommand.str() + "\""); - if (rListRuntimes.output.size() > 0) { - log(rListRuntimes.output); - } - exit(rListRuntimes.exitCode); - } - auto const runtimes = split(rListRuntimes.output, '\n'); - String runtime; - // TODO: improve iOS version detection - for (auto it = runtimes.rbegin(); it != runtimes.rend(); ++it) { - if (it->find("iOS") != String::npos) { - runtime = trim(*it); - log("found runtime: " + runtime); - break; + auto iosSimulatorDeviceSuffix = settings["ios_simulator_device"]; + std::replace(iosSimulatorDeviceSuffix.begin(), iosSimulatorDeviceSuffix.end(), ' ', '_'); + std::regex reSocketSDKDevice("SocketSimulator_" + iosSimulatorDeviceSuffix + "\\s\\((.+)\\)\\s\\((.+)\\)"); + + if (std::regex_search(rListDevices.output, match, reSocketSDKDevice)) { + uuid = match.str(1); + booted = match.str(2).find("Booted") != String::npos; + + log("found Socket simulator VM for " + settings["ios_simulator_device"] + " with uuid: " + uuid); + if (booted) { + log("Socket simulator VM is booted"); + } else { + log("Socket simulator VM is not booted"); } - } + } else { + log("creating a new iOS simulator VM for " + settings["ios_simulator_device"]); - std::regex reRuntime(R"(com.apple.CoreSimulator.SimRuntime.iOS(?:.*))"); - std::smatch matchRuntime; - String runtimeId; + StringStream listRuntimesCommand; + listRuntimesCommand + << "xcrun" + << " simctl" + << " list runtimes available"; + auto rListRuntimes = exec(listRuntimesCommand.str().c_str()); + if (rListRuntimes.exitCode != 0) { + log("failed to list available runtimes using \"" + listRuntimesCommand.str() + "\""); + if (rListRuntimes.output.size() > 0) { + log(rListRuntimes.output); + } + exit(rListRuntimes.exitCode); + } + auto const runtimes = split(rListRuntimes.output, '\n'); + String runtime; + // TODO: improve iOS version detection + for (auto it = runtimes.rbegin(); it != runtimes.rend(); ++it) { + if (it->find("iOS") != String::npos) { + runtime = trim(*it); + log("found runtime: " + runtime); + break; + } + } - if (std::regex_search(runtime, matchRuntime, reRuntime)) { - runtimeId = matchRuntime.str(0); - } + std::regex reRuntime(R"(com.apple.CoreSimulator.SimRuntime.iOS(?:.*))"); + std::smatch matchRuntime; + String runtimeId; + + if (std::regex_search(runtime, matchRuntime, reRuntime)) { + runtimeId = matchRuntime.str(0); + } - StringStream createSimulatorCommand; - createSimulatorCommand - << "xcrun simctl" - << " create SocketSimulator_" + iosSimulatorDeviceSuffix - << " " << deviceType - << " " << runtimeId; + StringStream createSimulatorCommand; + createSimulatorCommand + << "xcrun simctl" + << " create SocketSimulator_" + iosSimulatorDeviceSuffix + << " " << deviceType + << " " << runtimeId; - auto rCreateSimulator = exec(createSimulatorCommand.str().c_str()); - if (rCreateSimulator.exitCode != 0) { - log("unable to create simulator VM"); - if (rCreateSimulator.output.size() > 0) { - log(rCreateSimulator.output); + auto rCreateSimulator = exec(createSimulatorCommand.str().c_str()); + if (rCreateSimulator.exitCode != 0) { + log("unable to create simulator VM"); + if (rCreateSimulator.output.size() > 0) { + log(rCreateSimulator.output); + } + exit(rCreateSimulator.exitCode); } - exit(rCreateSimulator.exitCode); + uuid = rCreateSimulator.output; } - uuid = rCreateSimulator.output; } if (!booted) { @@ -1354,18 +1288,16 @@ bool setupAndroidAvd (AndroidCliState& state) { String package = state.quote + "system-images;" + state.platform + ";google_apis;" + replace(platform.arch, "arm64", "arm64-v8a") + state.quote; state.avdmanager << state.androidHome; - if (Env::get("ANDROID_SDK_MANAGER").size() > 0) - { + if (Env::get("ANDROID_SDK_MANAGER").size() > 0) { state.avdmanager << "/" << replace(Env::get("ANDROID_SDK_MANAGER"), "sdkmanager", "avdmanager"); - } - else { + } else { if (!platform.win) { if (std::system(("avdmanager list " + state.devNull).c_str()) != 0) { state.avdmanager << "/cmdline-tools/latest/bin/"; } - } - else + } else { state.avdmanager << "\\cmdline-tools\\latest\\bin\\"; + } } if (!fs::exists(state.avdmanager.str())) { @@ -1545,7 +1477,7 @@ bool startAndroidApp (AndroidCliState& state) { auto mainActivity = settings["android_main_activity"]; if (mainActivity.size() == 0) { - mainActivity = String(DEFAULT_ANDROID_ACTIVITY_NAME); + mainActivity = String(DEFAULT_ANDROID_MAIN_ACTIVITY_NAME); } state.adbShellStart << "shell am start -n " << settings["android_bundle_identifier"] << "/" << settings["android_bundle_identifier"] << mainActivity << " 2>&1"; @@ -1680,7 +1612,12 @@ void initializeRC (Path targetPath) { } if (fs::exists(path) && fs::is_regular_file(path)) { - extendMap(rc, INI::parse(readFile(path))); + extendMap(rc, INI::parse(tmpl(readFile(path), Map { + {"platform.arch", platform.arch}, + {"platform.arch.short", replace(platform.arch, "x86_64", "x64")}, + {"platform.os", platform.os}, + {"platform.os.short", replace(platform.os, "win32", "win")} + }))); for (const auto& tuple : rc) { auto key = tuple.first; @@ -1766,7 +1703,8 @@ void run (const String& targetPlatform, Map& settings, const Paths& paths, const exit(0); } else { auto executable = Path(settings["build_name"] + (platform.win ? ".exe" : "")); - auto exitCode = runApp(paths.pathBin / executable, argvForward, flagRunHeadless); + auto pathToExecutable = (paths.pathBin / executable).string(); + auto exitCode = runApp(pathToExecutable, argvForward, flagRunHeadless); return exit(exitCode); } @@ -1775,22 +1713,23 @@ void run (const String& targetPlatform, Map& settings, const Paths& paths, const exit(1); } -struct Option { +struct CommandLineOption { std::vector<String> aliases; bool isOptional; bool shouldHaveValue; }; -using Options = std::vector<Option>; + +using CommandLineOptions = Vector<CommandLineOption>; struct optionsAndEnv { Map optionsWithValue; std::unordered_set<String> optionsWithoutValue; - std::vector<String> envs; + Vector<String> envs; }; optionsAndEnv parseCommandLineOptions ( const std::span<const char*>& options, - const Options& availableOptions, + const CommandLineOptions& availableOptions, const String& subcommand ) { optionsAndEnv result; @@ -1838,7 +1777,7 @@ optionsAndEnv parseCommandLineOptions ( } // find option - Option recognizedOption; + CommandLineOption recognizedOption; bool found = false; for (const auto option : availableOptions) { for (const auto alias : option.aliases) { @@ -1930,7 +1869,7 @@ optionsAndEnv parseCommandLineOptions ( return result; } -int main (const int argc, const char* argv[]) { +int main (int argc, char* argv[]) { defaultTemplateAttrs = { { "ssc_version", SSC::VERSION_FULL_STRING }, { "project_name", "beepboop" } @@ -1942,11 +1881,11 @@ int main (const int argc, const char* argv[]) { auto const subcommand = argv[1]; -#ifndef _WIN32 - signal(SIGHUP, signalHandler); - signal(SIGUSR1, signalHandler); - signal(SIGUSR2, signalHandler); -#endif + #ifndef _WIN32 + signal(SIGHUP, signalHandler); + signal(SIGUSR1, signalHandler); + signal(SIGUSR2, signalHandler); + #endif signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); @@ -1967,7 +1906,11 @@ int main (const int argc, const char* argv[]) { exit(0); } - if (subcommand[0] == '-') { + if (equal(subcommand, "--verbose")) { + flagVerboseMode = true; + argc--; + argv++; + } else if (subcommand[0] == '-') { log("unknown option: " + String(subcommand)); printHelp("ssc"); exit(0); @@ -2004,14 +1947,14 @@ int main (const int argc, const char* argv[]) { auto getPaths = [&](String platform) -> Paths { Paths paths; - String platformPath = platform == "win32" - ? "win" - : platform; + String platformPath = platform == "win32" ? "win" : platform; + paths.platformSpecificOutputPath = { targetPath / settings["build_output"] / platformPath }; + if (platform == "mac") { Path pathBase = "Contents"; Path packageName = Path(settings["build_name"] + ".app"); @@ -2071,12 +2014,12 @@ int main (const int argc, const char* argv[]) { auto createSubcommand = [&]( const String& subcommand, - const Options& availableOptions, + const CommandLineOptions& availableOptions, const bool& needsConfig, std::function<void(Map, std::unordered_set<String>)> subcommandHandler ) -> void { if (argv[1] == subcommand) { - auto commandlineOptions = std::span(argv, argc).subspan(2, numberOfOptions); + auto commandlineOptions = std::span(const_cast<const char**>(argv), argc).subspan(2, numberOfOptions); auto optionsAndEnv = parseCommandLineOptions(commandlineOptions, availableOptions, subcommand); auto envs = optionsAndEnv.envs; @@ -2122,10 +2065,11 @@ int main (const int argc, const char* argv[]) { } } - ini += "\n"; - ini += "[ssc]\n"; - ini += "argv = " + arguments; - ini += "\n"; + if (arguments.size() > 0) { + ini += "\n"; + ini += "[ssc]\n"; + ini += "argv = " + arguments; + } ini += "\n"; if (configExists) { @@ -2152,11 +2096,18 @@ int main (const int argc, const char* argv[]) { } code = String( - "constexpr unsigned char __ssc_config_bytes["+ std::to_string(size) +"] = {\n" + bytes.str() + "\n};" + "static const unsigned char __socket_runtime_user_config_bytes["+ std::to_string(size) +"] = {\n" + bytes.str() + "\n};" ); } - settings = INI::parse(ini); + settingsSource = tmpl(ini, Map { + {"platform.arch", platform.arch}, + {"platform.arch.short", replace(platform.arch, "x86_64", "x64")}, + {"platform.os", platform.os}, + {"platform.os.short", replace(platform.os, "win32", "win")} + }); + + settings = INI::parse(settingsSource); if (settings["meta_type"] == "extension" || settings["build_type"] == "extension") { auto extension = settings["build_name"]; @@ -2264,9 +2215,10 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value - Options initOptions = { + CommandLineOptions initOptions = { { { "--config", "-C" }, true, false }, - { { "--name", "-n" }, true, true } + { { "--name", "-n" }, true, true }, + { { "--vebose", "-V" }, true, false } }; createSubcommand("init", initOptions, false, [&](Map optionsWithValue, std::unordered_set<String> optionsWithoutValue) -> void { auto isTargetPathEmpty = fs::exists(targetPath) ? fs::is_empty(targetPath) : true; @@ -2282,16 +2234,19 @@ int main (const int argc, const char* argv[]) { writeFile(targetPath / "src" / "index.html", gHelloWorld); log("src/index.html created in " + targetPath.string()); + writeFile(targetPath / "src" / "index.js", gHelloWorldScript); + log("src/index.js created in " + targetPath.string()); + // copy icon.png fs::copy(trim(prefixFile("assets/icon.png")), targetPath / "src" / "icon.png", fs::copy_options::overwrite_existing); log("icon.png created in " + targetPath.string() + "/src"); - if (platform.win) { + if (platform.win) { // copy icon.ico fs::copy(trim(prefixFile("assets/icon.ico")), targetPath / "src" / "icon.ico", fs::copy_options::overwrite_existing); log("icon.ico created in " + targetPath.string() + "/src"); - } + } } else { log("Current directory was not empty. Assuming index.html and icon are already in place."); } @@ -2320,11 +2275,12 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value - Options listDevicesOptions = { + CommandLineOptions listDevicesOptions = { { { "--platform" }, false, true }, { { "--ecid" }, true, false }, { { "--udid" }, true, false }, - { { "--only" }, true, false } + { { "--only" }, true, false }, + { { "--vebose", "-V" }, true, false } }; createSubcommand("list-devices", listDevicesOptions, false, [&](Map optionsWithValue, std::unordered_set<String> optionsWithoutValue) -> void { bool isUdid = @@ -2426,7 +2382,7 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value - Options installAppOptions = { + CommandLineOptions installAppOptions = { { { "--debug", "-D" }, true, false }, { { "--device" }, true, true }, { { "--platform" }, true, true }, @@ -2645,8 +2601,9 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value - Options printBuildDirOptions = { - { { "--platform" }, true, true }, { { "--root" }, true, false} + CommandLineOptions printBuildDirOptions = { + { { "--platform" }, true, true }, { { "--root" }, true, false}, + { { "--vebose", "-V" }, true, false } }; createSubcommand("print-build-dir", printBuildDirOptions, true, [&](Map optionsWithValue, std::unordered_set<String> optionsWithoutValue) -> void { @@ -2666,7 +2623,7 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value - Options runOptions = { + CommandLineOptions runOptions = { { { "--platform" }, true, true }, { { "--prod", "-P" }, true, false }, { { "--test", "-t" }, true, true }, @@ -2678,7 +2635,7 @@ int main (const int argc, const char* argv[]) { { { "--host"}, true, true } }; - Options buildOptions = { + CommandLineOptions buildOptions = { { { "--quiet", "-q" }, true, false }, { { "--only-build", "-o" }, true, false }, { { "--run", "-r" }, true, false }, @@ -2686,7 +2643,8 @@ int main (const int argc, const char* argv[]) { { { "--package", "-p" }, true, false }, { { "--package-format", "-f" }, true, true }, { { "--codesign", "-c" }, true, false }, - { { "--notarize", "-n" }, true, false } + { { "--notarize", "-n" }, true, false }, + { { "--vebose", "-V" }, true, false } }; // Insert the elements of runOptions into buildOptions @@ -2706,7 +2664,6 @@ int main (const int argc, const char* argv[]) { log("Please use reverse DNS notation (https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier#discussion)"); exit(1); } - } String argvForward = ""; @@ -2731,6 +2688,11 @@ int main (const int argc, const char* argv[]) { flagShouldPackage = true; } + if (flagShouldRun && flagShouldPackage) { + log("ERROR: use the 'run' command after packaging."); + exit(1); + } + if (flagBuildTest && testFile.size() == 0) { log("ERROR: --test value is required."); exit(1); @@ -2827,6 +2789,10 @@ int main (const int argc, const char* argv[]) { String quote = !platform.win ? "'" : "\""; String slash = !platform.win ? "/" : "\\"; + if (settings.count("meta_compile_bitcode") == 0) settings["meta_compile_bitcode"] = "false"; + if (settings.count("meta_upload_bitcode") == 0) settings["meta_upload_bitcode"] = "false"; + if (settings.count("meta_upload_symbols") == 0) settings["meta_upload_symbols"] = "true"; + if (settings.count("meta_file_limit") == 0) { settings["meta_file_limit"] = "4096"; } @@ -2856,12 +2822,12 @@ int main (const int argc, const char* argv[]) { auto binaryPath = paths.pathBin / executable; auto configPath = targetPath / "socket.ini"; - if (!fs::exists(binaryPath) && !flagBuildForAndroid && !flagBuildForAndroidEmulator) { + if (!fs::exists(binaryPath) && !flagBuildForAndroid && !flagBuildForAndroidEmulator && !flagBuildForIOS && !flagBuildForSimulator) { flagRunUserBuildOnly = false; } else { struct stat stats; if (stat(convertWStringToString(binaryPath).c_str(), &stats) == 0) { - if (SSC_BUILD_TIME > stats.st_mtime) { + if (SOCKET_RUNTIME_BUILD_TIME > stats.st_mtime) { flagRunUserBuildOnly = false; } } @@ -2919,10 +2885,124 @@ int main (const int argc, const char* argv[]) { bool isForDesktop = !flagBuildForIOS && !flagBuildForAndroid; if (isForDesktop) { - fs::create_directories(paths.platformSpecificOutputPath / "include"); - writeFile(paths.platformSpecificOutputPath / "include" / "user-config-bytes.hh", settings["ini_code"]); + fs::create_directories(paths.platformSpecificOutputPath / "include" / "socket"); + writeFile(paths.platformSpecificOutputPath / "include" / "socket" / "_user-config-bytes.hh", settings["ini_code"]); } + // + // Apple requires you to compile XCAssets/AppIcon.appiconset with a catalog for iOS and MacOS. + // + auto compileIconAssets = [&]() { + auto src = paths.platformSpecificOutputPath; + + Vector<Tuple<int, int>> types = {}; + JSON::Array images; + + const String prefix = isForDesktop ? "mac" : "ios"; + const String key = prefix + "_icon_sizes"; + const auto sizes = split(settings[key], " "); + + for (const auto& type : sizes) { + const auto pair = split(type, '@'); + + if (pair.size() != 2 || pair.size() == 0) { + return log("icon size requires <size>@<scale>"); + } + + const String size = pair[0]; + const String scale = pair[1]; + + images.push(JSON::Object::Entries { + { "size", size + "x" + size }, + { "idiom", isForDesktop ? "mac" : "iphone" }, + { "filename", "Icon-" + size + "x" + size + "@" + scale + ".png" }, + { "scale", scale } + }); + + types.push_back(std::make_tuple(stoi(pair[0]), stoi(pair[1]))); + } + + auto assetsPath = fs::path { src / "Assets.xcassets" }; + auto iconsPath = fs::path { assetsPath / "AppIcon.appiconset" }; + + fs::create_directories(iconsPath); + + JSON::Object json = JSON::Object::Entries { + { "images", images }, + { "info", JSON::Object::Entries { + { "version", 1 }, + { "author", "xcode" } + }} + }; + + writeFile(iconsPath / "Contents.json", json.str()); + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { + log(json.str()); + } + + for (const auto& type : types) { + const auto size = std::get<0>(type); + const auto scale = std::get<1>(type); + const auto scaled = std::to_string(size * scale); + const auto destFileName = "Icon-" + std::to_string(size) + "x" + std::to_string(size) + "@" + std::to_string(scale) + "x.png"; + const auto destFilePath = Path { iconsPath / destFileName }; + + const auto src = isForDesktop ? settings["mac_icon"] : settings["ios_icon"]; + + StringStream sipsCommand; + sipsCommand + << "sips" + << " -z " << size << " " << size + << " " << src + << " --out " << destFilePath; + + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { + log(sipsCommand.str()); + } + + const auto r = exec(sipsCommand.str().c_str()); + + if (r.exitCode != 0) { + log("ERROR: failed to create project icons"); + log(r.output); + exit(1); + } + } + + const auto dest = isForDesktop + ? paths.pathResourcesRelativeToUserBuild + : paths.platformSpecificOutputPath; + + StringStream compileAssetsCommand; + compileAssetsCommand + << "xcrun " + << "actool \"" << assetsPath.string() << "\" " + << "--compile \"" << dest.string() << "\" " + << "--platform " << (targetPlatform == "ios" || targetPlatform == "ios-simulator" ? "iphoneos" : "macosx") << " " + << "--minimum-deployment-target 10.15 " + << "--app-icon AppIcon " + << "--output-partial-info-plist " + << "\"" + << (paths.platformSpecificOutputPath / "assets-partial-info.plist").string() + << "\"" + ; + + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { + log(compileAssetsCommand.str()); + } + + auto r = exec(compileAssetsCommand.str().c_str()); + + if (r.exitCode != 0) { + log("ERROR: failed to compile car file from xcode assets"); + log(r.output); + exit(1); + } + + // if (Env::get("DEBUG") != "1") fs::remove_all(assetsPath); + log("generated icons"); + }; + // // Darwin Package Prep // --- @@ -2937,21 +3017,24 @@ int main (const int argc, const char* argv[]) { flags += " -framework Network"; flags += " -framework UserNotifications"; flags += " -framework WebKit"; + flags += " -framework Metal"; + flags += " -framework Accelerate"; flags += " -framework Carbon"; flags += " -framework Cocoa"; flags += " -framework OSLog"; flags += " -DMACOS=1"; if (flagCodeSign) { - flags += " -DWAS_CODESIGNED=1"; + flags += " -DSOCKET_RUNTIME_PLATFORM_SANDBOXED=1"; } else { - flags += " -DWAS_CODESIGNED=0"; + flags += " -DSOCKET_RUNTIME_PLATFORM_SANDBOXED=0"; } flags += " -I" + prefixFile(); flags += " -I" + prefixFile("include"); flags += " -L" + prefixFile("lib/" + platform.arch + "-desktop"); flags += " -lsocket-runtime"; flags += " -luv"; - flags += " -I" + Path(paths.platformSpecificOutputPath / "include").string(); + flags += " -lllama"; + flags += " -I\"" + Path(paths.platformSpecificOutputPath / "include").string() + "\""; files += prefixFile("objects/" + platform.arch + "-desktop/desktop/main.o"); files += prefixFile("src/init.cc"); flags += " " + getCxxFlags(); @@ -3074,6 +3157,9 @@ int main (const int argc, const char* argv[]) { writeFile(paths.pathResourcesRelativeToUserBuild / "Credits.html", credits); } + if (platform.mac && isForDesktop) { + compileIconAssets(); + } // used in multiple if blocks, need to declare here auto androidEnableStandardNdkBuild = settings["android_enable_standard_ndk_build"] == "true"; @@ -3090,23 +3176,22 @@ int main (const int argc, const char* argv[]) { auto jni = src / "main" / "jni"; auto res = src / "main" / "res"; auto pkg = src / "main" / "java" / bundle_path; + auto runtime = src / "main" / "java" / "socket" / "runtime"; if (settings["android_main_activity"].size() == 0) { - settings["android_main_activity"] = String(DEFAULT_ANDROID_ACTIVITY_NAME); + settings["android_main_activity"] = String(DEFAULT_ANDROID_MAIN_ACTIVITY_NAME); + } + + if (settings["android_application"].size() == 0) { + settings["android_application"] = String(DEFAULT_ANDROID_APPLICATION_NAME); } fs::create_directories(output); fs::create_directories(src); fs::create_directories(pkg); + fs::create_directories(runtime); fs::create_directories(jni); - fs::create_directories(jni / "android"); - fs::create_directories(jni / "app"); - fs::create_directories(jni / "core"); - fs::create_directories(jni / "include"); - fs::create_directories(jni / "ipc"); fs::create_directories(jni / "src"); - fs::create_directories(jni / "window"); - fs::create_directories(res); fs::create_directories(res / "layout"); fs::create_directories(res / "values"); @@ -3186,39 +3271,7 @@ int main (const int argc, const char* argv[]) { exit(1); } - // user entry fs::copy(trim(prefixFile("src/init.cc")), jni, fs::copy_options::overwrite_existing); - // android runtime - fs::copy(trim(prefixFile("src/android/bridge.cc")), jni / "android", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/android/runtime.cc")), jni / "android", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/android/string_wrap.cc")), jni / "android", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/android/window.cc")), jni / "android", fs::copy_options::overwrite_existing); - // core - fs::copy(trim(prefixFile("src/core/codec.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/config.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/core.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/debug.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/env.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/ini.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/io.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/json.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/platform.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/preload.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/string.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/types.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/version.hh")), jni / "core", fs::copy_options::overwrite_existing); - // ipc - fs::copy(trim(prefixFile("src/ipc/ipc.hh")), jni / "ipc", fs::copy_options::overwrite_existing); - // window - fs::copy(trim(prefixFile("src/window/options.hh")), jni / "window", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/window/window.hh")), jni / "window", fs::copy_options::overwrite_existing); - - // libuv - fs::copy( - trim(prefixFile("uv")), - jni / "uv", - fs::copy_options::overwrite_existing | fs::copy_options::recursive - ); fs::copy( trim(prefixFile("include")), @@ -3226,7 +3279,7 @@ int main (const int argc, const char* argv[]) { fs::copy_options::overwrite_existing | fs::copy_options::recursive ); - writeFile(jni / "user-config-bytes.hh", settings["ini_code"]); + writeFile(jni / "include" / "socket" / "_user-config-bytes.hh", settings["ini_code"]); auto aaptNoCompressOptionsNormalized = std::vector<String>(); auto aaptNoCompressDefaultOptions = split(R"OPTIONS("htm","html","txt","json","jsonld","js","jsx","mjs","ts","tsx","css","xml","wasm")OPTIONS", ','); @@ -3311,41 +3364,43 @@ int main (const int argc, const char* argv[]) { Map manifestContext; - manifestContext["android_manifest_xml_permissions"] = ""; + manifestContext["android_manifest_xml_permissions"] = "\n"; if (settings["permissions_allow_notifications"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n"; } if (settings["permissions_allow_geolocation"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n"; - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n"; if (settings["permissions_allow_geolocation_in_background"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\" />\n"; } } if (settings["permissions_allow_user_media"] != "false") { if (settings["permissions_allow_camera"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.CAMERA\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.CAMERA\" />\n"; } if (settings["permissions_allow_microphone"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.CAPTURE_AUDIO_OUTPUT\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.CAPTURE_AUDIO_OUTPUT\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n"; } } if (settings["permissions_allow_read_media"] != "false") { if (settings["permissions_allow_read_media_images"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" />\n"; } if (settings["permissions_allow_read_media_video"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" />\n"; } if (settings["permissions_allow_read_media_audio"] != "false") { - manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.READ_MEDIA_AUDIO\" />\n"; + manifestContext["android_manifest_xml_permissions"] += " <uses-permission android:name=\"android.permission.READ_MEDIA_AUDIO\" />\n"; } } @@ -3358,7 +3413,7 @@ int main (const int argc, const char* argv[]) { std::transform(permission.begin(), permission.end(), permission.begin(), ::toupper); xml - << "<uses-permission android:name=" + << " <uses-permission android:name=" << "\"android.permission." << permission << "\"" << " />"; @@ -3420,8 +3475,8 @@ int main (const int argc, const char* argv[]) { if (androidIcon.size() > 0) { settings["android_application_icon_config"] = ( - String(" android:roundIcon=\"@mipmap/ic_launcher_round\"\n") + - String(" android:icon=\"@mipmap/ic_launcher\"\n") + String(" android:roundIcon=\"@mipmap/ic_launcher_round\"\n") + + String(" android:icon=\"@mipmap/ic_launcher\"\n") ); fs::copy(targetPath / androidIcon, res / "mipmap" / "icon.png", fs::copy_options::overwrite_existing); @@ -3444,9 +3499,9 @@ int main (const int argc, const char* argv[]) { writeFile(output / "build.gradle", trim(tmpl(gGradleBuild, settings))); writeFile(output / "gradle.properties", trim(tmpl(gGradleProperties, settings))); - writeFile(res / "layout" / "web_view_activity.xml", trim(tmpl(gAndroidLayoutWebviewActivity, settings))); + writeFile(res / "layout" / "web_view.xml", trim(tmpl(gAndroidLayoutWebView, settings))); + writeFile(res / "layout" / "window_container_view.xml", trim(tmpl(gAndroidLayoutWindowContainerView, settings))); writeFile(res / "values" / "strings.xml", trim(tmpl(gAndroidValuesStrings, settings))); - writeFile(src / "main" / "assets" / "__ssc_vital_check_ok_file__.txt", "OK"); // allow user space to override all `res/` files if (fs::exists(androidResources)) { @@ -3461,9 +3516,8 @@ int main (const int argc, const char* argv[]) { pp << "-DDEBUG=" << (flagDebugMode ? 1 : 0) << " " << "-DANDROID=1" << " " - << "-DSSC_SETTINGS=\"" << encodeURIComponent(_settings) << "\" " - << "-DSSC_VERSION=" << SSC::VERSION_STRING << " " - << "-DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING << " "; + << "-DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING << " " + << "-DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING << " "; Map makefileContext; @@ -3620,7 +3674,7 @@ int main (const int argc, const char* argv[]) { std::unordered_set<String> cflags; std::unordered_set<String> cppflags; - compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION=1"; if (path.size() > 0) { fs::current_path(targetPath); @@ -3770,7 +3824,7 @@ int main (const int argc, const char* argv[]) { "-I$(LOCAL_PATH)/" ); - compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION=1"; compiler = CXX.size() > 0 ? CXX : "clang++"; compilerFlags += " -v"; compilerFlags += " -std=c++2a -v"; @@ -3803,7 +3857,7 @@ int main (const int argc, const char* argv[]) { << (" -I" + quote + trim(prefixFile("include/socket/webassembly")) + quote) << (" -I" + quote + trim(prefixFile("src")) + quote) << (" -L" + quote + trim(prefixFile("lib")) + quote) - << " -DSOCKET_RUNTIME_EXTENSION_WASM" + << " -DSOCKET_RUNTIME_EXTENSION_WASM=1" << " --target=wasm32" << " --no-standard-libraries" << " -Wl,--import-memory" @@ -3818,8 +3872,8 @@ int main (const int argc, const char* argv[]) { << " -DDEBUG=" << (flagDebugMode ? 1 : 0) << " -DHOST=" << "\\\"" << devHost << "\\\"" << " -DPORT=" << devPort - << " -DSSC_VERSION=" << SSC::VERSION_STRING - << " -DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING + << " -DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING + << " -DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING << " " << trim(compilerFlags + " " + (flagDebugMode ? compilerDebugFlags : "")) << " " << sources.str() << " -o " << lib.string() @@ -3844,7 +3898,7 @@ int main (const int argc, const char* argv[]) { continue; } - make << "## socket/extensions/" << extension << RUNTIME_EXTENSION_FILE_EXT << std::endl; + make << "## socket/extensions/" << extension << SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME << std::endl; make << "include $(CLEAR_VARS)" << std::endl; make << "LOCAL_MODULE := extension-" << extension << std::endl; make << std::endl; @@ -3865,8 +3919,8 @@ int main (const int argc, const char* argv[]) { make << "LOCAL_CFLAGS += \\" << std::endl; make << " -DDEBUG=" << (flagDebugMode ? 1 : 0) << " \\" << std::endl; make << " -DANDROID=1" << " \\" << std::endl; - make << " -DSSC_VERSION=" << SSC::VERSION_STRING << " \\" << std::endl; - make << " -DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING << std::endl; + make << " -DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING << " \\" << std::endl; + make << " -DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING << std::endl; make << std::endl; if (compilerFlags.size() > 0) { @@ -3949,71 +4003,48 @@ int main (const int argc, const char* argv[]) { trim(tmpl(tmpl(gAndroidMakefile, makefileContext), settings)) ); - // Android Source - writeFile( - jni / "android" / "internal.hh", - std::regex_replace( - convertWStringToString(readFile(trim(prefixFile("src/android/internal.hh")))), - std::regex("__BUNDLE_IDENTIFIER__"), - bundle_path_underscored - ) - ); - - writeFile( - pkg / "bridge.kt", - std::regex_replace( - convertWStringToString(readFile(trim(prefixFile("src/android/bridge.kt")))), - std::regex("__BUNDLE_IDENTIFIER__"), - settings["android_bundle_identifier"] - ) - ); - - writeFile( - pkg / "main.kt", - std::regex_replace( - convertWStringToString(readFile(trim(prefixFile("src/android/main.kt")))), - std::regex("__BUNDLE_IDENTIFIER__"), - settings["android_bundle_identifier"] - ) - ); - - writeFile( - pkg / "runtime.kt", - std::regex_replace( - convertWStringToString(readFile(trim(prefixFile("src/android/runtime.kt")))), - std::regex("__BUNDLE_IDENTIFIER__"), - settings["android_bundle_identifier"] - ) - ); - - writeFile( - pkg / "window.kt", - std::regex_replace( - convertWStringToString(readFile(trim(prefixFile("src/android/window.kt")))), - std::regex("__BUNDLE_IDENTIFIER__"), - settings["android_bundle_identifier"] - ) - ); + // Android Sources + std::map<Path, String> sources = { + // user files + {pkg / "app.kt", "src/android/app.kt"}, + {pkg / "main.kt", "src/android/main.kt"}, + + // runtime package files + {runtime / "app" / "app.kt", "src/app/app.kt"}, + {runtime / "core" / "console.kt", "src/core/console.kt"}, + {runtime / "core" / "webview.kt", "src/core/webview.kt"}, + {runtime / "ipc" / "bridge.kt", "src/ipc/bridge.kt"}, + {runtime / "ipc" / "message.kt", "src/ipc/message.kt"}, + {runtime / "ipc" / "navigator.kt", "src/ipc/navigator.kt"}, + {runtime / "ipc" / "scheme_handlers.kt", "src/ipc/scheme_handlers.kt"}, + {runtime / "window" / "dialog.kt", "src/window/dialog.kt"}, + {runtime / "window" / "manager.kt", "src/window/manager.kt"}, + {runtime / "window" / "window.kt", "src/window/window.kt"}, + }; - writeFile( - pkg / "webview.kt", - std::regex_replace( - convertWStringToString(readFile(trim(prefixFile("src/android/webview.kt")))), - std::regex("__BUNDLE_IDENTIFIER__"), - settings["android_bundle_identifier"] - ) - ); + for (const auto& entry : sources) { + const auto filename = trim(prefixFile(entry.second)); + const auto value = convertWStringToString(readFile(filename)); + fs::create_directories(entry.first.parent_path()); + writeFile( + entry.first, + replace( + value, + "__BUNDLE_IDENTIFIER__", + settings["android_bundle_identifier"] + ) + ); + } // custom source files for (auto const &file : parseStringList(settings["android_sources"])) { // log(String("Android source: " + String(target / file)).c_str()); writeFile( pkg / Path(file).filename(), - tmpl(std::regex_replace( + tmpl( convertWStringToString(readFile(targetPath / file )), - std::regex("__BUNDLE_IDENTIFIER__"), - settings["android_bundle_identifier"] - ), settings) + settings + ) ); } } @@ -4028,11 +4059,13 @@ int main (const int argc, const char* argv[]) { auto schemeName = (settings["build_name"] + ".xcscheme"); auto pathToProject = paths.platformSpecificOutputPath / projectName; auto pathToScheme = pathToProject / "xcshareddata" / "xcschemes"; - auto pathToProfile = targetPath / settings["ios_provisioning_profile"]; + auto pathToProfile = fs::absolute(targetPath / settings["ios_provisioning_profile"]); fs::create_directories(pathToProject); fs::create_directories(pathToScheme); + compileIconAssets(); + if (!flagBuildForSimulator) { if (!fs::exists(pathToProfile)) { log("provisioning profile not found: " + pathToProfile.string() + ". " + @@ -4099,8 +4132,16 @@ int main (const int argc, const char* argv[]) { settings["apple_team_identifier"] = ""; } + if (settings["ios_sdkroot"].size() == 0) { + if (flagBuildForSimulator) { + settings["ios_sdkroot"] = "iphonesimulator"; + } else { + settings["ios_sdkroot"] = "iphoneos"; + } + } + // --platform=ios should always build for arm64 even on Darwin x86_64 - auto arch = flagBuildForSimulator ? platform.arch : "arm64"; + auto arch = String(flagBuildForSimulator ? "x86_64" : "arm64"); auto deviceType = arch + "-iPhone" + (flagBuildForSimulator ? "Simulator" : "OS"); auto deviceLibs = Path(prefixFile()) / "lib" / deviceType; @@ -4137,16 +4178,15 @@ int main (const int argc, const char* argv[]) { ); writeFile( - paths.platformSpecificOutputPath / "include" / "user-config-bytes.hh", + paths.platformSpecificOutputPath / "include" / "socket" / "_user-config-bytes.hh", settings["ini_code"] ); Map xCodeProjectVariables = settings; extendMap(xCodeProjectVariables, Map { - {"SSC_SETTINGS", _settings}, - {"SSC_VERSION", VERSION_STRING}, - {"SSC_VERSION_HASH", VERSION_HASH_STRING}, - {"WAS_CODESIGNED", flagCodeSign ? "1" : "0"}, + {"SOCKET_RUNTIME_VERSION", VERSION_STRING}, + {"SOCKET_RUNTIME_VERSION_HASH", VERSION_HASH_STRING}, + {"SOCKET_RUNTIME_PLATFORM_SANDBOXED", flagCodeSign ? "1" : "0"}, {"__ios_native_extensions_build_ids", ""}, {"__ios_native_extensions_build_refs", ""}, {"__ios_native_extensions_build_context_refs", ""}, @@ -4310,9 +4350,12 @@ int main (const int argc, const char* argv[]) { settings["build_extensions_" + extension + "_ios_compiler_flags"] + " -framework UniformTypeIdentifiers" + " -framework CoreBluetooth" + + " -framework QuartzCore" + " -framework CoreLocation" + " -framework Network" + " -framework UserNotifications" + + " -framework Metal" + + " -framework Accelerate" + " -framework WebKit" + " -framework Cocoa" + " -framework OSLog" @@ -4325,7 +4368,7 @@ int main (const int argc, const char* argv[]) { settings["build_extensions_" + extension + "_ios_compiler_debug_flags"] + " " ); - compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION=1"; // --platform=ios should always build for arm64 even on Darwin x86_64 if (!flagBuildForSimulator) { @@ -4355,10 +4398,10 @@ int main (const int argc, const char* argv[]) { } auto objectFile = source; - objectFile = replace(objectFile, "\\.mm", ".o"); - objectFile = replace(objectFile, "\\.m", ".o"); - objectFile = replace(objectFile, "\\.cc", ".o"); - objectFile = replace(objectFile, "\\.c", ".o"); + objectFile = replace(objectFile, "\\.mm$", ".o"); + objectFile = replace(objectFile, "\\.m$", ".o"); + objectFile = replace(objectFile, "\\.cc$", ".o"); + objectFile = replace(objectFile, "\\.c$", ".o"); auto filename = Path(objectFile).filename(); auto object = ( @@ -4376,7 +4419,7 @@ int main (const int argc, const char* argv[]) { compileExtensionObjectCommand << "xcrun -sdk " << (flagBuildForSimulator ? "iphonesimulator" : "iphoneos") << " " << compiler - << " -I" << Path(paths.platformSpecificOutputPath / "include").string() + << " -I\"" << Path(paths.platformSpecificOutputPath / "include").string() << "\"" << " -I" << prefixFile() << " -I" << prefixFile("include") << " -DIOS=1" @@ -4384,8 +4427,8 @@ int main (const int argc, const char* argv[]) { << " -DDEBUG=" << (flagDebugMode ? 1 : 0) << " -DHOST=" << "\\\"" << devHost << "\\\"" << " -DPORT=" << devPort - << " -DSSC_VERSION=" << SSC::VERSION_STRING - << " -DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING + << " -DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING + << " -DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING << " -target " << (flagBuildForSimulator ? platform.arch + "-apple-ios-simulator": "arm64-apple-ios") << " -fembed-bitcode" << " -fPIC" @@ -4426,7 +4469,7 @@ int main (const int argc, const char* argv[]) { settings["build_extensions_" + extension + "_ios_compiler_debug_flags"] + " " ); - compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION=1"; compiler = CXX.size() > 0 ? CXX : "clang++"; compilerFlags += " -v"; compilerFlags += " -std=c++2a -v"; @@ -4454,12 +4497,12 @@ int main (const int argc, const char* argv[]) { fs::create_directories(lib.parent_path()); compileExtensionWASMCommand << compiler - << " -I" + Path(paths.platformSpecificOutputPath / "include").string() + << " -I\"" + Path(paths.platformSpecificOutputPath / "include").string() << "\"" << (" -I" + quote + trim(prefixFile("include")) + quote) << (" -I" + quote + trim(prefixFile("include/socket/webassembly")) + quote) << (" -I" + quote + trim(prefixFile("src")) + quote) << (" -L" + quote + trim(prefixFile("lib")) + quote) - << " -DSOCKET_RUNTIME_EXTENSION_WASM" + << " -DSOCKET_RUNTIME_EXTENSION_WASM=1" << " --target=wasm32" << " --no-standard-libraries" << " -Wl,--import-memory" @@ -4474,8 +4517,8 @@ int main (const int argc, const char* argv[]) { << " -DDEBUG=" << (flagDebugMode ? 1 : 0) << " -DHOST=" << "\\\"" << devHost << "\\\"" << " -DPORT=" << devPort - << " -DSSC_VERSION=" << SSC::VERSION_STRING - << " -DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING + << " -DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING + << " -DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING << " " << trim(compilerFlags + " " + (flagDebugMode ? compilerDebugFlags : "")) << " " << join(sources, " ") << " -o " << lib.string() @@ -4500,7 +4543,6 @@ int main (const int argc, const char* argv[]) { continue; } - auto linkerFlags = ( settings["build_extensions_linker_flags"] + " " + settings["build_extensions_ios_linker_flags"] + " " + @@ -4520,7 +4562,7 @@ int main (const int argc, const char* argv[]) { linkerFlags += " -arch arm64 "; } - auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + RUNTIME_EXTENSION_FILE_EXT)); + auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME)); fs::create_directories(lib.parent_path()); auto compileExtensionLibraryCommand = StringStream(); compileExtensionLibraryCommand @@ -4535,6 +4577,7 @@ int main (const int argc, const char* argv[]) { << " -L" + libdir << " -lsocket-runtime" << " -luv" + << " -lllama" << " -isysroot " << iosSdkPath << "/" << " -iframeworkwithsysroot /System/Library/Frameworks/" << " -F " << iosSdkPath << "/System/Library/Frameworks/" @@ -4544,6 +4587,8 @@ int main (const int argc, const char* argv[]) { << " -framework Foundation" << " -framework Network" << " -framework UserNotifications" + << " -framework Metal" + << " -framework Accelerate" << " -framework WebKit" << " -framework UIKit" << " -fembed-bitcode" @@ -4709,12 +4754,14 @@ int main (const int argc, const char* argv[]) { flags += " -I" + Path(paths.platformSpecificOutputPath / "include").string(); flags += " -I" + prefixFile(); flags += " -I" + prefixFile("include"); + flags += " -I" + prefixFile("include"); flags += " -L" + prefixFile("lib/" + platform.arch + "-desktop"); files += prefixFile("objects/" + platform.arch + "-desktop/desktop/main.o"); files += prefixFile("src/init.cc"); files += prefixFile("lib/" + platform.arch + "-desktop/libsocket-runtime.a"); files += prefixFile("lib/" + platform.arch + "-desktop/libuv.a"); + files += prefixFile("lib/" + platform.arch + "-desktop/libllama.a"); pathResources = paths.pathBin; @@ -4741,6 +4788,34 @@ int main (const int argc, const char* argv[]) { "apps" }; + { + auto desktopExtensionsPath = pathResources / "lib" / "extensions"; + auto CXX = Env::get("CXX", "clang++"); + + fs::create_directories(desktopExtensionsPath); + + StringStream command; + command + << CXX + << " -shared" + << " " << flags + << " " << prefixFile("lib/" + platform.arch + "-desktop/libsocket-runtime.a") + << " " << prefixFile("lib/" + platform.arch + "-desktop/libuv.a") + << " " << prefixFile("objects/" + platform.arch + "-desktop/extensions/linux.o") + << " -o " << (desktopExtensionsPath / "libsocket-runtime-desktop-extension.so").string() + ; + + log(command.str()); + auto result = exec(command.str().c_str()); + if (result.exitCode != 0) { + log("ERROR: failed to compile desktop runtime extension"); + if (flagVerboseMode) { + log(result.output); + } + exit(1); + } + } + fs::create_directories(pathIcons); fs::create_directories(pathResources); fs::create_directories(pathManifestFile); @@ -4822,7 +4897,7 @@ int main (const int argc, const char* argv[]) { } if (debugBuild) { - flags += " -D_DEBUG"; + flags += " -D_DEBUG -g"; } auto d = String(debugBuild ? "d" : "" ); @@ -4908,7 +4983,7 @@ int main (const int argc, const char* argv[]) { } } - handleBuildPhaseForUserScript( + handleBuildPhaseForUser( settings, targetPlatform, pathResourcesRelativeToUserBuild, @@ -4945,7 +5020,9 @@ int main (const int argc, const char* argv[]) { if (flagBuildForIOS) { if (flagBuildForSimulator) { - checkIosSimulatorDeviceAvailability(settings["ios_simulator_device"]); + if (settings.count("ios_simulator_uuid") < 0) { + checkIosSimulatorDeviceAvailability(settings["ios_simulator_device"]); + } log("building for iOS Simulator"); } else { log("building for iOS"); @@ -4960,11 +5037,11 @@ int main (const int argc, const char* argv[]) { // // Copy and or create the source files we need for the build. // - fs::copy(trim(prefixFile("src/init.cc")), pathToDist); - fs::copy(trim(prefixFile("src/core/config.hh")), pathToDist / "core"); - fs::copy(trim(prefixFile("src/core/string.hh")), pathToDist / "core"); - fs::copy(trim(prefixFile("src/core/types.hh")), pathToDist / "core"); - fs::copy(trim(prefixFile("src/core/ini.hh")), pathToDist / "core"); + fs::copy( + trim(prefixFile("src/init.cc")), + pathToDist, + fs::copy_options::overwrite_existing + ); auto pathBase = pathToDist / "Base.lproj"; fs::create_directories(pathBase); @@ -4976,9 +5053,6 @@ int main (const int argc, const char* argv[]) { entitlementSettings["configured_entitlements"] = ""; - if (flagDebugMode) { - } - if (settings["permissions_allow_push_notifications"] == "true") { entitlementSettings["configured_entitlements"] += ( " <key>com.apple.developer.usernotifications.filtering</key>\n" @@ -5026,9 +5100,14 @@ int main (const int argc, const char* argv[]) { // building, signing, bundling, archiving, noterizing, and uploading. // StringStream archiveCommand; + String deviceIdentity = settings.count("ios_simulator_uuid") > 0 + ? "id=" + settings["ios_simulator_uuid"] + : "name=" + settings["ios_simulator_device"]; + String destination = flagBuildForSimulator - ? "platform=iOS Simulator,OS=latest,name=" + settings["ios_simulator_device"] + ? "platform=iOS Simulator,OS=latest," + deviceIdentity + ",arch=x86_64" : "generic/platform=iOS"; + String deviceType; // TODO: should be "iPhone Distribution: <name/provisioning specifier>"? @@ -5036,20 +5115,18 @@ int main (const int argc, const char* argv[]) { settings["ios_codesign_identity"] = "iPhone Distribution"; } - auto sup = String("archive"); - auto configuration = String(flagDebugMode ? "Debug" : "Release"); - - if (!flagShouldPackage) { - sup = "CONFIGURATION_BUILD_DIR=" + pathToDist.string(); - } + const auto configuration = String(flagDebugMode ? "Debug" : "Release"); + const auto args = flagShouldPackage + ? String("archive") + : "CONFIGURATION_BUILD_DIR=" + pathToDist.string(); archiveCommand << "xcodebuild" - << " build " << sup + << " build " << args << " -allowProvisioningUpdates" - << " -configuration " << configuration << " -scheme " << settings["build_name"] - << " -destination '" << destination << "'"; + << " -destination '" << destination << "'" + << " -configuration " << configuration; if (flagShouldPackage) { archiveCommand << " -archivePath build/" << settings["build_name"]; @@ -5152,7 +5229,7 @@ int main (const int argc, const char* argv[]) { StringStream sdkmanager; StringStream packages; StringStream gradlew; - String ndkVersion = "26.0.10792818"; + String ndkVersion = "26.1.10909125"; String androidPlatform = "android-34"; if (platform.unix) { @@ -5174,7 +5251,7 @@ int main (const int argc, const char* argv[]) { sdkmanager << packages.str(); - if (Env::get("SSC_SKIP_ANDROID_SDK_MANAGER").size() == 0) { + if (Env::get("SOCKET_RUNTIME_SKIP_ANDROID_SDK_MANAGER").size() == 0) { if (debugEnv || verboseEnv) { log(sdkmanager.str()); } @@ -5301,7 +5378,7 @@ int main (const int argc, const char* argv[]) { } // just build for CI - if (Env::get("SSC_CI").size() > 0) { + if (Env::get("SOCKET_RUNTIME_CI").size() > 0) { StringStream gradlew; gradlew << localDirPrefix << "gradlew build"; @@ -5535,7 +5612,7 @@ int main (const int argc, const char* argv[]) { ); auto objects = StringStream(); - auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + RUNTIME_EXTENSION_FILE_EXT)); + auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME)); fs::create_directories(lib.parent_path()); @@ -5619,7 +5696,7 @@ int main (const int argc, const char* argv[]) { settings["build_extensions_" + extension + "_" + os + "_compiler_debug_flags"] + " " ); - compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION=1"; if (platform.mac) { compilerFlags += " -framework UniformTypeIdentifiers"; @@ -5627,6 +5704,8 @@ int main (const int argc, const char* argv[]) { compilerFlags += " -framework CoreLocation"; compilerFlags += " -framework Network"; compilerFlags += " -framework UserNotifications"; + compilerFlags += " -framework Metal"; + compilerFlags += " -framework Accelerate"; compilerFlags += " -framework WebKit"; compilerFlags += " -framework Cocoa"; compilerFlags += " -framework OSLog"; @@ -5704,10 +5783,10 @@ int main (const int argc, const char* argv[]) { } auto objectFile = source; - objectFile = replace(objectFile, "\\.mm", ".o"); - objectFile = replace(objectFile, "\\.m", ".o"); - objectFile = replace(objectFile, "\\.cc", ".o"); - objectFile = replace(objectFile, "\\.c", ".o"); + objectFile = replace(objectFile, "\\.mm$", ".o"); + objectFile = replace(objectFile, "\\.m$", ".o"); + objectFile = replace(objectFile, "\\.cc$", ".o"); + objectFile = replace(objectFile, "\\.c$", ".o"); auto object = Path(objectFile); @@ -5740,8 +5819,8 @@ int main (const int argc, const char* argv[]) { << " -DDEBUG=" << (flagDebugMode ? 1 : 0) << " -DHOST=" << "\\\"" << devHost << "\\\"" << " -DPORT=" << devPort - << " -DSSC_VERSION=" << SSC::VERSION_STRING - << " -DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING + << " -DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING + << " -DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING #if !defined(_WIN32) << " -fPIC" #endif @@ -5804,7 +5883,7 @@ int main (const int argc, const char* argv[]) { settings["build_extensions_" + extension + "_" + os + "_compiler_debug_flags"] + " " ); - compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION=1"; compiler = CXX.size() > 0 && !IN_GITHUB_ACTIONS_CI ? CXX : "clang++"; compilerFlags += " -v"; compilerFlags += " -std=c++2a -v"; @@ -5840,7 +5919,7 @@ int main (const int argc, const char* argv[]) { << (" -I" + quote + trim(prefixFile("include/socket/webassembly")) + quote) << (" -I" + quote + trim(prefixFile("src")) + quote) << (" -L" + quote + trim(prefixFile("lib")) + quote) - << " -DSOCKET_RUNTIME_EXTENSION_WASM" + << " -DSOCKET_RUNTIME_EXTENSION_WASM=1" << " --target=wasm32" << " --no-standard-libraries" << " -Wl,--import-memory" @@ -5865,8 +5944,8 @@ int main (const int argc, const char* argv[]) { << " -DDEBUG=" << (flagDebugMode ? 1 : 0) << " -DHOST=" << "\\\"" << devHost << "\\\"" << " -DPORT=" << devPort - << " -DSSC_VERSION=" << SSC::VERSION_STRING - << " -DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING + << " -DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING + << " -DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING << " " << trim(compilerFlags + " " + (flagDebugMode ? compilerDebugFlags : "")) << " " << join(sources, " ") << " -o " << (quote + lib.string() + quote) @@ -5927,10 +6006,12 @@ int main (const int argc, const char* argv[]) { #if defined(_WIN32) auto d = String(debugBuild ? "d" : ""); auto static_uv = prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libuv.lib"); + auto static_llama = prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\llama.lib"); auto static_runtime = trim(prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libsocket-runtime" + d + ".a")); #else auto d = ""; auto static_uv = ""; + auto static_llama = ""; auto static_runtime = ""; #endif @@ -5941,6 +6022,7 @@ int main (const int argc, const char* argv[]) { << Env::get("CXX") << quote // win32 - quote the binary path << " " << static_runtime + << " " << static_llama << " " << static_uv << " " << objects.str() #if defined(_WIN32) @@ -5956,6 +6038,7 @@ int main (const int argc, const char* argv[]) { << " " << extraFlags #if defined(__linux__) << " -luv" + << " -lllama" << " -lsocket-runtime" #endif << (" -L" + quote + trim(prefixFile("lib/" + platform.arch + "-desktop")) + quote) @@ -5977,7 +6060,13 @@ int main (const int argc, const char* argv[]) { if (platform.mac) { if (isForDesktop) { - settings["mac_codesign_paths"] += (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + RUNTIME_EXTENSION_FILE_EXT)).string() + ";"; + settings["mac_codesign_paths"] += ( + paths.pathResourcesRelativeToUserBuild / + "socket" / + "extensions" / + extension / + (extension + SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME) + ).string() + ";"; } } @@ -6019,15 +6108,14 @@ int main (const int argc, const char* argv[]) { << " " << files << " " << flags << " " << extraFlags - << " -o " << binaryPath.string() + << " -o \"" << binaryPath.string() << "\"" << " -DIOS=" << (flagBuildForIOS ? 1 : 0) << " -DANDROID=" << (flagBuildForAndroid ? 1 : 0) << " -DDEBUG=" << (flagDebugMode ? 1 : 0) << " -DHOST=" << "\\\"" << devHost << "\\\"" << " -DPORT=" << devPort - << " -DSSC_SETTINGS=\"" << encodeURIComponent(_settings) << "\"" - << " -DSSC_VERSION=" << SSC::VERSION_STRING - << " -DSSC_VERSION_HASH=" << SSC::VERSION_HASH_STRING + << " -DSOCKET_RUNTIME_VERSION=" << SSC::VERSION_STRING + << " -DSOCKET_RUNTIME_VERSION_HASH=" << SSC::VERSION_HASH_STRING << quote // win32 - quote the entire command ; @@ -6273,7 +6361,7 @@ int main (const int argc, const char* argv[]) { } if (key.starts_with("$HOST_HOME") || key.starts_with("~")) { - const auto path = replace(replace(key, "$HOST_HOME", ""), "~", ""); + const auto path = replace(replace(key, "^(\\$HOST_HOME)", ""), "^(~)", ""); entitlementSettings["configured_entitlements"] += ( " <string>" + (path.ends_with("/") ? path : path + "/") + "</string>\n" ); @@ -6941,7 +7029,7 @@ int main (const int argc, const char* argv[]) { INI::parse(readFile(targetPath / "socket.ini")) ); - handleBuildPhaseForUserScript( + handleBuildPhaseForUser( settingsForSourcesWatcher, targetPlatform, pathResourcesRelativeToUserBuild, @@ -7042,12 +7130,12 @@ int main (const int argc, const char* argv[]) { settings.insert(std::make_pair("port", devPort)); const bool debugEnv = ( - Env::get("SSC_DEBUG").size() > 0 || + Env::get("SOCKET_RUNTIME_DEBUG").size() > 0 || Env::get("DEBUG").size() > 0 ); const bool verboseEnv = ( - Env::get("SSC_VERBOSE").size() > 0 || + Env::get("SOCKET_RUNTIME_VERBOSE").size() > 0 || Env::get("VERBOSE").size() > 0 ); @@ -7073,7 +7161,7 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value - Options setupOptions = { + CommandLineOptions setupOptions = { { { "--platform" }, true, true }, { { "--yes", "-y" }, true, false }, { { "--quiet", "-q" }, true, false }, diff --git a/src/cli/cli.hh b/src/cli/cli.hh index 85411d330e..c298c74fad 100644 --- a/src/cli/cli.hh +++ b/src/cli/cli.hh @@ -1,12 +1,15 @@ -#ifndef SSC_CLI_HH -#define SSC_CLI_HH +#ifndef SOCKET_RUNTIME_CLI_H +#define SOCKET_RUNTIME_CLI_H -#include "../core/platform.hh" -#include "../core/string.hh" +#include "../platform/platform.hh" #include "../core/env.hh" #include <signal.h> +#ifndef SOCKET_CLI +#define SOCKET_CLI 1 +#endif + namespace SSC::CLI { inline void notify (int signal) { #if !defined(_WIN32) diff --git a/src/cli/templates.hh b/src/cli/templates.hh index a4ed99fd25..a73e160a98 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -193,15 +193,18 @@ constexpr auto gHelloWorld = R"HTML( <!doctype html> <html> <head> + <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta http-equiv="Content-Security-Policy" content=" - connect-src https: file: ipc: socket: ws://localhost:*; - script-src https: socket: http://localhost:* 'unsafe-eval'; - img-src https: data: file: http://localhost:*; - child-src 'none'; - object-src 'none'; + connect-src socket: https: http: blob: ipc: npm: node: wss: ws: ws://localhost:*; + script-src socket: https: http: blob: npm: node: http://localhost:* 'unsafe-eval' 'unsafe-inline'; + worker-src socket: https: http: blob: 'unsafe-eval' 'unsafe-inline'; + frame-src socket: https: http: blob: http://localhost:*; + img-src socket: https: http: blob: http://localhost:*; + child-src socket: https: http: blob:; + object-src 'none'; " > <style type="text/css"> @@ -213,8 +216,10 @@ constexpr auto gHelloWorld = R"HTML( justify-content: center; align-content: center; font-family: helvetica; + overflow: hidden; } </style> + <script src="index.js" type="module"></script> </head> <body> <h1>Hello, World.</h1> @@ -222,6 +227,13 @@ constexpr auto gHelloWorld = R"HTML( </html> )HTML"; +constexpr auto gHelloWorldScript = R"JavaScript(// +// Your JavaScript goes here! +// +import process from 'socket:process' +console.log(`Hello, ${process.platform}!`) +)JavaScript"; + // // macOS 'Info.plist' file // @@ -236,8 +248,11 @@ constexpr auto gMacOSInfoPList = R"XML(<?xml version="1.0" encoding="UTF-8"?> <key>CFBundleName</key> <string>{{build_name}}</string> - <key>CFBundleIconFile</key> - <string>icon.icns</string> + <key>CFBundleIconFile</key> + <string>AppIcon</string> + + <key>CFBundleIconName</key> + <string>AppIcon</string> <key>CFBundlePackageType</key> <string>APPL</string> @@ -451,7 +466,8 @@ constexpr auto gCredits = R"HTML( </p> )HTML"; -constexpr auto DEFAULT_ANDROID_ACTIVITY_NAME = ".MainActivity"; +constexpr auto DEFAULT_ANDROID_APPLICATION_NAME = ".App"; +constexpr auto DEFAULT_ANDROID_MAIN_ACTIVITY_NAME = ".MainActivity"; // // Android Manifest @@ -469,22 +485,27 @@ constexpr auto gAndroidManifest = R"XML( <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - {{android_manifest_xml_permissions}} <application + android:name="{{android_application}}" android:allowBackup="true" android:label="{{meta_title}}" - android:theme="@style/Theme.AppCompat.Light" + android:theme="@style/Theme.AppCompat.DayNight" android:supportsRtl="true" {{android_application_icon_config}} {{android_allow_cleartext}} > + <meta-data + android:name="android.webkit.WebView.MetricsOptOut" + android:value="true" + /> <activity android:name="{{android_main_activity}}" android:exported="true" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" - android:launchMode="singleInstance" + android:launchMode="singleTop" + android:enableOnBackInvokedCallback="true" android:hardwareAccelerated="true" > <intent-filter> @@ -663,43 +684,51 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! 17A7F8F229358D220051D146 /* init.cc in Sources */ = {isa = PBXBuildFile; fileRef = 17A7F8EE29358D180051D146 /* init.cc */; }; 17A7F8F529358D430051D146 /* libsocket-runtime.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A7F8F329358D430051D146 /* libsocket-runtime.a */; }; 17A7F8F629358D430051D146 /* libuv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A7F8F429358D430051D146 /* libuv.a */; }; + 17A7F8F629358D4A0051D146 /* libllama.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A7F8F429358D430051D147 /* libllama.a */; }; 17A7F8F729358D4D0051D146 /* main.o in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A7F8F129358D180051D146 /* main.o */; }; 17C230BA28E9398700301440 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17C230B928E9398700301440 /* Foundation.framework */; }; 290F7EBF2768C49000486988 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 294A3C792763E9C6007B5B9A /* UIKit.framework */; }; 290F7F87276BC2B000486988 /* lib in Resources */ = {isa = PBXBuildFile; fileRef = 290F7F86276BC2B000486988 /* lib */; }; 29124C5D2761336B001832A0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29124C5B2761336B001832A0 /* LaunchScreen.storyboard */; }; 294A3C852764EAB7007B5B9A /* ui in Resources */ = {isa = PBXBuildFile; fileRef = 294A3C842764EAB7007B5B9A /* ui */; }; + 034B592125768A7B005D0134 /* default.metallib in Resources */ = {isa = PBXBuildFile; fileRef = 034B592025768A7B005D0134 /* default.metallib */; }; 294A3CA02768C429007B5B9A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 294A3C7B2763EA7F007B5B9A /* WebKit.framework */; }; + 2996EDB22770BC1F00C672A0 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2996EDB12770BC1F00C672B1 /* Accelerate.framework */; }; + 2996EDB22770BC1F00C672A1 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2996EDB12770BC1F00C672A1 /* Metal.framework */; }; 2996EDB22770BC1F00C672A2 /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2996EDB12770BC1F00C672A2 /* Network.framework */; }; 2996EDB22770BC1F00C672A3 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2996EDB12770BC1F00C672A3 /* CoreBluetooth.framework */; }; 2996EDB22770BC1F00C672A4 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2996EDB12770BC1F00C672A4 /* UserNotifications.framework */; }; + 2996EDB22770BC1F00C672B0 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2996EDB12770BC1F00C672B0 /* QuartzCore.framework */; }; + 2996EDB22770BC1F00C672A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29124C5E2761336B001832A1 /* Assets.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ {{__ios_native_extensions_build_context_refs}} 171C1C2A2AC38A70005F587F /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; - 1790CE4D2AD78CCF00AA7E5B /* ini.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ini.hh; sourceTree = "<group>"; }; - 1790CE4E2AD78CCF00AA7E5B /* string.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = string.hh; sourceTree = "<group>"; }; - 1790CE4F2AD78CCF00AA7E5B /* config.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = config.hh; sourceTree = "<group>"; }; - 1790CE502AD78CCF00AA7E5B /* types.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = types.hh; sourceTree = "<group>"; }; 179989D12A867B260041EDC1 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; 17A7F8EE29358D180051D146 /* init.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = init.cc; sourceTree = "<group>"; }; 17A7F8F129358D180051D146 /* main.o */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.objfile"; path = main.o; sourceTree = "<group>"; }; 17A7F8F329358D430051D146 /* libsocket-runtime.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libsocket-runtime.a"; path = "lib/libsocket-runtime.a"; sourceTree = "<group>"; }; 17A7F8F429358D430051D146 /* libuv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libuv.a; path = lib/libuv.a; sourceTree = "<group>"; }; + 17A7F8F429358D430051D147 /* libllama.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libllama.a; path = lib/libllama.a; sourceTree = "<group>"; }; 17C230B928E9398700301440 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 17E73FEE28FCD3360087604F /* libuv-ios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libuv-ios.a"; path = "lib/libuv-ios.a"; sourceTree = "<group>"; }; 290F7F86276BC2B000486988 /* lib */ = {isa = PBXFileReference; lastKnownFileType = folder; path = lib; sourceTree = "<group>"; }; 29124C4A27613369001832A0 /* {{build_name}}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "{{build_name}}.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 29124C5C2761336B001832A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 29124C5E2761336B001832A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 034B592025768A7B005D0134 /* default.metallib */ = {isa = PBXFileReference; lastKnownFileType = "archive.metal-library"; path = "lib/default.metallib"; sourceTree = "<group>"; }; + 29124C5E2761336B001832A1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 294A3C792763E9C6007B5B9A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 294A3C7B2763EA7F007B5B9A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 294A3C842764EAB7007B5B9A /* ui */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ui; sourceTree = "<group>"; }; 294A3C9027677424007B5B9A /* socket.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = socket.entitlements; sourceTree = "<group>"; }; + 2996EDB12770BC1F00C672B1 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; + 2996EDB12770BC1F00C672A1 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; 2996EDB12770BC1F00C672A2 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; }; 2996EDB12770BC1F00C672A3 /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = System/Library/Frameworks/CoreBluetooth.framework; sourceTree = SDKROOT; }; 2996EDB12770BC1F00C672A4 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 2996EDB12770BC1F00C672B0 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartzcore.framework; path = System/Library/Frameworks/Quartzcore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -712,11 +741,15 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! 179989D22A867B260041EDC1 /* UniformTypeIdentifiers.framework in Frameworks */, 17A7F8F529358D430051D146 /* libsocket-runtime.a in Frameworks */, 17A7F8F629358D430051D146 /* libuv.a in Frameworks */, + 17A7F8F629358D4A0051D146 /* libllama.a in Frameworks */, 17A7F8F729358D4D0051D146 /* main.o in Frameworks */, 17C230BA28E9398700301440 /* Foundation.framework in Frameworks */, + 2996EDB22770BC1F00C672A0 /* Accelerate.framework in Frameworks */, + 2996EDB22770BC1F00C672A1 /* Metal.framework in Frameworks */, 2996EDB22770BC1F00C672A2 /* Network.framework in Frameworks */, 2996EDB22770BC1F00C672A3 /* CoreBluetooth.framework in Frameworks */, 2996EDB22770BC1F00C672A4 /* UserNotifications.framework in Frameworks */, + 2996EDB22770BC1F00C672B0 /* QuartzCore.framework in Frameworks */, 294A3CA02768C429007B5B9A /* WebKit.framework in Frameworks */, 290F7EBF2768C49000486988 /* UIKit.framework in Frameworks */, ); @@ -728,10 +761,6 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! 1790CE4C2AD78CCF00AA7E5B /* core */ = { isa = PBXGroup; children = ( - 1790CE4D2AD78CCF00AA7E5B /* ini.hh */, - 1790CE4E2AD78CCF00AA7E5B /* string.hh */, - 1790CE4F2AD78CCF00AA7E5B /* config.hh */, - 1790CE502AD78CCF00AA7E5B /* types.hh */, ); path = core; sourceTree = "<group>"; @@ -763,6 +792,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! 294A3C842764EAB7007B5B9A /* ui */, 29124C5B2761336B001832A0 /* LaunchScreen.storyboard */, 29124C5E2761336B001832A0 /* Info.plist */, + 29124C5E2761336B001832A1 /* Assets.xcassets */, 29124C4B27613369001832A0 /* Products */, 294A3C782763E9C6007B5B9A /* Frameworks */, ); @@ -784,11 +814,15 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! 179989D12A867B260041EDC1 /* UniformTypeIdentifiers.framework */, 17A7F8F329358D430051D146 /* libsocket-runtime.a */, 17A7F8F429358D430051D146 /* libuv.a */, + 17A7F8F429358D430051D147 /* libllama.a */, 17E73FEE28FCD3360087604F /* libuv-ios.a */, 17C230B928E9398700301440 /* Foundation.framework */, + 2996EDB12770BC1F00C672A1 /* Metal.framework */, + 2996EDB12770BC1F00C672B1 /* Accelerate.framework */, 2996EDB12770BC1F00C672A2 /* Network.framework */, 2996EDB12770BC1F00C672A3 /* CoreBluetooth.framework */, 2996EDB12770BC1F00C672A4 /* UserNotifications.framework */, + 2996EDB12770BC1F00C672B0 /* QuartzCore.framework */, 294A3C7B2763EA7F007B5B9A /* WebKit.framework */, 294A3C792763E9C6007B5B9A /* UIKit.framework */, ); @@ -854,6 +888,8 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! files = ( 29124C5D2761336B001832A0 /* LaunchScreen.storyboard in Resources */, 294A3C852764EAB7007B5B9A /* ui in Resources */, + 034B592125768A7B005D0134 /* default.metallib in Resources */, + 2996EDB22770BC1F00C672A5 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -926,9 +962,9 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", - "SSC_VERSION={{SSC_VERSION}}", - "SSC_VERSION_HASH={{SSC_VERSION_HASH}}", - "WAS_CODESIGNED={{WAS_CODESIGNED}}", + "SOCKET_RUNTIME_VERSION={{SOCKET_RUNTIME_VERSION}}", + "SOCKET_RUNTIME_VERSION_HASH={{SOCKET_RUNTIME_VERSION_HASH}}", + "SOCKET_RUNTIME_PLATFORM_SANDBOXED={{SOCKET_RUNTIME_PLATFORM_SANDBOXED}}", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -937,11 +973,11 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; + SDKROOT = {{ios_sdkroot}}; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; }; name = Debug; @@ -993,14 +1029,14 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GCC_PREPROCESSOR_DEFINITIONS = ( - "SSC_VERSION={{SSC_VERSION}}", - "SSC_VERSION_HASH={{SSC_VERSION_HASH}}", + "SOCKET_RUNTIME_VERSION={{SOCKET_RUNTIME_VERSION}}", + "SOCKET_RUNTIME_VERSION_HASH={{SOCKET_RUNTIME_VERSION_HASH}}", "$(inherited)", ); - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - SDKROOT = iphoneos; + SDKROOT = {{ios_sdkroot}}; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; VALIDATE_PRODUCT = YES; }; @@ -1021,12 +1057,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include"; INFOPLIST_FILE = Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "{{build_name}}"; - INFOPLIST_KEY_LSApplicationCategoryType = Developer; - INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to the camera"; INFOPLIST_KEY_NSHumanReadableCopyright = "{{meta_copyright}}"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app needs access to the microphone"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIStatusBarHidden = YES; @@ -1048,7 +1079,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "{{ios_provisioning_specifier}}"; SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; WARNING_CFLAGS = ( "$(inherited)", "-Wno-nullability-completeness", @@ -1071,12 +1102,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include"; INFOPLIST_FILE = Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "{{build_name}}"; - INFOPLIST_KEY_LSApplicationCategoryType = Developer; - INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to the camera"; INFOPLIST_KEY_NSHumanReadableCopyright = "{{meta_copyright}}"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app needs access to the microphone"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIStatusBarHidden = YES; @@ -1094,7 +1120,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "{{ios_provisioning_specifier}}"; SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; WARNING_CFLAGS = ( "$(inherited)", "-Wno-nullability-completeness", @@ -1169,8 +1195,26 @@ constexpr auto gIOSInfoPList = R"XML(<?xml version="1.0" encoding="UTF-8"?> <key>CFBundleIdentifier</key> <string>{{meta_bundle_identifier}}</string> - <key>CFBundleIconFile</key> - <string>ui/icon.png</string> + <key>CFBundleDisplayName</key> + <string>{{build_name}}</string> + + <key>CFBundleName</key> + <string>{{build_name}}</string> + + <key>CFBundleIconFile</key> + <string>AppIcon</string> + + <key>CFBundleIconName</key> + <string>AppIcon</string> + + <key>compileBitcode</key> + <{{meta_compile_bitcode}}/> + + <key>uploadBitcode</key> + <{{meta_upload_bitcode}}/> + + <key>uploadSymbols</key> + <{{meta_upload_symbols}}/> <key>CFBundleURLTypes</key> <array> @@ -1184,6 +1228,9 @@ constexpr auto gIOSInfoPList = R"XML(<?xml version="1.0" encoding="UTF-8"?> </dict> </array> + <key>LSApplicationCategoryType</key> + <string>{{ios_category}}</string> + <!-- Application configuration --> <key>LSApplicationQueriesSchemes</key> <array> @@ -1270,6 +1317,9 @@ constexpr auto gIOSInfoPList = R"XML(<?xml version="1.0" encoding="UTF-8"?> </dict> + <key>UIApplicationSupportsIndirectInputEvents</key> + <true/> + <!-- User given plist data --> {{ios_info_plist_data}} </dict> @@ -1289,7 +1339,7 @@ constexpr auto gXcodeEntitlements = R"XML(<?xml version="1.0" encoding="UTF-8"?> // constexpr auto gGradleBuild = R"GROOVY( buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '1.9.20' repositories { google() mavenCentral() @@ -1322,7 +1372,7 @@ apply plugin: 'kotlin-android' android { compileSdkVersion 34 - ndkVersion "26.0.10792818" + ndkVersion "26.1.10909125" flavorDimensions "default" namespace '{{android_bundle_identifier}}' @@ -1387,9 +1437,11 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'androidx.fragment:fragment-ktx:1.7.1' + implementation 'androidx.lifecycle:lifecycle-process:2.7.0' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.webkit:webkit:1.8.0' + implementation 'androidx.core:core-ktx:1.13.0' + implementation 'androidx.webkit:webkit:1.9.0' } )GROOVY"; @@ -1416,7 +1468,6 @@ android.suppressUnsupportedCompileSdk=34 android.experimental.legacyTransform.forceNonIncremental=true kotlin.code.style=official -android.experimental.legacyTransform.forceNonIncremental=true )GRADLE"; // @@ -1432,6 +1483,13 @@ LOCAL_MODULE := libuv LOCAL_SRC_FILES = ../libs/$(TARGET_ARCH_ABI)/libuv.a include $(PREBUILT_STATIC_LIBRARY) +## libllama.a +include $(CLEAR_VARS) +LOCAL_MODULE := libllama + +LOCAL_SRC_FILES = ../libs/$(TARGET_ARCH_ABI)/libllama.a +include $(PREBUILT_STATIC_LIBRARY) + ## libsocket-runtime.a include $(CLEAR_VARS) LOCAL_MODULE := libsocket-runtime-static @@ -1446,30 +1504,29 @@ include $(PREBUILT_STATIC_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := socket-runtime -LOCAL_CFLAGS += \ - -std=c++2a \ - -g \ - -I$(LOCAL_PATH)/include \ - -I$(LOCAL_PATH) \ - -pthreads \ - -fexceptions \ - -fPIC \ - -frtti \ - -fsigned-char \ +LOCAL_CFLAGS += \ + -std=c++2a \ + -g \ + -I$(LOCAL_PATH)/include \ + -I$(LOCAL_PATH) \ + -pthreads \ + -fexceptions \ + -fPIC \ + -frtti \ + -fsigned-char \ -O0 LOCAL_CFLAGS += {{cflags}} LOCAL_LDLIBS := -landroid -llog -LOCAL_SRC_FILES = \ - android/bridge.cc \ - android/runtime.cc \ - android/string_wrap.cc \ - android/window.cc \ +LOCAL_SRC_FILES = \ init.cc -LOCAL_STATIC_LIBRARIES := \ - libuv \ +LOCAL_STATIC_LIBRARIES := \ + libllama \ + +LOCAL_WHOLE_STATIC_LIBRARIES := \ + libuv \ libsocket-runtime-static include $(BUILD_SHARED_LIBRARY) @@ -1514,9 +1571,9 @@ constexpr auto gProGuardRules = R"PGR( )PGR"; // -// Android `layout/web_view_activity.xml` +// Android `layout/web_view.xml` // -constexpr auto gAndroidLayoutWebviewActivity = R"XML( +constexpr auto gAndroidLayoutWebView = R"XML( <?xml version="1.0" encoding="utf-8"?> <WebView xmlns:android="http://schemas.android.com/apk/res/android" @@ -1527,6 +1584,25 @@ constexpr auto gAndroidLayoutWebviewActivity = R"XML( /> )XML"; +// +// Android `layout/window_container_view.xml` +// +constexpr auto gAndroidLayoutWindowContainerView = R"XML( +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" +> + <androidx.fragment.app.FragmentContainerView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/window" + android:layout_width="match_parent" + android:layout_height="match_parent" + /> +</FrameLayout> +)XML"; + constexpr auto gAndroidValuesStrings = R"XML( <resources> <string name="app_name">{{meta_title}}</string> @@ -1539,7 +1615,6 @@ constexpr auto gAndroidValuesStrings = R"XML( constexpr auto gXCodeScheme = R"XML(<?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1310" - version = "1.3"> <BuildAction parallelizeBuildables = "YES" @@ -1651,14 +1726,6 @@ constexpr auto gDefaultConfig = R"INI( ; ; Socket ⚡︎ Runtime · A modern runtime for Web Apps · v{{ssc_version}} ; - -; The value of the "script" property in the build section will be interpreted as -; a shell command when you run "ssc build". This is the most important command -; in this file. It will do all the heavy lifting and should handle 99.9% of your -; use cases for moving files into place or tweaking platform-specific build -; artifacts. If you don't specify it, ssc will just copy everything in your -; project to the build target. -; ; Note that "~" alias won't expand to the home directory in any of the config ; files. Use the full path instead. @@ -1697,18 +1764,21 @@ output = "build" [build.script] + ; If true, it will pass build arguments to the build script. WARNING: this could be deprecated in the future. ; default value: false forward_arguments = false [build.watch] + ; Configure your project to watch for sources that could change when running `ssc`. ; Could be a string or an array of strings sources[] = "src" [webview] + ; Make root open index.html ; default value: "/" root = "/" @@ -1722,7 +1792,6 @@ root = "/" watch = true ; Custom headers injected on all webview routes -[webview] ; default value: "" ; headers[] = "X-Custom-Header: Some-Value" @@ -1731,15 +1800,23 @@ watch = true ; default value: true reload = true +; Timeout in milliseconds to wait for service worker to reload before reloading webview +; default value: 500 +service_worker_reload_timeout = 500 ; Mount file system paths in webview navigator [webview.navigator.mounts] + ; $HOST_HOME/directory-in-home-folder/ = /mount/path/in/navigator ; $HOST_CONTAINER/directory-app-container/ = /mount/path/in/navigator ; $HOST_PROCESS_WORKING_DIRECTORY/directory-in-app-process-working-directory/ = /mount/path/in/navigator +; Specify allowed navigator navigation policy patterns +[webview.navigator.policies] +; allowed[] = "https://*.example.com/*" [permissions] + ; Allow/Disallow fullscreen in application ; default value: true ; allow_fullscreen = true @@ -1789,6 +1866,7 @@ reload = true ; allow_hotkeys = true [debug] + ; Advanced Compiler Settings for debug purposes (ie C++ compiler -g, etc). flags = "-g" @@ -1831,8 +1909,6 @@ version = 1.0.0 [android] -; The icon to use for identifying your app on Android. -icon = "src/icon.png" ; Extensions of files that will not be stored compressed in the APK. aapt_no_compress = "" @@ -1855,14 +1931,20 @@ native_sources = "" native_makefile = "" sources = "" +; The icon to use for identifying your app on Android. +icon = "src/icon.png" + +; The various sizes and scales of the icons to create, required minimum are listed by default. +icon_sizes = "512@1x" + [ios] ; signing guide: https://socketsupply.co/guides/#ios-1 codesign_identity = "" -; Describes how Xcode should export the archive. Available options: app-store, package, ad-hoc, enterprise, development, and developer-id. -distribution_method = "ad-hoc" +; Describes how Xcode should export the archive. Available options: app-store, package, release-testing, enterprise, development, and developer-id. +distribution_method = "release-testing" ; A path to the provisioning profile used for signing iOS app. provisioning_profile = "" @@ -1874,8 +1956,14 @@ simulator_device = "iPhone 14" ; default value: false ; nonexempt_encryption = false +; The icon to use for identifying your app on iOS. +icon = "src/icon.png" + +; The various sizes and scales of the icons to create, required minimum are listed by default. +icon_sizes = "29@1x 29@2x 29@3x 40@2x 40@3x 57@1x 57@2x 60@2x 60@3x" [linux] + ; Helps to make your app searchable in Linux desktop environments. categories = "Developer Tools" @@ -1885,6 +1973,9 @@ categories = "Developer Tools" ; The icon to use for identifying your app in Linux desktop environments. icon = "src/icon.png" +; The various sizes and scales of the icons to create, required minimum are listed by default. +icon_sizes = "512@1x" + [mac] @@ -1894,9 +1985,6 @@ category = "" ; The command to execute to spawn the "back-end" process. ; cmd = "node backend/index.js" -; The icon to use for identifying your app on MacOS. -icon = "src/icon.png" - ; TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates codesign_identity = "" @@ -1907,6 +1995,15 @@ codesign_paths = "" ; default value: "13.0.0" ; minimum_supported_version = "13.0.0" +; If titlebar_style is "hiddenInset", this will determine the x and y offsets of the window controls (traffic lights). +; window_control_offsets = "10x24" + +; The icon to use for identifying your app on MacOS. +icon = "src/icon.png" + +; The various sizes and scales of the icons to create, required minimum are listed by default. +icon_sizes = "16@1x 32@1x 128@1x" + [native] @@ -1922,9 +2019,6 @@ headers = native-module1.hh ; The command to execute to spawn the “back-end” process. ; cmd = "node backend/index.js" -; The icon to use for identifying your app on Windows. -icon = "src/icon.ico" - ; The icon to use for identifying your app on Windows, relative to copied path resources logo = "icon.ico" @@ -1934,6 +2028,12 @@ logo = "icon.ico" ; The signing information needed by the appx api. ; publisher = "CN=Beep Boop Corp., O=Beep Boop Corp., L=San Francisco, S=California, C=US" +; The icon to use for identifying your app on Windows. +icon = "src/icon.ico" + +; The various sizes and scales of the icons to create, required minimum are listed by default. +icon_sizes = "512@1x" + [window] @@ -1943,6 +2043,18 @@ height = 50% ; The initial width of the first window in pixels or as a percentage of the screen. width = 50% +; The initial color of the window in dark mode. If not provided, matches the current theme. +; default value: "" +; backgroundColorDark = "rgba(0, 0, 0, 1)" + +; The initial color of the window in light mode. If not provided, matches the current theme. +; default value: "" +; backgroundColorLight = "rgba(255, 255, 255, 1)" + +; Determine if the titlebar style (hidden, hiddenInset) +; default value: "" +; titlebar_style = "hiddenInset" + ; Maximum height of the window in pixels or as a percentage of the screen. ; default value: 100% ; max_height = 100% @@ -1959,38 +2071,53 @@ width = 50% ; default value: 0 ; min_width = 0 -; If the window is resizable or not. +; Determines if the window has a title bar and border. +; default value: false +; frameless = false + +; Determines if the window is resizable. ; default value: true ; resizable = true -; If the window has a title bar or not. -; default value: false -; frameless = false +; Determines if the window is maximizable. +; default value: true +; maximizable = true + +; Determines if the window is minimizable. +; default value: true +; minimizable = true + +; Determines if the window is closable. +; default value: true +; closable = true -; If the window is utility window or not. +; Determines the window is utility window. ; default value: false ; utility = false - [window.alert] + ; The title that appears in the 'alert', 'prompt', and 'confirm' dialogs. If this value is not present, then the application title is used instead. Currently only supported on iOS/macOS. ; defalut value = "" ; title = "" [application] + ; If agent is set to true, the app will not display in the tab/window switcher or dock/task-bar etc. Useful if you are building a tray-only app. ; default value: false ; agent = true [tray] + ; The icon to be displayed in the operating system tray. On Windows, you may need to use ICO format. ; defalut value = "" -; icon = "icon.png" +; icon = "src/icon.png" [headless] + ; The headless runner command. It is used when no OS specific runner is set. runner = "" ; The headless runner command flags. It is used when no OS specific runner is set. diff --git a/src/core/bluetooth.cc b/src/core/bluetooth.cc index b8366ee080..e4a55a2038 100644 --- a/src/core/bluetooth.cc +++ b/src/core/bluetooth.cc @@ -1,9 +1,10 @@ #include "core.hh" +#include "bluetooth.hh" #include "../ipc/ipc.hh" using namespace SSC; -#if defined(__APPLE__) +#if SOCKET_RUNTIME_PLATFORM_APPLE @interface SSCBluetoothController () @property (nonatomic) Bluetooth* bluetooth; @end @@ -369,10 +370,14 @@ using namespace SSC; Post post = {0}; post.id = rand64(); - post.body = bytes; - post.length = length; post.headers = headers.str(); + if (bytes != nullptr && length > 0) { + post.body = std::make_shared<char[]>(length); + post.length = length; + memcpy(post.body.get(), bytes, length); + } + auto json = JSON::Object::Entries { {"data", JSON::Object::Entries { {"event", "data"}, diff --git a/src/core/bluetooth.hh b/src/core/bluetooth.hh new file mode 100644 index 0000000000..073eb8b437 --- /dev/null +++ b/src/core/bluetooth.hh @@ -0,0 +1,72 @@ +#ifndef SOCKET_RUNTIME_CORE_BLUETOOTH_H +#define SOCKET_RUNTIME_CORE_BLUETOOTH_H + +#include "../platform/platform.hh" +#include "json.hh" +#include "post.hh" + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@interface SSCBluetoothController : NSObject< + CBCentralManagerDelegate, + CBPeripheralManagerDelegate, + CBPeripheralDelegate +> +@property (strong, nonatomic) CBCentralManager* centralManager; +@property (strong, nonatomic) CBPeripheralManager* peripheralManager; +@property (strong, nonatomic) CBPeripheral* bluetoothPeripheral; +@property (strong, nonatomic) NSMutableArray* peripherals; +@property (strong, nonatomic) NSMutableDictionary* services; +@property (strong, nonatomic) NSMutableDictionary* characteristics; +@property (strong, nonatomic) NSMutableDictionary* serviceMap; +- (void) startAdvertising; +- (void) startScanning; +- (id) init; +@end +#endif + +namespace SSC { + class Core; + class Bluetooth { + public: + using SendFunction = Function<void(const String, JSON::Any, Post)>; + using EmitFunction = Function<void(const String, JSON::Any)>; + using Callback = Function<void(String, JSON::Any)>; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + SSCBluetoothController* controller = nullptr; + #endif + + Core *core = nullptr; + SendFunction sendFunction; + EmitFunction emitFunction; + + Bluetooth (); + ~Bluetooth (); + bool send (const String& seq, JSON::Any json, Post post); + bool send (const String& seq, JSON::Any json); + bool emit (const String& seq, JSON::Any json); + void startScanning (); + void publishCharacteristic ( + const String& seq, + char* bytes, + size_t size, + const String& serviceId, + const String& characteristicId, + Callback callback + ); + + void subscribeCharacteristic ( + const String& seq, + const String& serviceId, + const String& characteristicId, + Callback callback + ); + + void startService ( + const String& seq, + const String& serviceId, + Callback callback + ); + }; +} +#endif diff --git a/src/core/codec.cc b/src/core/codec.cc index e164bb5ec5..12c24f7f37 100644 --- a/src/core/codec.cc +++ b/src/core/codec.cc @@ -1,7 +1,8 @@ -#include "codec.hh" -#include "string.hh" #include <math.h> +#include "../platform/platform.hh" +#include "codec.hh" + #define UNSIGNED_IN_RANGE(value, min, max) ( \ (unsigned char) (value) >= (unsigned char) (min) && \ (unsigned char) (value) <= (unsigned char) (max) \ @@ -75,6 +76,131 @@ namespace SSC { return bytes; } + void innerHash (unsigned int* result, unsigned int* w) { + unsigned int a = result[0]; + unsigned int b = result[1]; + unsigned int c = result[2]; + unsigned int d = result[3]; + unsigned int e = result[4]; + int round = 0; + + #define sha1macro(func,val) \ + { \ + const unsigned int t = rol(a, 5) + (func) + e + val + w[round]; \ + e = d; \ + d = c; \ + c = rol(b, 30); \ + b = a; \ + a = t; \ + } + while (round < 16) { + sha1macro((b & c) | (~b & d), 0x5a827999) + ++round; + } + while (round < 20) { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (~b & d), 0x5a827999) + ++round; + } + while (round < 40) { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0x6ed9eba1) + ++round; + } + while (round < 60) { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (b & d) | (c & d), 0x8f1bbcdc) + ++round; + } + while (round < 80) { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0xca62c1d6) + ++round; + } + #undef sha1macro + + result[0] += a; + result[1] += b; + result[2] += c; + result[3] += d; + result[4] += e; + } + + void shacalc (const char* src, char* dest) { + unsigned char hash[20]; + int bytelength = strlen(src); + unsigned int result[5] = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 }; + const unsigned char* sarray = (const unsigned char*) src; + unsigned int w[80]; + const int endOfFullBlocks = bytelength - 64; + int endCurrentBlock; + int currentBlock = 0; + + while (currentBlock <= endOfFullBlocks) { + endCurrentBlock = currentBlock + 64; + int roundPos = 0; + + for (roundPos = 0; currentBlock < endCurrentBlock; currentBlock += 4) { + w[roundPos++] = (unsigned int) sarray[currentBlock + 3] + | (((unsigned int) sarray[currentBlock + 2]) << 8) + | (((unsigned int) sarray[currentBlock + 1]) << 16) + | (((unsigned int) sarray[currentBlock]) << 24); + } + innerHash(result, w); + } + + endCurrentBlock = bytelength - currentBlock; + clearWBuffert(w); + int lastBlockBytes = 0; + + for (; lastBlockBytes < endCurrentBlock; ++lastBlockBytes) { + w[lastBlockBytes >> 2] |= (unsigned int) sarray[lastBlockBytes + currentBlock] << ((3 - (lastBlockBytes & 3)) << 3); + } + + w[lastBlockBytes >> 2] |= 0x80 << ((3 - (lastBlockBytes & 3)) << 3); + + if (endCurrentBlock >= 56) { + innerHash(result, w); + clearWBuffert(w); + } + + w[15] = bytelength << 3; + innerHash(result, w); + int hashByte = 0; + + for (hashByte = 20; --hashByte >= 0;) { + hash[hashByte] = (result[hashByte >> 2] >> (((3 - hashByte) & 0x3) << 3)) & 0xff; + } + memcpy(dest, hash, 20); + } + + unsigned char* base64Encode(const unsigned char *data, size_t input_length, size_t *output_length) { + *output_length = (size_t) (4.0 * ceil((double) input_length / 3.0)); + unsigned char *encoded_data = (unsigned char *) malloc(*output_length + 1); + if (!encoded_data) return 0; + + int i = 0; + int j = 0; + + for (i = 0, j = 0; i < input_length;) { + uint32_t octet_a = i < input_length ? data[i++] : 0; + uint32_t octet_b = i < input_length ? data[i++] : 0; + uint32_t octet_c = i < input_length ? data[i++] : 0; + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F]; + encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F]; + encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F]; + encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F]; + } + + for (i = 0; i < mod_table[input_length % 3]; i++) { + encoded_data[*output_length - 1 - i] = '='; + } + + encoded_data[*output_length] = '\0'; // Null-terminate the string + return encoded_data; + } + String encodeURIComponent (const String& input) { auto pointer = (unsigned char*) input.c_str(); const auto length = (int) input.length(); diff --git a/src/core/codec.hh b/src/core/codec.hh index 6ab89360be..5c154ee647 100644 --- a/src/core/codec.hh +++ b/src/core/codec.hh @@ -1,7 +1,7 @@ -#ifndef SSC_CORE_CODEC_HH -#define SSC_CORE_CODEC_HH +#ifndef SOCKET_RUNTIME_CORE_CODEC_H +#define SOCKET_RUNTIME_CORE_CODEC_H -#include "types.hh" +#include "../platform/types.hh" namespace SSC { /** @@ -50,6 +50,56 @@ namespace SSC { * @return An array of `uint8_t` values */ const Array<uint8_t, 8> toBytes (const uint64_t input); + + /** + * Rotates an unsigned integer value left by a specified number of steps. + * @param value The unsigned integer value to rotate + * @param steps The number of steps to rotate left + * @return The rotated unsigned integer value + */ + inline const unsigned int rol (const unsigned int value, const unsigned int steps) { + return ((value << steps) | (value >> (32 - steps))); + } + + /** + * Clears a buffer of unsigned integers by setting each element to zero. + * @param buffert Pointer to the buffer to clear + */ + inline void clearWBuffert (unsigned int* buffert) { + int pos = 0; + for (pos = 16; --pos >= 0;) { + buffert[pos] = 0; + } + } + + /** + * Computes the inner hash for the SHA-1 algorithm. + * @param result Pointer to an array of unsigned integers to store the result + * @param w Pointer to an array of unsigned integers to use in computation + */ + void innerHash (unsigned int* result, unsigned int* w); + + /** + * Calculates the SHA-1 hash of a given input string. + * @param src Pointer to the input string + * @param dest Pointer to the destination buffer to store the hash + */ + void shacalc (const char* src, char* dest); + + /** + * Encodes a given input data to a Base64 encoded string. + * @param data Pointer to the input data + * @param input_length Length of the input data + * @param output_length Pointer to store the length of the encoded output + * @return Pointer to the Base64 encoded string + */ + unsigned char* base64Encode(const unsigned char *data, size_t input_length, size_t *output_length); + + // Encoding table for base64 encoding + const char encoding_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + // Modulus table for base64 encoding + const int mod_table[] = {0, 2, 1}; } #endif diff --git a/src/core/color.cc b/src/core/color.cc new file mode 100644 index 0000000000..704510cf4d --- /dev/null +++ b/src/core/color.cc @@ -0,0 +1,336 @@ +#include "../platform/string.hh" +#include "color.hh" + +namespace SSC { + template <typename T> + static inline T clamp (T v, T x, T y) { + if (v < x) { + return x; + } + + if (v > y) { + return y; + } + + return x; + } + + ColorComponents::ColorComponents (const ColorComponents& components) { + this->red = components.red; + this->green = components.green; + this->blue = components.blue; + this->alpha = components.alpha; + } + + ColorComponents::ColorComponents (ColorComponents&& components) { + this->red = components.red; + this->green = components.green; + this->blue = components.blue; + this->alpha = components.alpha; + + components.red = 0; + components.green = 0; + components.blue = 0; + components.alpha = 0.0f; + } + + ColorComponents::ColorComponents (const String& string) { + auto buffer = replace(trim(string), "none", "0"); + Vector<String> values; + + if (buffer.starts_with("rgba(")) { + buffer = buffer.substr(5); + if (buffer.ends_with(")")) { + buffer = trim(buffer.substr(0, buffer.size() - 1)); + const auto components = split(buffer, ','); + if (components.size() == 4) { + values.push_back(trim(components[0])); + values.push_back(trim(components[1])); + values.push_back(trim(components[2])); + values.push_back(trim(components[3])); + } + } + } else if (buffer.starts_with("rgb(")) { + buffer = buffer.substr(4); + if (buffer.ends_with(")")) { + buffer = trim(buffer.substr(0, buffer.size() - 1)); + const auto components = split(buffer, ','); + if (components.size() == 3) { + values.push_back(trim(components[0])); + values.push_back(trim(components[1])); + values.push_back(trim(components[2])); + } + } + } else if (buffer.starts_with("#")) { + buffer = buffer.substr(1); + if (buffer.size() == 6) { + try { + values.push_back(std::to_string(std::stoul(buffer.substr(0, 2), 0, 16))); + values.push_back(std::to_string(std::stoul(buffer.substr(2, 2), 0, 16))); + values.push_back(std::to_string(std::stoul(buffer.substr(4, 2), 0, 16))); + } catch (...) {} + } + } + + if (values.size() == 3 || values.size() == 4) { + try { + if (values[0].ends_with("%")) { + this->red = 255 * ( + std::stoi(values[0].substr(0, values[0].size() - 1)) / 100.0f + ); + } else { + this->red = std::stoi(values[0]); + } + + if (values[1].ends_with("%")) { + this->green = 255 * ( + std::stoi(values[1].substr(0, values[1].size() - 1)) / 100.0f + ); + } else { + this->green = std::stoi(values[1]); + } + + if (values[2].ends_with("%")) { + this->blue = 255 * ( + std::stoi(values[2].substr(0, values[2].size() - 1)) / 100.0f + ); + } else { + this->blue = std::stoi(values[2]); + } + + if (values.size() == 4) { + if (values[3].ends_with("%")) { + this->alpha = std::stoi(values[2].substr(0, values[2].size() - 1)) / 100.0f; + } else { + this->alpha = std::stoi(values[2]); + if (this->alpha > 1.0f) { + this->alpha = this->alpha / 255.0f; + } + } + } + } catch (...) {} + } + } + + ColorComponents::ColorComponents ( + unsigned int red, + unsigned int green, + unsigned int blue, + float alpha + ) : red(red & 0xFF), + green(green & 0xFF), + blue(blue & 0xFF), + alpha(clamp(alpha, MIN_FLOAT_CHANNEL, MAX_FLOAT_CHANNEL)) + {} + + ColorComponents::ColorComponents ( + float red, + float green, + float blue, + float alpha + ) { + this->red = static_cast<int>(red * MAX_INT_CHANNEL) & 0xFF; + this->green = static_cast<int>(green * MAX_INT_CHANNEL) & 0xFF; + this->blue = static_cast<int>(blue * MAX_INT_CHANNEL) & 0xFF; + this->alpha = clamp(alpha, MIN_FLOAT_CHANNEL, MAX_FLOAT_CHANNEL); + } + + ColorComponents::ColorComponents ( + unsigned int red, + unsigned int green, + unsigned int blue, + unsigned int alpha + ) { + this->red = red & 0xFF; + this->green = green & 0xFF; + this->blue = blue & 0xFF; + this->alpha = clamp( + alpha / static_cast<float>(MAX_INT_CHANNEL), + MIN_FLOAT_CHANNEL, + MAX_FLOAT_CHANNEL + ); + } + + ColorComponents::ColorComponents ( + uint32_t value, + const Pack& pack + ) { + switch (pack) { + case Pack::RGB: { + this->red = (value >> 24) & 0xFF; + this->green = (value >> 16) & 0xFF; + this->blue = (value >> 8) & 0xFF; + this->alpha = 1.0f; + break; + } + + case Pack::RGBA: { + this->red = (value >> 24) & 0xFF; + this->green = (value >> 16) & 0xFF; + this->blue = (value >> 8) & 0xFF; + this->alpha = (value & 0xFF) / static_cast<float>(MAX_INT_CHANNEL); + break; + } + + case Pack::ARGB: { + this->alpha = ((value >> 24) & 0xFF) / static_cast<float>(MAX_INT_CHANNEL); + this->red = (value >> 16) & 0xFF; + this->green = (value >> 8) & 0xFF; + this->blue = value & 0xFF; + break; + } + } + } + + ColorComponents& ColorComponents::operator = (const ColorComponents& components) { + this->red = components.red; + this->green = components.green; + this->blue = components.blue; + this->alpha = components.alpha; + return *this; + } + + ColorComponents& ColorComponents::operator = (ColorComponents&& components) { + this->red = components.red; + this->green = components.green; + this->blue = components.blue; + this->alpha = components.alpha; + + components.red = 0; + components.green = 0; + components.blue = 0; + components.alpha = 0.0f; + return *this; + } + + JSON::Object ColorComponents::json () const { + return JSON::Object::Entries { + {"red", this->red}, + {"green", this->green}, + {"blue", this->blue}, + {"alpha", this->alpha} + }; + } + + String ColorComponents::str (const Pack& pack) const { + String output; + + switch (pack) { + case Pack::RGB: + output = "rgb({{red}}, {{green}}, {{blue}})"; + break; + case Pack::RGBA: + output = "rgba({{red}}, {{green}}, {{blue}}, {{alpha}})"; + break; + case Pack::ARGB: + output = "argb({{alpha}}, {{red}}, {{green}}, {{blue}})"; + break; + } + + return tmpl(output, Map { + {"red", std::to_string(this->red)}, + {"green", std::to_string(this->green)}, + {"blue", std::to_string(this->blue)}, + {"alpha", std::to_string(this->alpha)}, + }); + } + + uint32_t ColorComponents::pack (const Pack& pack) const { + switch (pack) { + case Pack::RGB: { + return ( + (this->red & 0xFF) << 24 | + (this->green & 0xFF) << 16 | + (this->blue & 0xFF) << 8 | + (255) // asume full alpha channel value + ); + } + + case Pack::RGBA: { + const auto alpha = static_cast<int>( + 255.0f * clamp(this->alpha, MIN_FLOAT_CHANNEL, MAX_FLOAT_CHANNEL) + ); + return ( + (this->red & 0xFF) << 24 | + (this->green & 0xFF) << 16 | + (this->blue & 0xFF) << 8 | + (alpha & 0xFF) + ); + } + + case Pack::ARGB: { + const auto alpha = static_cast<int>( + 255.0f * clamp(this->alpha, MIN_FLOAT_CHANNEL, MAX_FLOAT_CHANNEL) + ); + return ( + (alpha & 0xFF) << 24 | + (this->red & 0xFF) << 16 | + (this->green & 0xFF) << 8 | + (this->blue & 0xFF) + ); + } + } + + return 0; + } + + Color::Color (const ColorComponents&) { + } + + Color::Color ( + unsigned int red, + unsigned int green, + unsigned int blue, + float alpha + ) : ColorComponents(red, green, blue, alpha) + {} + + Color::Color (const Color& color) + : Color(color.red, color.green, color.blue, color.alpha) + {} + + Color::Color (Color&& color) + : Color(color.red, color.green, color.blue, color.alpha) + { + color.red = 0; + color.green = 0; + color.blue = 0; + color.alpha = 0.0f; + } + + Color& Color::operator = (const Color& color) { + this->red = color.red; + this->green = color.green; + this->blue = color.blue; + this->alpha = color.alpha; + return *this; + } + + Color& Color::operator = (Color&& color) { + this->red = color.red; + this->green = color.green; + this->blue = color.blue; + this->alpha = color.alpha; + color.red = 0; + color.green = 0; + color.blue = 0; + color.alpha = 0.0f; + return *this; + } + + bool Color::operator == (const Color& color) const { + return this->pack() == color.pack(); + } + + bool Color::operator != (const Color& color) const { + return this->pack() != color.pack(); + } + + bool Color::operator > (const Color& color) const { + return this->pack() > color.pack(); + } + + bool Color::operator < (const Color& color) const { + return this->pack() < color.pack(); + } +} diff --git a/src/core/color.hh b/src/core/color.hh new file mode 100644 index 0000000000..e742dfb953 --- /dev/null +++ b/src/core/color.hh @@ -0,0 +1,302 @@ +#ifndef SOCKET_RUNTIME_CORE_COLOR_H +#define SOCKET_RUNTIME_CORE_COLOR_H + +#include "json.hh" + +namespace SSC { + /** + * A container for RGB color components with an alpha channel + */ + class ColorComponents { + public: + /** + * Various pack formats for color components represented as a + * 32 bit unsigned integer. + */ + enum class Pack { RGB, RGBA, ARGB }; + + /** + * The minimum "int" color component value. + */ + static constexpr unsigned int MIN_INT_CHANNEL = 0; + + /** + * The maximum "int" color component value. + */ + static constexpr unsigned int MAX_INT_CHANNEL = 255; + + /** + * The minimum "float" color component value. + */ + static constexpr float MIN_FLOAT_CHANNEL = 0.0f; + + /** + * The maximum "float" color component value. + */ + static constexpr float MAX_FLOAT_CHANNEL = 1.0f; + + /** + * The "red" color component. + */ + unsigned int red = MIN_INT_CHANNEL; + + /** + * The "green" color component. + */ + unsigned int green = MIN_INT_CHANNEL; + + /** + * The "blue" color component. + */ + unsigned int blue = MIN_INT_CHANNEL; + + /** + * The alpha channel component. + */ + float alpha = MAX_FLOAT_CHANNEL; + + /** + * Default `ColorComponents` constructor. + */ + ColorComponents () = default; + + /** + * `ColorComponents` "copy" constructor. + */ + ColorComponents (const ColorComponents&); + + /** + * `ColorComponents` "move" constructor. + */ + ColorComponents (ColorComponents&&); + + /** + * `ColorComponents` constructor from a "rgba()", "rgb()", + * or "#RRGGBB" syntax DSL. + */ + ColorComponents (const String&); + + /** + * `ColorComponents` constructor for "int" color components + * with a "float" alpha channel. + */ + ColorComponents ( + unsigned int red, + unsigned int green, + unsigned int blue, + float alpha = MAX_FLOAT_CHANNEL + ); + + /** + * `ColorComponents` constructor for "float" color components + * with a "float" alpha channel. + */ + ColorComponents ( + float red, + float green, + float blue, + float alpha = MAX_FLOAT_CHANNEL + ); + + /** + * `ColorComponents` constructor for "float" color components + * with a "float" alpha channel. + */ + ColorComponents ( + unsigned int red, + unsigned int green, + unsigned int blue, + unsigned int alpha = MAX_INT_CHANNEL + ); + + /** + * `ColorComponents` constructor "packed" color components + * with a pack type. + */ + ColorComponents ( + uint32_t value, + const Pack& pack = Pack::RGBA + ); + + /** + * `ColorComponents` "copy" assignment. + */ + ColorComponents& operator = (const ColorComponents&); + + /** + * `ColorComponents` "move" assignment. + */ + ColorComponents& operator = (ColorComponents&&); + + /** + * Returns a JSON object of color components. + */ + JSON::Object json () const; + + /** + * Returns a string representation of the color components. + */ + String str (const Pack& pack = Pack::RGBA) const; + + /** + * Returns a packed representation of the color components. + */ + uint32_t pack (const Pack& pack = Pack::RGBA) const; + }; + + /** + * A container for RGBA based color space. + */ + class Color : public ColorComponents { + public: + /** + * An alias to `ColorComponents::Pack` + */ + using Pack = ColorComponents::Pack; + + /** + * An alias for `ColorComponents::MIN_INT_CHANNEL` + */ + static constexpr unsigned int MIN_INT_CHANNEL = ColorComponents::MIN_INT_CHANNEL; + + /** + * An alias for `ColorComponents::MIN_INT_CHANNEL` + */ + static constexpr unsigned int MAX_INT_CHANNEL = ColorComponents::MAX_INT_CHANNEL; + + /** + * An alias for `ColorComponents::MIN_INT_CHANNEL` + */ + static constexpr float MIN_FLOAT_CHANNEL = ColorComponents::MIN_FLOAT_CHANNEL; + + /** + * An alias for `ColorComponents::MIN_INT_CHANNEL` + */ + static constexpr float MAX_FLOAT_CHANNEL = ColorComponents::MAX_FLOAT_CHANNEL; + + /** + * Creates a packed rgba value from `red`, `green`, `blue` "int" + * color components and an `alpha` "float" channel. + */ + static uint32_t rgba ( + unsigned int red = MIN_INT_CHANNEL, + unsigned int green = MIN_INT_CHANNEL, + unsigned int blue = MIN_INT_CHANNEL, + float alpha = MAX_FLOAT_CHANNEL + ) { + return Color(red, green, blue, alpha).pack(Pack::RGBA); + } + + /** + * Creates a packed rgba value from `red`, `green`, `blue` "float" + * color components and an `alpha` "float" channel. + */ + static uint32_t rgba ( + float red = MIN_FLOAT_CHANNEL, + float green = MIN_FLOAT_CHANNEL, + float blue = MIN_FLOAT_CHANNEL, + float alpha = MAX_FLOAT_CHANNEL + ) { + return Color(red, green, blue, alpha).pack(Pack::RGBA); + } + + /** + * Creates a packed rgba value from `red`, `green`, `blue` "int" + * color components and an `alpha` "int" channel. + */ + static uint32_t rgba ( + unsigned int red = MIN_INT_CHANNEL, + unsigned int green = MIN_INT_CHANNEL, + unsigned int blue = MIN_INT_CHANNEL, + unsigned int alpha = MAX_INT_CHANNEL + ) { + return Color(red, green, blue, alpha).pack(Pack::RGBA); + } + + /** + * Creates a packed rgb value from `red`, `green`, and `blue` "int" + * color components. + */ + static uint32_t rgb ( + unsigned int red = MIN_INT_CHANNEL, + unsigned int green = MIN_INT_CHANNEL, + unsigned int blue = MIN_INT_CHANNEL + ) { + return Color(red, green, blue).pack(Pack::RGB); + } + + /** + * Creates a packed rgb value from `red`, `green`, and `blue` "float" + * color components. + */ + static uint32_t rgb ( + float red = MIN_FLOAT_CHANNEL, + float green = MIN_FLOAT_CHANNEL, + float blue = MIN_FLOAT_CHANNEL + ) { + return Color(red, green, blue).pack(Pack::RGB); + } + + /** + * Default `Color` constructor. + */ + Color () = default; + + /** + * `Color` constructor from `ColorComponents` + */ + Color (const ColorComponents&); + + /** + * `Color` constructor with "int" color components and + * a "float" alpha channel. + */ + Color ( + unsigned int red, + unsigned int green, + unsigned int blue, + float alpha = MIN_FLOAT_CHANNEL + ); + + /** + * `Color` "copy" constructor. + */ + Color (const Color&); + + /** + * `Color` "move" constructor. + */ + Color (Color&&); + + /** + * `Color` "copy" assignment. + */ + Color& operator = (const Color&); + + /** + * `Color` "move" assignment. + */ + Color& operator = (Color&&); + + /** + * `Color` equality operator. + */ + bool operator == (const Color&) const; + + /** + * `Color` inequality operator. + */ + bool operator != (const Color&) const; + + /** + * `Color` greater than inequality operator. + */ + bool operator > (const Color&) const; + + /** + * `Color` less than inequality operator. + */ + bool operator < (const Color&) const; + }; +} +#endif diff --git a/src/core/config.cc b/src/core/config.cc index 79e0f3ef3b..b0598c45a8 100644 --- a/src/core/config.cc +++ b/src/core/config.cc @@ -1,13 +1,31 @@ #include "config.hh" -#include "ini.hh" -#include "string.hh" - #include "debug.hh" +#include "ini.hh" namespace SSC { static constexpr char NAMESPACE_SEPARATOR = '.'; static const String NAMESPACE_SEPARATOR_STRING = String(1, NAMESPACE_SEPARATOR); + bool isDebugEnabled () { + return socket_runtime_init_is_debug_enabled(); + } + + const Map getUserConfig () { + const auto bytes = socket_runtime_init_get_user_config_bytes(); + const auto size = socket_runtime_init_get_user_config_bytes_size(); + const auto string = String(reinterpret_cast<const char*>(bytes), size); + const auto userConfig = INI::parse(string); + return userConfig; + } + + const String getDevHost () { + return socket_runtime_init_get_dev_host(); + } + + int getDevPort () { + return socket_runtime_init_get_dev_port(); + } + Config::Config (const String& source) { this->map = INI::parse(source, NAMESPACE_SEPARATOR_STRING); } diff --git a/src/core/config.hh b/src/core/config.hh index 118d5a1527..bce1a96a8d 100644 --- a/src/core/config.hh +++ b/src/core/config.hh @@ -1,24 +1,19 @@ -#ifndef SSC_CORE_CONFIG_H -#define SSC_CORE_CONFIG_H +#ifndef SOCKET_RUNTIME_CORE_CONFIG_H +#define SOCKET_RUNTIME_CORE_CONFIG_H #include <iterator> -// TODO(@jwerle): remove this and any need for it -#ifndef SSC_SETTINGS -#define SSC_SETTINGS "" +#ifndef SOCKET_RUNTIME_VERSION +#define SOCKET_RUNTIME_VERSION "" #endif -#ifndef SSC_VERSION -#define SSC_VERSION "" -#endif - -#ifndef SSC_VERSION_HASH -#define SSC_VERSION_HASH "" +#ifndef SOCKET_RUNTIME_VERSION_HASH +#define SOCKET_RUNTIME_VERSION_HASH "" #endif // TODO(@jwerle): use a better name -#if !defined(WAS_CODESIGNED) -#define WAS_CODESIGNED 0 +#if !defined(SOCKET_RUNTIME_PLATFORM_SANDBOXED) +#define SOCKET_RUNTIME_PLATFORM_SANDBOXED 0 #endif // TODO(@jwerle): stop using this and prefer a namespaced macro @@ -37,14 +32,22 @@ #endif #if defined(__cplusplus) -#include "types.hh" +#include "../platform/platform.hh" -namespace SSC { +extern "C" { // implemented in `init.cc` - extern const Map getUserConfig (); - extern bool isDebugEnabled (); - extern const String getDevHost (); - extern int getDevPort (); + extern const unsigned char* socket_runtime_init_get_user_config_bytes (); + extern unsigned int socket_runtime_init_get_user_config_bytes_size (); + extern bool socket_runtime_init_is_debug_enabled (); + extern const char* socket_runtime_init_get_dev_host (); + extern int socket_runtime_init_get_dev_port (); +} + +namespace SSC { + const Map getUserConfig (); + bool isDebugEnabled (); + const String getDevHost (); + int getDevPort (); /** * A container for configuration that can be mutated and queried using @@ -211,5 +214,4 @@ namespace SSC { }; } #endif - #endif diff --git a/src/core/console.kt b/src/core/console.kt new file mode 100644 index 0000000000..be43837e82 --- /dev/null +++ b/src/core/console.kt @@ -0,0 +1,20 @@ +package socket.runtime.core + +object console { + val TAG = "Console" + fun log (string: String) { + android.util.Log.i(TAG, string) + } + + fun info (string: String) { + android.util.Log.i(TAG, string) + } + + fun debug (string: String) { + android.util.Log.d(TAG, string) + } + + fun error (string: String) { + android.util.Log.e(TAG, string) + } +} diff --git a/src/core/core.cc b/src/core/core.cc index bccbf77783..63d294728e 100644 --- a/src/core/core.cc +++ b/src/core/core.cc @@ -1,200 +1,130 @@ #include "core.hh" - -#define IMAX_BITS(m) ((m)/((m) % 255+1) / 255 % 255 * 8 + 7-86 / ((m) % 255+12)) -#define RAND_MAX_WIDTH IMAX_BITS(RAND_MAX) +#include "modules/fs.hh" namespace SSC { - uint64_t rand64 () { - uint64_t r = 0; - static bool init = false; +#if SOCKET_RUNTIME_PLATFORM_LINUX + struct UVSource { + GSource base; // should ALWAYS be first member + gpointer tag; + Core *core; + }; +#endif - if (!init) { - init = true; - srand(time(0)); + Post Core::getPost (uint64_t id) { + if (this->posts.find(id) == this->posts.end()) { + return Post{}; } - for (int i = 0; i < 64; i += RAND_MAX_WIDTH) { - r <<= RAND_MAX_WIDTH; - r ^= (unsigned) rand(); - } - return r; + return posts.at(id); } + void Core::shutdown () { + if (this->isShuttingDown || this->isPaused) { + return; + } - void msleep (uint64_t ms) { - std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(ms)); - } + this->isShuttingDown = true; + this->pause(); - Headers::Header::Header (const Header& header) { - this->key = header.key; - this->value = header.value; - } + #if !SOCKET_RUNTIME_PLATFORM_IOS + this->childProcess.shutdown(); + #endif - Headers::Header::Header (const String& key, const Value& value) { - this->key = trim(key); - this->value = trim(value.str()); + this->stopEventLoop(); + this->isShuttingDown = false; } - Headers::Headers (const String& source) { - for (const auto& entry : split(source, '\n')) { - const auto tuple = split(entry, ':'); - if (tuple.size() == 2) { - set(trim(tuple.front()), trim(tuple.back())); - } + void Core::resume () { + if (!this->isPaused) { + return; } - } - Headers::Headers (const Headers& headers) { - this->entries = headers.entries; - } + this->isPaused = false; + this->runEventLoop(); - Headers::Headers (const Vector<std::map<String, Value>>& entries) { - for (const auto& entry : entries) { - for (const auto& pair : entry) { - this->entries.push_back(Header { pair.first, pair.second }); - } + if (this->options.features.useUDP) { + this->udp.resumeAllSockets(); } - } - Headers::Headers (const Entries& entries) { - for (const auto& entry : entries) { - this->entries.push_back(entry); + if (options.features.useNetworkStatus) { + this->networkStatus.start(); } - } - void Headers::set (const String& key, const String& value) { - set(Header{ key, value }); - } - - void Headers::set (const Header& header) { - for (auto& entry : entries) { - if (header.key == entry.key) { - entry.value = header.value; - return; - } + if (options.features.useConduit) { + this->conduit.start(); } - entries.push_back(header); - } - - bool Headers::has (const String& name) const { - for (const auto& header : entries) { - if (header.key == name) { - return true; - } + if (options.features.useNotifications) { + this->notifications.start(); } - - return false; } - const Headers::Header& Headers::get (const String& name) const { - static const auto empty = Header(); - - for (const auto& header : entries) { - if (header.key == name) { - return header; - } + void Core::pause () { + if (this->isPaused) { + return; } - return empty; - } - - size_t Headers::size () const { - return this->entries.size(); - } + this->isPaused = true; - String Headers::str () const { - StringStream headers; - auto count = this->size(); - for (const auto& entry : this->entries) { - headers << entry.key << ": " << entry.value.str();; - if (--count > 0) { - headers << "\n"; - } + if (this->options.features.useUDP) { + this->udp.pauseAllSockets(); } - return headers.str(); - } - - Headers::Value::Value (const String& value) { - this->string = trim(value); - } - Headers::Value::Value (const char* value) { - this->string = value; - } - - Headers::Value::Value (const Value& value) { - this->string = value.string; - } - - Headers::Value::Value (bool value) { - this->string = value ? "true" : "false"; - } - - Headers::Value::Value (int value) { - this->string = std::to_string(value); - } - - Headers::Value::Value (float value) { - this->string = std::to_string(value); - } - - Headers::Value::Value (int64_t value) { - this->string = std::to_string(value); - } - - Headers::Value::Value (uint64_t value) { - this->string = std::to_string(value); - } + if (options.features.useNetworkStatus) { + this->networkStatus.stop(); + } - Headers::Value::Value (double_t value) { - this->string = std::to_string(value); - } + if (options.features.useConduit) { + this->conduit.stop(); + } -#if defined(__APPLE__) - Headers::Value::Value (ssize_t value) { - this->string = std::to_string(value); - } -#endif + if (options.features.useNotifications) { + this->notifications.stop(); + } - const String& Headers::Value::str () const { - return this->string; + #if !SOCKET_RUNTIME_PLATFORM_ANDROID + this->pauseEventLoop(); + #endif } - const char * Headers::Value::c_str() const { - return this->str().c_str(); - } + void Core::stop () { + Lock lock(this->mutex); + this->stopEventLoop(); + #if SOCKET_RUNTIME_PLATFORM_LINUX + if (this->gsource) { + const auto id = g_source_get_id(this->gsource); + if (id > 0) { + g_source_remove(id); + } - Post Core::getPost (uint64_t id) { - Lock lock(postsMutex); - if (posts->find(id) == posts->end()) return Post{}; - return posts->at(id); + g_object_unref(this->gsource); + this->gsource = nullptr; + this->didInitGSource = false; + } + #endif } bool Core::hasPost (uint64_t id) { - Lock lock(postsMutex); - return posts->find(id) != posts->end(); + return posts.find(id) != posts.end(); } bool Core::hasPostBody (const char* body) { - Lock lock(postsMutex); if (body == nullptr) return false; - for (auto const &tuple : *posts) { + for (const auto& tuple : posts) { auto post = tuple.second; - if (post.body == body) return true; + if (post.body.get() == body) return true; } return false; } void Core::expirePosts () { - Lock lock(postsMutex); - std::vector<uint64_t> ids; - auto now = std::chrono::system_clock::now() + Lock lock(this->mutex); + const auto now = std::chrono::system_clock::now() .time_since_epoch() .count(); - for (auto const &tuple : *posts) { + Vector<uint64_t> ids; + for (auto const &tuple : posts) { auto id = tuple.first; auto post = tuple.second; @@ -209,31 +139,27 @@ namespace SSC { } void Core::putPost (uint64_t id, Post p) { - Lock lock(postsMutex); + Lock lock(this->mutex); p.ttl = std::chrono::time_point_cast<std::chrono::milliseconds>( std::chrono::system_clock::now() + std::chrono::milliseconds(32 * 1024) ) .time_since_epoch() .count(); - posts->insert_or_assign(id, p); + + this->posts.insert_or_assign(id, p); } void Core::removePost (uint64_t id) { - Lock lock(postsMutex); - if (posts->find(id) == posts->end()) return; - auto post = getPost(id); + Lock lock(this->mutex); - if (post.body) { - delete [] post.body; + if (this->posts.find(id) != this->posts.end()) { + // debug("remove post %ld", this->posts.at(id).body.use_count()); + posts.erase(id); } - - posts->erase(id); } String Core::createPost (String seq, String params, Post post) { - Lock lock(postsMutex); - if (post.id == 0) { post.id = rand64(); } @@ -262,7 +188,8 @@ namespace SSC { " id, \n" " seq, \n" " params, \n" - " { workerId } \n" + " headers, \n" + " { workerId } \n" "); \n" ); @@ -271,10 +198,10 @@ namespace SSC { } void Core::removeAllPosts () { - Lock lock(postsMutex); - std::vector<uint64_t> ids; + Lock lock(this->mutex); + Vector<uint64_t> ids; - for (auto const &tuple : *posts) { + for (auto const &tuple : posts) { auto id = tuple.first; ids.push_back(id); } @@ -284,674 +211,8 @@ namespace SSC { } } - void Core::OS::cpus ( - const String seq, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { - #if defined(__ANDROID__) - { - auto json = JSON::Object::Entries { - {"source", "os.cpus"}, - {"data", JSON::Array::Entries {}} - }; - - cb(seq, json, Post{}); - return; - } - #endif - - uv_cpu_info_t* infos = nullptr; - int count = 0; - int status = uv_cpu_info(&infos, &count); - - if (status != 0) { - auto json = JSON::Object::Entries { - {"source", "os.cpus"}, - {"err", JSON::Object::Entries { - {"message", uv_strerror(status)} - }} - }; - - cb(seq, json, Post{}); - return; - } - - JSON::Array::Entries entries(count); - for (int i = 0; i < count; ++i) { - auto info = infos[i]; - entries[i] = JSON::Object::Entries { - {"model", info.model}, - {"speed", info.speed}, - {"times", JSON::Object::Entries { - {"user", info.cpu_times.user}, - {"nice", info.cpu_times.nice}, - {"sys", info.cpu_times.sys}, - {"idle", info.cpu_times.idle}, - {"irq", info.cpu_times.irq} - }} - }; - } - - auto json = JSON::Object::Entries { - {"source", "os.cpus"}, - {"data", entries} - }; - - uv_free_cpu_info(infos, count); - cb(seq, json, Post{}); - }); - } - - void Core::OS::networkInterfaces ( - const String seq, - Module::Callback cb - ) const { - uv_interface_address_t *infos = nullptr; - StringStream value; - StringStream v4; - StringStream v6; - int count = 0; - - int status = uv_interface_addresses(&infos, &count); - - if (status != 0) { - auto json = JSON::Object(JSON::Object::Entries { - {"source", "os.networkInterfaces"}, - {"err", JSON::Object::Entries { - {"type", "InternalError"}, - {"message", - String("Unable to get network interfaces: ") + String(uv_strerror(status)) - } - }} - }); - - return cb(seq, json, Post{}); - } - - JSON::Object::Entries ipv4; - JSON::Object::Entries ipv6; - JSON::Object::Entries data; - - for (int i = 0; i < count; ++i) { - uv_interface_address_t info = infos[i]; - struct sockaddr_in *addr = (struct sockaddr_in*) &info.address.address4; - char mac[18] = {0}; - snprintf(mac, 18, "%02x:%02x:%02x:%02x:%02x:%02x", - (unsigned char) info.phys_addr[0], - (unsigned char) info.phys_addr[1], - (unsigned char) info.phys_addr[2], - (unsigned char) info.phys_addr[3], - (unsigned char) info.phys_addr[4], - (unsigned char) info.phys_addr[5] - ); - - if (addr->sin_family == AF_INET) { - JSON::Object::Entries entries; - entries["internal"] = info.is_internal == 0 ? "false" : "true"; - entries["address"] = addrToIPv4(addr); - entries["mac"] = String(mac, 17); - ipv4[String(info.name)] = entries; - } - - if (addr->sin_family == AF_INET6) { - JSON::Object::Entries entries; - entries["internal"] = info.is_internal == 0 ? "false" : "true"; - entries["address"] = addrToIPv6((struct sockaddr_in6*) addr); - entries["mac"] = String(mac, 17); - ipv6[String(info.name)] = entries; - } - } - - uv_free_interface_addresses(infos, count); - - data["ipv4"] = ipv4; - data["ipv6"] = ipv6; - - auto json = JSON::Object::Entries { - {"source", "os.networkInterfaces"}, - {"data", data} - }; - - cb(seq, json, Post{}); - } - - void Core::OS::rusage ( - const String seq, - Module::Callback cb - ) { - uv_rusage_t usage; - auto status = uv_getrusage(&usage); - - if (status != 0) { - auto json = JSON::Object::Entries { - {"source", "os.rusage"}, - {"err", JSON::Object::Entries { - {"message", uv_strerror(status)} - }} - }; - - cb(seq, json, Post{}); - return; - } - - auto json = JSON::Object::Entries { - {"source", "os.rusage"}, - {"data", JSON::Object::Entries { - {"ru_maxrss", usage.ru_maxrss} - }} - }; - - cb(seq, json, Post{}); - } - - void Core::OS::uname ( - const String seq, - Module::Callback cb - ) { - uv_utsname_t uname; - auto status = uv_os_uname(&uname); - - if (status != 0) { - auto json = JSON::Object::Entries { - {"source", "os.uname"}, - {"err", JSON::Object::Entries { - {"message", uv_strerror(status)} - }} - }; - - cb(seq, json, Post{}); - return; - } - - auto json = JSON::Object::Entries { - {"source", "os.uname"}, - {"data", JSON::Object::Entries { - {"sysname", uname.sysname}, - {"release", uname.release}, - {"version", uname.version}, - {"machine", uname.machine} - }} - }; - - cb(seq, json, Post{}); - } - - void Core::OS::uptime ( - const String seq, - Module::Callback cb - ) { - double uptime; - auto status = uv_uptime(&uptime); - - if (status != 0) { - auto json = JSON::Object::Entries { - {"source", "os.uptime"}, - {"err", JSON::Object::Entries { - {"message", uv_strerror(status)} - }} - }; - - cb(seq, json, Post{}); - return; - } - - auto json = JSON::Object::Entries { - {"source", "os.uptime"}, - {"data", uptime * 1000} // in milliseconds - }; - - cb(seq, json, Post{}); - } - - void Core::OS::hrtime ( - const String seq, - Module::Callback cb - ) { - auto hrtime = uv_hrtime(); - auto bytes = toBytes(hrtime); - auto size = bytes.size(); - auto post = Post {}; - auto body = new char[size]{0}; - auto json = JSON::Object {}; - post.body = body; - post.length = size; - memcpy(body, bytes.data(), size); - cb(seq, json, post); - } - - void Core::OS::availableMemory ( - const String seq, - Module::Callback cb - ) { - auto memory = uv_get_available_memory(); - auto bytes = toBytes(memory); - auto size = bytes.size(); - auto post = Post {}; - auto body = new char[size]{0}; - auto json = JSON::Object {}; - post.body = body; - post.length = size; - memcpy(body, bytes.data(), size); - cb(seq, json, post); - } - - void Core::OS::bufferSize ( - const String seq, - uint64_t peerId, - size_t size, - int buffer, - Module::Callback cb - ) { - if (buffer == 0) { - buffer = Core::OS::SEND_BUFFER; - } else if (buffer == 1) { - buffer = Core::OS::RECV_BUFFER; - } - - this->core->dispatchEventLoop([=, this]() { - auto peer = this->core->getPeer(peerId); - - if (peer == nullptr) { - auto json = JSON::Object::Entries { - {"source", "bufferSize"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"code", "NOT_FOUND_ERR"}, - {"type", "NotFoundError"}, - {"message", "No peer with specified id"} - }} - }; - - cb(seq, json, Post{}); - return; - } - - Lock lock(peer->mutex); - auto handle = (uv_handle_t*) &peer->handle; - auto err = buffer == RECV_BUFFER - ? uv_recv_buffer_size(handle, (int *) &size) - : uv_send_buffer_size(handle, (int *) &size); - - if (err < 0) { - auto json = JSON::Object::Entries { - {"source", "bufferSize"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"code", "NOT_FOUND_ERR"}, - {"type", "NotFoundError"}, - {"message", String(uv_strerror(err))} - }} - }; - - cb(seq, json, Post{}); - return; - } - - auto json = JSON::Object::Entries { - {"source", "bufferSize"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"size", (int) size} - }} - }; - - cb(seq, json, Post{}); - }); - } - - void Core::Platform::event ( - const String seq, - const String event, - const String data, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { - // init page - if (event == "domcontentloaded") { - Lock lock(this->core->fs.mutex); - - for (auto const &tuple : this->core->fs.descriptors) { - auto desc = tuple.second; - if (desc != nullptr) { - desc->stale = true; - } else { - this->core->fs.descriptors.erase(tuple.first); - } - } - - #if !defined(__ANDROID__) - for (auto const &tuple : this->core->fs.watchers) { - auto watcher = tuple.second; - if (watcher != nullptr) { - watcher->stop(); - delete watcher; - } - } - - this->core->fs.watchers.clear(); - #endif - } - - auto json = JSON::Object::Entries { - {"source", "platform.event"}, - {"data", JSON::Object::Entries{}} - }; - - cb(seq, json, Post{}); - }); - } - - void Core::Platform::notify ( - const String seq, - const String title, - const String body, - Module::Callback cb - ) { -#if defined(__APPLE__) - auto center = [UNUserNotificationCenter currentNotificationCenter]; - auto content = [[UNMutableNotificationContent alloc] init]; - content.body = [NSString stringWithUTF8String: body.c_str()]; - content.title = [NSString stringWithUTF8String: title.c_str()]; - content.sound = [UNNotificationSound defaultSound]; - - auto trigger = [UNTimeIntervalNotificationTrigger - triggerWithTimeInterval: 1.0f - repeats: NO - ]; - - auto request = [UNNotificationRequest - requestWithIdentifier: @"LocalNotification" - content: content - trigger: trigger - ]; - - auto options = ( - UNAuthorizationOptionAlert | - UNAuthorizationOptionBadge | - UNAuthorizationOptionSound - ); - - [center requestAuthorizationWithOptions: options - completionHandler: ^(BOOL granted, NSError* error) - { - #if !__has_feature(objc_arc) - [content release]; - [trigger release]; - #endif - - if (granted) { - auto json = JSON::Object::Entries { - {"source", "platform.notify"}, - {"data", JSON::Object::Entries {}} - }; - - cb(seq, json, Post{}); - } else if (error) { - [center addNotificationRequest: request - withCompletionHandler: ^(NSError* error) - { - auto json = JSON::Object {}; - - #if !__has_feature(objc_arc) - [request release]; - #endif - - if (error) { - json = JSON::Object::Entries { - {"source", "platform.notify"}, - {"err", JSON::Object::Entries { - {"message", [error.debugDescription UTF8String]} - }} - }; - - debug("Unable to create notification: %@", error.debugDescription); - } else { - json = JSON::Object::Entries { - {"source", "platform.notify"}, - {"data", JSON::Object::Entries {}} - }; - } - - cb(seq, json, Post{}); - }]; - } else { - auto json = JSON::Object::Entries { - {"source", "platform.notify"}, - {"err", JSON::Object::Entries { - {"message", "Failed to create notification"} - }} - }; - - cb(seq, json, Post{}); - } - - if (!error || granted) { - #if !__has_feature(objc_arc) - [request release]; - #endif - } - }]; -#endif - } - - void Core::Platform::openExternal ( - const String seq, - const String value, - Module::Callback cb - ) { -#if defined(__APPLE__) - auto string = [NSString stringWithUTF8String: value.c_str()]; - auto url = [NSURL URLWithString: string]; - - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - auto app = [UIApplication sharedApplication]; - [app openURL: url options: @{} completionHandler: ^(BOOL success) { - auto json = JSON::Object {}; - - if (!success) { - json = JSON::Object::Entries { - {"source", "platform.openExternal"}, - {"err", JSON::Object::Entries { - {"message", "Failed to open external URL"} - }} - }; - } else { - json = JSON::Object::Entries { - {"source", "platform.openExternal"}, - {"data", JSON::Object::Entries {}} - }; - } - - cb(seq, json, Post{}); - }]; - #else - auto workspace = [NSWorkspace sharedWorkspace]; - auto configuration = [NSWorkspaceOpenConfiguration configuration]; - [workspace openURL: url - configuration: configuration - completionHandler: ^(NSRunningApplication *app, NSError *error) - { - auto json = JSON::Object {}; - if (error) { - json = JSON::Object::Entries { - {"source", "platform.openExternal"}, - {"err", JSON::Object::Entries { - {"message", [error.debugDescription UTF8String]} - }} - }; - } else { - json = JSON::Object::Entries { - {"source", "platform.openExternal"}, - {"data", JSON::Object::Entries {}} - }; - } - - cb(seq, json, Post{}); - }]; - #endif -#elif defined(__linux__) && !defined(__ANDROID__) - auto list = gtk_window_list_toplevels(); - auto json = JSON::Object {}; - - // initial state is a failure - json = JSON::Object::Entries { - {"source", "platform.openExternal"}, - {"err", JSON::Object::Entries { - {"message", "Failed to open external URL"} - }} - }; - - if (list != nullptr) { - for (auto entry = list; entry != nullptr; entry = entry->next) { - auto window = GTK_WINDOW(entry->data); - - if (window != nullptr && gtk_window_is_active(window)) { - auto err = (GError*) nullptr; - auto uri = value.c_str(); - auto ts = GDK_CURRENT_TIME; - - /** - * GTK may print a error in the terminal that looks like: - * - * libva error: vaGetDriverNameByIndex() failed with unknown libva error, driver_name = (null) - * - * It doesn't prevent the URI from being opened. - * See https://github.com/intel/media-driver/issues/1349 for more info - */ - auto success = gtk_show_uri_on_window(window, uri, ts, &err); - - if (success) { - json = JSON::Object::Entries { - {"source", "platform.openExternal"}, - {"data", JSON::Object::Entries {}} - }; - } else if (err != nullptr) { - json = JSON::Object::Entries { - {"source", "platform.openExternal"}, - {"err", JSON::Object::Entries { - {"message", err->message} - }} - }; - } - - break; - } - } - - g_list_free(list); - } - - cb(seq, json, Post{}); -#elif defined(_WIN32) - auto uri = value.c_str(); - ShellExecute(nullptr, "Open", uri, nullptr, nullptr, SW_SHOWNORMAL); - // TODO how to detect success here. do we care? - cb(seq, JSON::Object{}, Post{}); -#endif - } - - void Core::DNS::lookup ( - const String seq, - LookupOptions options, - Core::Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { - auto ctx = new Core::Module::RequestContext(seq, cb); - auto loop = this->core->getEventLoop(); - - struct addrinfo hints = {0}; - - if (options.family == 6) { - hints.ai_family = AF_INET6; - } else if (options.family == 4) { - hints.ai_family = AF_INET; - } else { - hints.ai_family = AF_UNSPEC; - } - - hints.ai_socktype = 0; // `0` for any - hints.ai_protocol = 0; // `0` for any - - auto resolver = new uv_getaddrinfo_t; - resolver->data = ctx; - - auto err = uv_getaddrinfo(loop, resolver, [](uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) { - auto ctx = (Core::DNS::RequestContext*) resolver->data; - - if (status < 0) { - auto result = JSON::Object::Entries { - {"source", "dns.lookup"}, - {"err", JSON::Object::Entries { - {"code", std::to_string(status)}, - {"message", String(uv_strerror(status))} - }} - }; - - ctx->cb(ctx->seq, result, Post{}); - uv_freeaddrinfo(res); - delete resolver; - delete ctx; - return; - } - - String address = ""; - - if (res->ai_family == AF_INET) { - char addr[17] = {'\0'}; - uv_ip4_name((struct sockaddr_in*)(res->ai_addr), addr, 16); - address = String(addr, 17); - } else if (res->ai_family == AF_INET6) { - char addr[40] = {'\0'}; - uv_ip6_name((struct sockaddr_in6*)(res->ai_addr), addr, 39); - address = String(addr, 40); - } - - address = address.erase(address.find('\0')); - - auto family = res->ai_family == AF_INET - ? 4 - : res->ai_family == AF_INET6 - ? 6 - : 0; - - auto result = JSON::Object::Entries { - {"source", "dns.lookup"}, - {"data", JSON::Object::Entries { - {"address", address}, - {"family", family} - }} - }; - - ctx->cb(ctx->seq, result, Post{}); - uv_freeaddrinfo(res); - delete resolver; - delete ctx; - }, options.hostname.c_str(), nullptr, &hints); - - if (err < 0) { - auto result = JSON::Object::Entries { - {"source", "dns.lookup"}, - {"err", JSON::Object::Entries { - {"code", std::to_string(err)}, - {"message", String(uv_strerror(err))} - }} - }; - - ctx->cb(seq, result, Post{}); - delete ctx; - } - }); - } - -#if defined(__linux__) && !defined(__ANDROID__) - struct UVSource { - GSource base; // should ALWAYS be first member - gpointer tag; - Core *core; - }; - - // @see https://api.gtkd.org/glib.c.types.GSourceFuncs.html +#if SOCKET_RUNTIME_PLATFORM_LINUX + // @see https://api.gtkd.org/glib.c.types.GSourceFuncs.html static GSourceFuncs loopSourceFunctions = { .prepare = [](GSource *source, gint *timeout) -> gboolean { auto core = reinterpret_cast<UVSource *>(source)->core; @@ -967,13 +228,28 @@ namespace SSC { return *timeout == 0; }, + .check = [](GSource* source) -> gboolean { + auto core = reinterpret_cast<UVSource *>(source)->core; + auto tag = reinterpret_cast<UVSource *>(source)->tag; + const auto timeout = core->getEventLoopTimeout(); + + if (timeout == 0) { + return true; + } + + const auto condition = g_source_query_unix_fd(source, tag); + return ( + ((condition & G_IO_IN) == G_IO_IN) || + ((condition & G_IO_OUT) == G_IO_OUT) + ); + }, + .dispatch = []( GSource *source, GSourceFunc callback, gpointer user_data ) -> gboolean { auto core = reinterpret_cast<UVSource *>(source)->core; - Lock lock(core->loopMutex); auto loop = core->getEventLoop(); uv_run(loop, UV_RUN_NOWAIT); return G_SOURCE_CONTINUE; @@ -987,95 +263,170 @@ namespace SSC { } didLoopInit = true; - Lock lock(loopMutex); - uv_loop_init(&eventLoop); - eventLoopAsync.data = (void *) this; - uv_async_init(&eventLoop, &eventLoopAsync, [](uv_async_t *handle) { - auto core = reinterpret_cast<SSC::Core *>(handle->data); + Lock lock(this->mutex); + uv_loop_init(&this->eventLoop); + uv_loop_set_data(&this->eventLoop, reinterpret_cast<void*>(this)); + this->eventLoopAsync.data = reinterpret_cast<void*>(this); + + uv_async_init(&this->eventLoop, &this->eventLoopAsync, [](uv_async_t *handle) { + auto core = reinterpret_cast<Core*>(handle->data); + while (true) { - std::function<void()> dispatch; - { - Lock lock(core->loopMutex); - if (core->eventLoopDispatchQueue.size() == 0) break; - dispatch = core->eventLoopDispatchQueue.front(); - core->eventLoopDispatchQueue.pop(); + Function<void()> dispatch = nullptr; + + do { + Lock lock(core->mutex); + if (core->eventLoopDispatchQueue.size() > 0) { + dispatch = core->eventLoopDispatchQueue.front(); + core->eventLoopDispatchQueue.pop(); + } + } while (0); + + if (dispatch == nullptr) { + break; } - if (dispatch != nullptr) dispatch(); + + dispatch(); } }); -#if defined(__linux__) && !defined(__ANDROID__) - GSource *source = g_source_new(&loopSourceFunctions, sizeof(UVSource)); - UVSource *uvSource = (UVSource *) source; - uvSource->core = this; - uvSource->tag = g_source_add_unix_fd( - source, - uv_backend_fd(&eventLoop), - (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR) - ); + #if SOCKET_RUNTIME_PLATFORM_LINUX + if (!this->options.dedicatedLoopThread && !this->didInitGSource) { + if (this->gsource) { + const auto id = g_source_get_id(this->gsource); + if (id > 0) { + g_source_remove(id); + } - g_source_attach(source, nullptr); -#endif + g_object_unref(this->gsource); + this->gsource = nullptr; + } + + this->gsource = g_source_new(&loopSourceFunctions, sizeof(UVSource)); + + UVSource *uvsource = reinterpret_cast<UVSource*>(gsource); + uvsource->core = this; + uvsource->tag = g_source_add_unix_fd( + this->gsource, + uv_backend_fd(&this->eventLoop), + (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR) + ); + + g_source_set_priority(this->gsource, G_PRIORITY_HIGH); + g_source_attach(this->gsource, nullptr); + this->didInitGSource = true; + } + #endif } uv_loop_t* Core::getEventLoop () { - initEventLoop(); - return &eventLoop; + this->initEventLoop(); + return &this->eventLoop; } int Core::getEventLoopTimeout () { - auto loop = getEventLoop(); + auto loop = this->getEventLoop(); uv_update_time(loop); return uv_backend_timeout(loop); } bool Core::isLoopAlive () { - return uv_loop_alive(getEventLoop()); + return uv_loop_alive(this->getEventLoop()); } - void Core::stopEventLoop() { - isLoopRunning = false; - uv_stop(&eventLoop); - #if defined(__ANDROID__) || defined(_WIN32) - if (eventLoopThread != nullptr) { - if (eventLoopThread->joinable()) { - eventLoopThread->join(); + void Core::pauseEventLoop() { + #if !SOCKET_RUNTIME_PLATFORM_LINUX + // wait for drain of event loop dispatch queue + while (true) { + Lock lock(this->mutex); + if (this->eventLoopDispatchQueue.size() == 0) { + break; + } + } + #endif + + this->isLoopRunning = false; + do { + Lock lock(this->mutex); + uv_stop(&this->eventLoop); + } while (0); + + #if !SOCKET_RUNTIME_PLATFORM_APPLE + #if SOCKET_RUNTIME_PLATFORM_LINUX + if (this->options.dedicatedLoopThread) { + #endif + if (this->eventLoopThread != nullptr) { + if (this->isPollingEventLoop && eventLoopThread->joinable()) { + this->eventLoopThread->join(); + } + + delete this->eventLoopThread; + this->eventLoopThread = nullptr; + } + #if SOCKET_RUNTIME_PLATFORM_LINUX } + #endif + #endif + } - delete eventLoopThread; - eventLoopThread = nullptr; + void Core::stopEventLoop() { + if (this->isLoopRunning) { + return; } + + this->isLoopRunning = false; + Lock lock(this->mutex); + uv_stop(&eventLoop); + #if !SOCKET_RUNTIME_PLATFORM_APPLE + #if SOCKET_RUNTIME_PLATFORM_LINUX + if (this->options.dedicatedLoopThread) { + #endif + if (eventLoopThread != nullptr) { + if (this->isPollingEventLoop && eventLoopThread->joinable()) { + eventLoopThread->join(); + } + + delete eventLoopThread; + eventLoopThread = nullptr; + } + #if SOCKET_RUNTIME_PLATFORM_LINUX + } + #endif #endif + + uv_loop_close(&eventLoop); } void Core::sleepEventLoop (int64_t ms) { if (ms > 0) { - auto timeout = getEventLoopTimeout(); + auto timeout = this->getEventLoopTimeout(); ms = timeout > ms ? timeout : ms; - std::this_thread::sleep_for(std::chrono::milliseconds(ms)); + msleep(ms); } } void Core::sleepEventLoop () { - sleepEventLoop(getEventLoopTimeout()); + this->sleepEventLoop(this->getEventLoopTimeout()); } void Core::signalDispatchEventLoop () { - initEventLoop(); - runEventLoop(); - uv_async_send(&eventLoopAsync); + Lock lock(this->mutex); + this->initEventLoop(); + this->runEventLoop(); + uv_async_send(&this->eventLoopAsync); } void Core::dispatchEventLoop (EventLoopDispatchCallback callback) { { - Lock lock(loopMutex); - eventLoopDispatchQueue.push(callback); + Lock lock(this->mutex); + this->eventLoopDispatchQueue.push(callback); } - signalDispatchEventLoop(); + this->signalDispatchEventLoop(); } - void pollEventLoop (Core *core) { + static void pollEventLoop (Core *core) { + core->isPollingEventLoop = true; auto loop = core->getEventLoop(); while (core->isLoopRunning) { @@ -1086,46 +437,72 @@ namespace SSC { } while (core->isLoopRunning && core->isLoopAlive()); } + core->isPollingEventLoop = false; core->isLoopRunning = false; } void Core::runEventLoop () { - if (isLoopRunning) { + if ( + this->isShuttingDown || + this->isLoopRunning || + this->isPaused + ) { return; } - isLoopRunning = true; + this->isLoopRunning = true; - initEventLoop(); - dispatchEventLoop([=, this]() { - initTimers(); - startTimers(); + this->initEventLoop(); + this->dispatchEventLoop([=, this]() { + this->initTimers(); + this->startTimers(); }); -#if defined(__APPLE__) - Lock lock(loopMutex); - dispatch_async(eventLoopQueue, ^{ pollEventLoop(this); }); -#elif defined(__ANDROID__) || !defined(__linux__) - Lock lock(loopMutex); + #if SOCKET_RUNTIME_PLATFORM_APPLE + Lock lock(this->mutex); + dispatch_async(this->eventLoopQueue, ^{ + pollEventLoop(this); + }); + #else + #if SOCKET_RUNTIME_PLATFORM_LINUX + if (this->options.dedicatedLoopThread) { + #endif + Lock lock(this->mutex); // clean up old thread if still running - if (eventLoopThread != nullptr) { - if (eventLoopThread->joinable()) { - eventLoopThread->join(); + if (this->eventLoopThread != nullptr) { + if (!this->isPollingEventLoop && this->eventLoopThread->joinable()) { + this->eventLoopThread->join(); } - delete eventLoopThread; - eventLoopThread = nullptr; + delete this->eventLoopThread; + this->eventLoopThread = nullptr; } - eventLoopThread = new std::thread(&pollEventLoop, this); -#endif + this->eventLoopThread = new std::thread( + &pollEventLoop, + this + ); + #if SOCKET_RUNTIME_PLATFORM_LINUX + } + #endif + #endif } - static Timer releaseWeakDescriptors = { - .timeout = 256, // in milliseconds + struct Timer { + uv_timer_t handle; + bool repeated = false; + bool started = false; + uint64_t timeout = 0; + uint64_t interval = 0; + uv_timer_cb invoke; + }; + + static Timer releaseStrongReferenceDescriptors = { + .repeated = true, + .timeout = 1024, // in milliseconds .invoke = [](uv_timer_t *handle) { auto core = reinterpret_cast<Core *>(handle->data); - Vector<uint64_t> ids; + Vector<CoreFS::ID> ids; String msg = ""; { @@ -1137,6 +514,10 @@ namespace SSC { for (auto const id : ids) { Lock lock(core->fs.mutex); + if (!core->fs.descriptors.contains(id)) { + continue; + } + auto desc = core->fs.descriptors.at(id); if (desc == nullptr) { @@ -1155,23 +536,64 @@ namespace SSC { } else { // free core->fs.descriptors.erase(id); - delete desc; } } } }; + #define RELEASE_STRONG_REFERENCE_SHARED_POINTER_BUFFERS_RESOLUTION 8 + + static Timer releaseStrongReferenceSharedPointerBuffers = { + .repeated = true, + .timeout = RELEASE_STRONG_REFERENCE_SHARED_POINTER_BUFFERS_RESOLUTION, // in milliseconds + .invoke = [](uv_timer_t *handle) { + auto core = reinterpret_cast<Core *>(handle->data); + static constexpr auto resolution = RELEASE_STRONG_REFERENCE_SHARED_POINTER_BUFFERS_RESOLUTION; + Lock lock(core->mutex); + for (int i = 0; i < core->sharedPointerBuffers.size(); ++i) { + auto entry = &core->sharedPointerBuffers[i]; + if (entry == nullptr) { + continue; + } + + // expired + if (entry->ttl <= resolution) { + entry->pointer = nullptr; + entry->ttl = 0; + if (i == core->sharedPointerBuffers.size() - 1) { + core->sharedPointerBuffers.pop_back(); + break; + } + } else { + entry->ttl = entry->ttl - resolution; + } + } + + while ( + core->sharedPointerBuffers.size() > 0 && + core->sharedPointerBuffers.back().pointer == nullptr + ) { + core->sharedPointerBuffers.pop_back(); + } + + if (core->sharedPointerBuffers.size() == 0) { + uv_timer_stop(&releaseStrongReferenceSharedPointerBuffers.handle); + } + } + }; + void Core::initTimers () { - if (didTimersInit) { + if (this->didTimersInit) { return; } - Lock lock(timersMutex); + Lock lock(this->mutex); - auto loop = getEventLoop(); + auto loop = this->getEventLoop(); - std::vector<Timer *> timersToInit = { - &releaseWeakDescriptors + Vector<Timer*> timersToInit = { + &releaseStrongReferenceDescriptors, + &releaseStrongReferenceSharedPointerBuffers }; for (const auto& timer : timersToInit) { @@ -1179,14 +601,15 @@ namespace SSC { timer->handle.data = (void *) this; } - didTimersInit = true; + this->didTimersInit = true; } void Core::startTimers () { - Lock lock(timersMutex); + Lock lock(this->mutex); - std::vector<Timer *> timersToStart = { - &releaseWeakDescriptors + Vector<Timer*> timersToStart = { + &releaseStrongReferenceDescriptors, + &releaseStrongReferenceSharedPointerBuffers }; for (const auto &timer : timersToStart) { @@ -1206,18 +629,19 @@ namespace SSC { } } - didTimersStart = false; + this->didTimersStart = true; } void Core::stopTimers () { - if (didTimersStart == false) { + if (this->didTimersStart == false) { return; } - Lock lock(timersMutex); + Lock lock(this->mutex); - std::vector<Timer *> timersToStop = { - &releaseWeakDescriptors + Vector<Timer*> timersToStop = { + &releaseStrongReferenceDescriptors, + &releaseStrongReferenceSharedPointerBuffers }; for (const auto& timer : timersToStop) { @@ -1226,6 +650,71 @@ namespace SSC { } } - didTimersStart = false; + this->didTimersStart = false; + } + + const CoreTimers::ID Core::setTimeout ( + uint64_t timeout, + const CoreTimers::TimeoutCallback& callback + ) { + return this->timers.setTimeout(timeout, callback); + } + + const CoreTimers::ID Core::setImmediate ( + const CoreTimers::ImmediateCallback& callback + ) { + return this->timers.setImmediate(callback); + } + + const CoreTimers::ID Core::setInterval ( + uint64_t interval, + const CoreTimers::IntervalCallback& callback + ) { + return this->timers.setInterval(interval, callback); + } + + bool Core::clearTimeout (const CoreTimers::ID id) { + return this->timers.clearTimeout(id); + } + + bool Core::clearImmediate (const CoreTimers::ID id) { + return this->timers.clearImmediate(id); + } + + bool Core::clearInterval (const CoreTimers::ID id) { + return this->timers.clearInterval(id); + } + + void Core::retainSharedPointerBuffer ( + SharedPointer<char[]> pointer, + unsigned int ttl + ) { + if (pointer == nullptr) { + return; + } + + Lock lock(this->mutex); + + this->sharedPointerBuffers.emplace_back(SharedPointerBuffer { + pointer, + ttl + }); + + uv_timer_again(&releaseStrongReferenceSharedPointerBuffers.handle); + } + + void Core::releaseSharedPointerBuffer (SharedPointer<char[]> pointer) { + if (pointer == nullptr) { + return; + } + + Lock lock(this->mutex); + for (auto& entry : this->sharedPointerBuffers) { + if (entry.pointer.get() == pointer.get()) { + entry.pointer = nullptr; + entry.ttl = 0; + return; + } + } } } diff --git a/src/core/core.hh b/src/core/core.hh index 7b5d65f006..f33ebfcb4a 100644 --- a/src/core/core.hh +++ b/src/core/core.hh @@ -1,785 +1,212 @@ -#ifndef SSC_CORE_CORE_H -#define SSC_CORE_CORE_H +#ifndef SOCKET_RUNTIME_CORE_CORE_H +#define SOCKET_RUNTIME_CORE_CORE_H +#include "../platform/platform.hh" + +#include "bluetooth.hh" #include "codec.hh" +#include "color.hh" #include "config.hh" #include "debug.hh" #include "env.hh" +#include "file_system_watcher.hh" +#include "headers.hh" #include "ini.hh" #include "io.hh" +#include "ip.hh" #include "json.hh" -#include "platform.hh" -#include "preload.hh" -#include "string.hh" -#include "types.hh" +#include "module.hh" +#include "options.hh" +#include "post.hh" +#include "resource.hh" +#include "socket.hh" +#include "unique_client.hh" +#include "url.hh" #include "version.hh" - -#if !defined(__ANDROID__) -#include "file_system_watcher.hh" -#endif - -#if defined(__APPLE__) -@interface SSCBluetoothController : NSObject< - CBCentralManagerDelegate, - CBPeripheralManagerDelegate, - CBPeripheralDelegate -> -@property (strong, nonatomic) CBCentralManager* centralManager; -@property (strong, nonatomic) CBPeripheralManager* peripheralManager; -@property (strong, nonatomic) CBPeripheral* bluetoothPeripheral; -@property (strong, nonatomic) NSMutableArray* peripherals; -@property (strong, nonatomic) NSMutableDictionary* services; -@property (strong, nonatomic) NSMutableDictionary* characteristics; -@property (strong, nonatomic) NSMutableDictionary* serviceMap; -- (void) startAdvertising; -- (void) startScanning; -- (id) init; -@end -#endif +#include "webview.hh" + +#include "modules/ai.hh" +#include "modules/child_process.hh" +#include "modules/conduit.hh" +#include "modules/diagnostics.hh" +#include "modules/dns.hh" +#include "modules/fs.hh" +#include "modules/geolocation.hh" +#include "modules/media_devices.hh" +#include "modules/network_status.hh" +#include "modules/notifications.hh" +#include "modules/os.hh" +#include "modules/permissions.hh" +#include "modules/platform.hh" +#include "modules/timers.hh" +#include "modules/udp.hh" namespace SSC { constexpr int EVENT_LOOP_POLL_TIMEOUT = 32; // in milliseconds - uint64_t rand64 (); - void msleep (uint64_t ms); - -#if defined(_WIN32) - String FormatError (DWORD error, String source); -#endif - - // forward class Core; + class Process; - class Headers { - public: - class Value { - public: - String string; - Value () = default; - Value (const String& value); - Value (const char* value); - Value (const Value& value); - Value (bool value); - Value (int value); - Value (float value); - Value (int64_t value); - Value (uint64_t value); - Value (double_t value); - #if defined(__APPLE__) - Value (ssize_t value); - #endif - const String& str () const; - const char * c_str() const; - - template <typename T> void set (T value) { - auto v = Value(value); - this->string = v.string; - } - }; - - class Header { - public: - String key; - Value value; - Header () = default; - Header (const Header& header); - Header (const String& key, const Value& value); - }; - - using Entries = Vector<Header>; - Entries entries; - Headers () = default; - Headers (const Headers& headers); - Headers (const String& source); - Headers (const Vector<std::map<String, Value>>& entries); - Headers (const Entries& entries); - size_t size () const; - String str () const; - - void set (const String& key, const String& value); - void set (const Header& header); - bool has (const String& name) const; - const Header& get (const String& name) const; - }; - - struct Post { - uint64_t id = 0; - uint64_t ttl = 0; - char* body = nullptr; - size_t length = 0; - String headers = ""; - String workerId = ""; - std::shared_ptr<std::function<bool(const char*, const char*, bool)>> event_stream; - std::shared_ptr<std::function<bool(const char*, size_t, bool)>> chunk_stream; - }; - - using Posts = std::map<uint64_t, Post>; - using EventLoopDispatchCallback = std::function<void()>; - - struct Timer { - uv_timer_t handle; - bool repeated = false; - bool started = false; - uint64_t timeout = 0; - uint64_t interval = 0; - uv_timer_cb invoke; - }; - - typedef enum { - PEER_TYPE_NONE = 0, - PEER_TYPE_TCP = 1 << 1, - PEER_TYPE_UDP = 1 << 2, - PEER_TYPE_MAX = 0xF - } peer_type_t; - - typedef enum { - PEER_FLAG_NONE = 0, - PEER_FLAG_EPHEMERAL = 1 << 1 - } peer_flag_t; - - typedef enum { - PEER_STATE_NONE = 0, - // general states - PEER_STATE_CLOSED = 1 << 1, - // udp states (10) - PEER_STATE_UDP_BOUND = 1 << 10, - PEER_STATE_UDP_CONNECTED = 1 << 11, - PEER_STATE_UDP_RECV_STARTED = 1 << 12, - PEER_STATE_UDP_PAUSED = 1 << 13, - // tcp states (20) - PEER_STATE_TCP_BOUND = 1 << 20, - PEER_STATE_TCP_CONNECTED = 1 << 21, - PEER_STATE_TCP_PAUSED = 1 << 13, - PEER_STATE_MAX = 1 << 0xF - } peer_state_t; - - struct LocalPeerInfo { - struct sockaddr_storage addr; - String address = ""; - String family = ""; - int port = 0; - int err = 0; - - int getsockname (uv_udp_t *socket, struct sockaddr *addr); - int getsockname (uv_tcp_t *socket, struct sockaddr *addr); - void init (uv_udp_t *socket); - void init (uv_tcp_t *socket); - void init (const struct sockaddr_storage *addr); - }; - - struct RemotePeerInfo { - struct sockaddr_storage addr; - String address = ""; - String family = ""; - int port = 0; - int err = 0; - - int getpeername (uv_udp_t *socket, struct sockaddr *addr); - int getpeername (uv_tcp_t *socket, struct sockaddr *addr); - void init (uv_udp_t *socket); - void init (uv_tcp_t *socket); - void init (const struct sockaddr_storage *addr); - }; - - /** - * A generic structure for a bound or connected peer. - */ - class Peer { - public: - struct RequestContext { - using Callback = std::function<void(int, Post)>; - Callback cb; - Peer *peer = nullptr; - RequestContext (Callback cb) { this->cb = cb; } - }; - - using UDPReceiveCallback = std::function<void( - ssize_t, - const uv_buf_t*, - const struct sockaddr* - )>; - - // uv handles - union { - uv_udp_t udp; - uv_tcp_t tcp; // XXX: FIXME - } handle; - - // sockaddr - struct sockaddr_in addr; - - // callbacks - UDPReceiveCallback receiveCallback; - std::vector<std::function<void()>> onclose; - - // instance state - uint64_t id = 0; - std::recursive_mutex mutex; - Core *core; - - struct { - struct { - bool reuseAddr = false; - bool ipv6Only = false; // @TODO - } udp; - } options; - - // peer state - LocalPeerInfo local; - RemotePeerInfo remote; - peer_type_t type = PEER_TYPE_NONE; - peer_flag_t flags = PEER_FLAG_NONE; - peer_state_t state = PEER_STATE_NONE; - - /** - * Private `Peer` class constructor - */ - Peer (Core *core, peer_type_t peerType, uint64_t peerId, bool isEphemeral); - ~Peer (); - - int init (); - int initRemotePeerInfo (); - int initLocalPeerInfo (); - void addState (peer_state_t value); - void removeState (peer_state_t value); - bool hasState (peer_state_t value); - const RemotePeerInfo* getRemotePeerInfo (); - const LocalPeerInfo* getLocalPeerInfo (); - bool isUDP (); - bool isTCP (); - bool isEphemeral (); - bool isBound (); - bool isActive (); - bool isClosing (); - bool isClosed (); - bool isConnected (); - bool isPaused (); - int bind (); - int bind (String address, int port); - int bind (String address, int port, bool reuseAddr); - int rebind (); - int connect (String address, int port); - int disconnect (); - void send ( - char *buf, - size_t size, - int port, - const String address, - Peer::RequestContext::Callback cb - ); - int recvstart (); - int recvstart (UDPReceiveCallback onrecv); - int recvstop (); - int resume (); - int pause (); - void close (); - void close (std::function<void()> onclose); - }; - - static inline String addrToIPv4 (struct sockaddr_in* sin) { - char buf[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &sin->sin_addr, buf, INET_ADDRSTRLEN); - return String(buf); - } - - static inline String addrToIPv6 (struct sockaddr_in6* sin) { - char buf[INET6_ADDRSTRLEN]; - inet_ntop(AF_INET6, &sin->sin6_addr, buf, INET6_ADDRSTRLEN); - return String(buf); - } - - static inline void parseAddress (struct sockaddr *name, int* port, char* address) { - struct sockaddr_in *name_in = (struct sockaddr_in *) name; - *port = ntohs(name_in->sin_port); - uv_ip4_name(name_in, address, 17); - } - - class Bluetooth { - public: - using SendFunction = std::function<void(const String, JSON::Any, Post)>; - using EmitFunction = std::function<void(const String, JSON::Any)>; - using Callback = std::function<void(String, JSON::Any)>; - - Core *core = nullptr; - #if defined(__APPLE__) - SSCBluetoothController* controller= nullptr; - #endif - - SendFunction sendFunction; - EmitFunction emitFunction; - - Bluetooth (); - ~Bluetooth (); - bool send (const String& seq, JSON::Any json, Post post); - bool send (const String& seq, JSON::Any json); - bool emit (const String& seq, JSON::Any json); - void startScanning (); - void publishCharacteristic ( - const String& seq, - char* bytes, - size_t size, - const String& serviceId, - const String& characteristicId, - Callback callback - ); - void subscribeCharacteristic ( - const String& seq, - const String& serviceId, - const String& characteristicId, - Callback callback - ); - void startService ( - const String& seq, - const String& serviceId, - Callback callback - ); - }; + using EventLoopDispatchCallback = Function<void()>; class Core { public: - class Module { - public: - using Callback = std::function<void(String, JSON::Any, Post)>; - struct RequestContext { - String seq; - Module::Callback cb; - RequestContext () = default; - RequestContext (String seq, Module::Callback cb) { - this->seq = seq; - this->cb = cb; - } - }; - - Core *core = nullptr; - Module (Core* core) { - this->core = core; - } - }; - - class Diagnostics : public Module { - public: - Diagnostics (auto core) : Module(core) {} - }; - - class DNS : public Module { - public: - DNS (auto core) : Module(core) {} - struct LookupOptions { - String hostname; - int family; - // TODO: support these options - // - hints - // - all - // -verbatim - }; - void lookup ( - const String seq, - LookupOptions options, - Module::Callback cb - ); - }; - - class FS : public Module { - public: - FS (auto core) : Module(core) {} - - struct Descriptor { - uint64_t id; - std::atomic<bool> retained = false; - std::atomic<bool> stale = false; - Mutex mutex; - uv_dir_t *dir = nullptr; - uv_file fd = 0; - Core *core; - - Descriptor (Core *core, uint64_t id); - bool isDirectory (); - bool isFile (); - bool isRetained (); - bool isStale (); - }; - - struct RequestContext : Module::RequestContext { - uint64_t id; - Descriptor *desc = nullptr; - uv_fs_t req; - uv_buf_t buf; - // 256 which corresponds to DirectoryHandle.MAX_BUFFER_SIZE - uv_dirent_t dirents[256]; - int offset = 0; - int result = 0; - bool recursive; // A place to stash recursive options when needed - - RequestContext () = default; - RequestContext (Descriptor *desc) - : RequestContext(desc, "", nullptr) {} - RequestContext (String seq, Callback cb) - : RequestContext(nullptr, seq, cb) {} - RequestContext (Descriptor *desc, String seq, Callback cb) { - this->id = SSC::rand64(); - this->cb = cb; - this->seq = seq; - this->desc = desc; - this->req.data = (void *) this; - this->recursive = false; - } - - ~RequestContext () { - uv_fs_req_cleanup(&this->req); - } - - void setBuffer (char* base, uint32_t len); - void freeBuffer (); - char* getBuffer (); - uint32_t getBufferSize (); - }; - - #if !defined(__ANDROID__) - std::map<uint64_t, FileSystemWatcher*> watchers; + #if !SOCKET_RUNTIME_PLATFORM_IOS + using ChildProcess = CoreChildProcess; + #endif + using DNS = CoreDNS; + using Diagnostics = CoreDiagnostics; + using FS = CoreFS; + using Conduit = CoreConduit; + using Geolocation = CoreGeolocation; + using MediaDevices = CoreMediaDevices; + using NetworkStatus = CoreNetworkStatus; + using Notifications = CoreNotifications; + using OS = CoreOS; + using Permissions = CorePermissions; + using Platform = CorePlatform; + using Timers = CoreTimers; + using UDP = CoreUDP; + using AI = CoreAI; + + struct Options : SSC::Options { + struct Features { + #if !SOCKET_RUNTIME_PLATFORM_IOS + bool useChildProcess = true; + #endif + + bool useDNS = true; + bool useFS = true; + bool useGeolocation = true; + bool useNetworkStatus = true; + bool useNotifications = true; + bool useConduit = true; + bool useOS = true; + bool usePermissions = true; + bool usePlatform = true; + bool useTimers = true; + bool useUDP = true; + bool useAI = true; + }; + + Features features; + + #if SOCKET_RUNTIME_PLATFORM_LINUX + // this is turned on in the WebKitWebProcess extension to avoid + // deadlocking the GTK loop AND WebKit WebView thread as they + // are shared and we typically "interpolate" loop execution + // with the GTK thread on the main runtime process + bool dedicatedLoopThread = false; #endif - - std::map<uint64_t, Descriptor*> descriptors; - Mutex mutex; - - Descriptor * getDescriptor (uint64_t id); - void removeDescriptor (uint64_t id); - bool hasDescriptor (uint64_t id); - - void constants (const String seq, Module::Callback cb); - void access ( - const String seq, - const String path, - int mode, - Module::Callback cb - ); - void chmod ( - const String seq, - const String path, - int mode, - Module::Callback cb - ); - void chown ( - const String seq, - const String path, - uv_uid_t uid, - uv_gid_t gid, - Module::Callback cb - ); - void lchown ( - const String seq, - const String path, - uv_uid_t uid, - uv_gid_t gid, - Module::Callback cb - ); - void close (const String seq, uint64_t id, Module::Callback cb); - void copyFile ( - const String seq, - const String src, - const String dst, - int flags, - Module::Callback cb - ); - void closedir (const String seq, uint64_t id, Module::Callback cb); - void closeOpenDescriptor ( - const String seq, - uint64_t id, - Module::Callback cb - ); - void closeOpenDescriptors (const String seq, Module::Callback cb); - void closeOpenDescriptors ( - const String seq, - bool preserveRetained, - Module::Callback cb - ); - void fstat (const String seq, uint64_t id, Module::Callback cb); - void fsync (const String seq, uint64_t id, Module::Callback cb); - void ftruncate ( - const String seq, - uint64_t id, - int64_t offset, - Module::Callback cb - ); - void getOpenDescriptors (const String seq, Module::Callback cb); - void lstat (const String seq, const String path, Module::Callback cb); - void link ( - const String seq, - const String src, - const String dest, - Module::Callback cb - ); - void symlink ( - const String seq, - const String src, - const String dest, - int flags, - Module::Callback cb - ); - void mkdir ( - const String seq, - const String path, - int mode, - bool recursive, - Module::Callback cb - ); - void readlink ( - const String seq, - const String path, - Module::Callback cb - ); - void realpath ( - const String seq, - const String path, - Module::Callback cb - ); - void open ( - const String seq, - uint64_t id, - const String path, - int flags, - int mode, - Module::Callback cb - ); - void opendir ( - const String seq, - uint64_t id, - const String path, - Module::Callback cb - ); - void read ( - const String seq, - uint64_t id, - size_t len, - size_t offset, - Module::Callback cb - ); - void readdir ( - const String seq, - uint64_t id, - size_t entries, - Module::Callback cb - ); - void retainOpenDescriptor ( - const String seq, - uint64_t id, - Module::Callback cb - ); - void rename ( - const String seq, - const String src, - const String dst, - Module::Callback cb - ); - void rmdir ( - const String seq, - const String path, - Module::Callback cb - ); - void stat ( - const String seq, - const String path, - Module::Callback cb - ); - void stopWatch ( - const String seq, - uint64_t id, - Module::Callback cb - ); - void unlink ( - const String seq, - const String path, - Module::Callback cb - ); - void watch ( - const String seq, - uint64_t id, - const String path, - Module::Callback cb - ); - void write ( - const String seq, - uint64_t id, - char *bytes, - size_t size, - size_t offset, - Module::Callback cb - ); - }; - - class OS : public Module { - public: - static const int RECV_BUFFER = 1; - static const int SEND_BUFFER = 0; - - OS (auto core) : Module(core) {} - void bufferSize ( - const String seq, - uint64_t peerId, - size_t size, - int buffer, - Module::Callback cb - ); - void cpus ( - const String seq, - Module::Callback cb - ); - void networkInterfaces (const String seq, Module::Callback cb) const; - void rusage ( - const String seq, - Module::Callback cb - ); - void uname ( - const String seq, - Module::Callback cb - ); - void uptime ( - const String seq, - Module::Callback cb - ); - void hrtime ( - const String seq, - Module::Callback cb - ); - void availableMemory ( - const String seq, - Module::Callback cb - ); }; - class Platform : public Module { - public: - Platform (auto core) : Module(core) {} - void event ( - const String seq, - const String event, - const String data, - Module::Callback cb - ); - void notify ( - const String seq, - const String title, - const String body, - Module::Callback cb - ); - void openExternal ( - const String seq, - const String value, - Module::Callback cb - ); - }; - - class UDP : public Module { - public: - UDP (auto core) : Module(core) {} - - struct BindOptions { - String address; - int port; - bool reuseAddr = false; - }; - - struct ConnectOptions { - String address; - int port; - }; - - struct SendOptions { - String address = ""; - int port = 0; - char *bytes = nullptr; - size_t size = 0; - bool ephemeral = false; - }; - - void bind ( - const String seq, - uint64_t id, - BindOptions options, - Module::Callback cb - ); - void close (const String seq, uint64_t id, Module::Callback cb); - void connect ( - const String seq, - uint64_t id, - ConnectOptions options, - Module::Callback cb - ); - void disconnect (const String seq, uint64_t id, Module::Callback cb); - void getPeerName (const String seq, uint64_t id, Module::Callback cb); - void getSockName (const String seq, uint64_t id, Module::Callback cb); - void getState (const String seq, uint64_t id, Module::Callback cb); - void readStart (const String seq, uint64_t id, Module::Callback cb); - void readStop (const String seq, uint64_t id, Module::Callback cb); - void send ( - const String seq, - uint64_t id, - SendOptions options, - Module::Callback cb - ); + struct SharedPointerBuffer { + SharedPointer<char[]> pointer; + unsigned int ttl = 0; }; + #if !SOCKET_RUNTIME_PLATFORM_IOS + ChildProcess childProcess; + #endif Diagnostics diagnostics; DNS dns; FS fs; + Conduit conduit; + Geolocation geolocation; + MediaDevices mediaDevices; + NetworkStatus networkStatus; + Notifications notifications; OS os; + Permissions permissions; Platform platform; + Timers timers; UDP udp; + AI ai; - std::shared_ptr<Posts> posts; - std::map<uint64_t, Peer*> peers; + Vector<SharedPointerBuffer> sharedPointerBuffers; + Options options = {}; + Posts posts; - std::recursive_mutex loopMutex; - std::recursive_mutex peersMutex; - std::recursive_mutex postsMutex; - std::recursive_mutex timersMutex; + Mutex mutex; - std::atomic<bool> didLoopInit = false; - std::atomic<bool> didTimersInit = false; - std::atomic<bool> didTimersStart = false; + Atomic<bool> didLoopInit = false; + Atomic<bool> didTimersInit = false; + Atomic<bool> didTimersStart = false; - std::atomic<bool> isLoopRunning = false; + Atomic<bool> isPollingEventLoop = false; + Atomic<bool> isShuttingDown = false; + Atomic<bool> isLoopRunning = false; + Atomic<bool> isPaused = true; uv_loop_t eventLoop; uv_async_t eventLoopAsync; - std::queue<EventLoopDispatchCallback> eventLoopDispatchQueue; - - #if defined(__APPLE__) - dispatch_queue_attr_t eventLoopQueueAttrs = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_SERIAL, - QOS_CLASS_DEFAULT, - -1 - ); - - dispatch_queue_t eventLoopQueue = dispatch_queue_create( - "socket.runtime.core.loop.queue", - eventLoopQueueAttrs - ); - #else - std::thread *eventLoopThread = nullptr; - #endif + Queue<EventLoopDispatchCallback> eventLoopDispatchQueue; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + dispatch_queue_attr_t eventLoopQueueAttrs = dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_SERIAL, + QOS_CLASS_DEFAULT, + -1 + ); + + dispatch_queue_t eventLoopQueue = dispatch_queue_create( + "socket.runtime.core.loop.queue", + eventLoopQueueAttrs + ); + #else + Thread *eventLoopThread = nullptr; + #endif + + #if SOCKET_RUNTIME_PLATFORM_LINUX + Atomic<bool> didInitGSource = false; + GSource* gsource = nullptr; + #endif - Core () : + Core (const Options& options) : + options(options), + #if !SOCKET_RUNTIME_PLATFORM_IOS + childProcess(this), + #endif + ai(this), diagnostics(this), + conduit(this), dns(this), fs(this), + geolocation(this), + mediaDevices(this), + networkStatus(this), + notifications(this), os(this), + permissions(this), platform(this), + timers(this), udp(this) { - this->posts = std::shared_ptr<Posts>(new Posts()); - initEventLoop(); + this->initEventLoop(); + this->resume(); + } + + Core () + : Core(Options {}) + {} + + ~Core () { + this->shutdown(); } - void resumeAllPeers (); - void pauseAllPeers (); - bool hasPeer (uint64_t id); - void removePeer (uint64_t id); - void removePeer (uint64_t id, bool autoClose); - Peer* getPeer (uint64_t id); - Peer* createPeer (peer_type_t type, uint64_t id); - Peer* createPeer (peer_type_t type, uint64_t id, bool isEphemeral); + // called when the application is shutting down + void shutdown (); + void resume (); + void pause (); + void stop (); + int logSeq{0}; + + void retainSharedPointerBuffer (SharedPointer<char[]> pointer, unsigned int ttl); + void releaseSharedPointerBuffer (SharedPointer<char[]> pointer); + + // core module post data management Post getPost (uint64_t id); bool hasPost (uint64_t id); bool hasPostBody (const char* body); @@ -793,6 +220,12 @@ namespace SSC { void initTimers (); void startTimers (); void stopTimers (); + const CoreTimers::ID setTimeout (uint64_t timeout, const CoreTimers::TimeoutCallback& callback); + const CoreTimers::ID setInterval (uint64_t interval, const CoreTimers::IntervalCallback& callback); + const CoreTimers::ID setImmediate (const CoreTimers::ImmediateCallback& callback); + bool clearTimeout (const CoreTimers::ID id); + bool clearInterval (const CoreTimers::ID id); + bool clearImmediate (const CoreTimers::ID id); // loop uv_loop_t* getEventLoop (); @@ -800,6 +233,7 @@ namespace SSC { bool isLoopAlive (); void initEventLoop (); void runEventLoop (); + void pauseEventLoop (); void stopEventLoop (); void dispatchEventLoop (EventLoopDispatchCallback dispatch); void signalDispatchEventLoop (); @@ -833,6 +267,10 @@ namespace SSC { const String& state, const String& value ); + + void setcwd (const String& cwd); + const String getcwd (); + const String getcwd_state_value (); } // SSC -#endif // SSC_CORE_CORE_H +#endif // SOCKET_RUNTIME_CORE_CORE_H diff --git a/src/core/cwd.cc b/src/core/cwd.cc new file mode 100644 index 0000000000..341010144b --- /dev/null +++ b/src/core/cwd.cc @@ -0,0 +1,27 @@ +#include "core.hh" +#include "resource.hh" + +namespace SSC { + static struct { Mutex mutex; String value = ""; } state; + + void setcwd (const String& value) { + Lock lock(state.mutex); + state.value = value; + } + + const String getcwd_state_value () { + Lock lock(state.mutex); + return state.value; + } + + const String getcwd () { + Lock lock(state.mutex); + + if (state.value.size() > 0) { + return state.value; + } + + state.value = FileResource::getResourcesPath().string(); + return state.value; + } +} diff --git a/src/core/debug.hh b/src/core/debug.hh index d588a18947..4bcec8ba66 100644 --- a/src/core/debug.hh +++ b/src/core/debug.hh @@ -1,26 +1,22 @@ -#ifndef SSC_CORE_DEBUG_H -#define SSC_CORE_DEBUG_H +#ifndef SOCKET_RUNTIME_CORE_DEBUG_H +#define SOCKET_RUNTIME_CORE_DEBUG_H #include "config.hh" -#include "platform.hh" - -#if defined(__APPLE__) -#include <TargetConditionals.h> -#include <OSLog/OSLog.h> +#if SOCKET_RUNTIME_PLATFORM_APPLE // Apple #ifndef debug -// define `ssc_os_debug` (macos/ios) +// define `socket_runtime_os_log_debug` (macos/ios) #if defined(SSC_CLI) -# define ssc_os_debug(...) +# define socket_runtime_os_log_debug(...) #else -static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; +static os_log_t SOCKET_RUNTIME_OS_LOG_DEBUG = nullptr; // wrap `os_log*` functions for global debugger -#define ssc_os_debug(format, fmt, ...) ({ \ - if (!SSC_OS_LOG_DEBUG_BUNDLE) { \ +#define socket_runtime_os_log_debug(format, fmt, ...) ({ \ + if (!SOCKET_RUNTIME_OS_LOG_DEBUG) { \ static auto userConfig = SSC::getUserConfig(); \ static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; \ - SSC_OS_LOG_DEBUG_BUNDLE = os_log_create( \ + SOCKET_RUNTIME_OS_LOG_DEBUG = os_log_create( \ bundleIdentifier.c_str(), \ "socket.runtime.debug" \ ); \ @@ -28,7 +24,7 @@ static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; \ auto string = [NSString stringWithFormat: @fmt, ##__VA_ARGS__]; \ os_log_error( \ - SSC_OS_LOG_DEBUG_BUNDLE, \ + SOCKET_RUNTIME_OS_LOG_DEBUG, \ "%{public}s", \ string.UTF8String \ ); \ @@ -38,20 +34,20 @@ static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; // define `debug(...)` macro #define debug(format, ...) ({ \ NSLog(@format, ##__VA_ARGS__); \ - ssc_os_debug("%{public}@", format, ##__VA_ARGS__); \ + socket_runtime_os_log_debug("%{public}@", format, ##__VA_ARGS__); \ }) #endif // `debug` #endif // `__APPLE__` // Linux -#if defined(__linux__) && !defined(__ANDROID__) +#if SOCKET_RUNTIME_PLATFORM_LINUX # ifndef debug # define debug(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__) # endif // `debug` #endif // `__linux__` // Android (Linux) -#if defined(__linux__) && defined(__ANDROID__) +#if SOCKET_RUNTIME_PLATFORM_ANDROID # ifndef debug # define debug(format, ...) \ __android_log_print( \ @@ -64,13 +60,10 @@ static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; #endif // `__ANDROID__` // Windows -#if defined(_WIN32) -# if defined(_WIN32) && defined(DEBUG) -# define _WIN32_DEBUG 1 -# endif // `_WIN32 && DEBUG` -# ifndef debug -# define debug(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__) -# endif // `debug` -#endif // `_WIN32` - +#if SOCKET_RUNTIME_PLATFORM_WINDOWS && defined(DEBUG) +# define _WIN32_DEBUG 1 +#endif // `_WIN32 && DEBUG` +#ifndef debug +# define debug(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__) +#endif // `debug` #endif diff --git a/src/core/env.cc b/src/core/env.cc index 750c9897d4..65bda894b2 100644 --- a/src/core/env.cc +++ b/src/core/env.cc @@ -2,17 +2,16 @@ #include "config.hh" #include "env.hh" -#include "string.hh" -namespace SSC { - bool Env::has (const char* name) { +namespace SSC::Env { + bool has (const char* name) { static auto userConfig = getUserConfig(); if (userConfig[String("env_") + name].size() > 0) { return true; } - #if defined(_WIN32) + #if SOCKET_RUNTIME_PLATFORM_WINDOWS char* value = nullptr; size_t size = 0; auto result = _dupenv_s(&value, &size, name); @@ -38,18 +37,18 @@ namespace SSC { return true; } - bool Env::has (const String& name) { + bool has (const String& name) { return has(name.c_str()); } - String Env::get (const char* name) { + String get (const char* name) { static auto userConfig = getUserConfig(); if (userConfig[String("env_") + name].size() > 0) { return userConfig[String("env_") + name]; } - #if defined(_WIN32) + #if SOCKET_RUNTIME_PLATFORM_WINDOWS char* variableValue = nullptr; std::size_t valueSize = 0; auto query = _dupenv_s(&variableValue, &valueSize, name); @@ -72,11 +71,11 @@ namespace SSC { #endif } - String Env::get (const String& name) { + String get (const String& name) { return get(name.c_str()); } - String Env::get (const String& name, const String& fallback) { + String get (const String& name, const String& fallback) { const auto value = get(name); if (value.size() == 0) { @@ -86,16 +85,16 @@ namespace SSC { return value; } - void Env::set (const String& name, const String& value) { - #if defined(_WIN32) + void set (const String& name, const String& value) { + #if SOCKET_RUNTIME_PLATFORM_WINDOWS _putenv((name + "=" + value).c_str()); #else setenv(name.c_str(), value.c_str(), 1); #endif } - void Env::set (const char* name) { - #if defined(_WIN32) + void set (const char* name) { + #if SOCKET_RUNTIME_PLATFORM_WINDOWS _putenv(name); #else auto parts = split(String(name), '='); diff --git a/src/core/env.hh b/src/core/env.hh index ac30dae014..66471a4547 100644 --- a/src/core/env.hh +++ b/src/core/env.hh @@ -1,21 +1,18 @@ -#ifndef SSC_CORE_ENV_H -#define SSC_CORE_ENV_H +#ifndef SOCKET_RUNTIME_CORE_ENV_H +#define SOCKET_RUNTIME_CORE_ENV_H -#include "types.hh" +#include "../platform/types.hh" -namespace SSC { - class Env { - public: - static bool has (const char* name); - static bool has (const String& name); +namespace SSC::Env { + bool has (const char* name); + bool has (const String& name); - static String get (const char* name); - static String get (const String& name); - static String get (const String& name, const String& fallback); + String get (const char* name); + String get (const String& name); + String get (const String& name, const String& fallback); - static void set (const String& name, const String& value); - static void set (const char* name); - }; + void set (const String& name, const String& value); + void set (const char* name); } #endif diff --git a/src/core/file_system_watcher.cc b/src/core/file_system_watcher.cc index a3ad341958..d12a68076f 100644 --- a/src/core/file_system_watcher.cc +++ b/src/core/file_system_watcher.cc @@ -108,7 +108,6 @@ namespace SSC { this->watchedPaths.clear(); if (this->ownsCore && this->core != nullptr) { - this->core->stopEventLoop(); delete this->core; } @@ -121,13 +120,14 @@ namespace SSC { return false; } - Lock lock(this->mutex); this->callback = callback; // a loop may be configured for the instance already, perhaps here or // manually by the caller if (this->core == nullptr) { - this->core = new Core(); + Core::Options options; + options.features.useNotifications = false; + this->core = new Core(options); this->ownsCore = true; } @@ -232,7 +232,6 @@ namespace SSC { } bool FileSystemWatcher::stop () { - Lock lock(this->mutex); if (!this->isRunning) { return false; } diff --git a/src/core/file_system_watcher.hh b/src/core/file_system_watcher.hh index e19673c5ea..fa73ebe7e8 100644 --- a/src/core/file_system_watcher.hh +++ b/src/core/file_system_watcher.hh @@ -1,8 +1,7 @@ -#ifndef SSC_FILE_SYSTEM_WATCHER -#define SSC_FILE_SYSTEM_WATCHER +#ifndef SOCKET_RUNTIME_FILE_SYSTEM_WATCHER_H +#define SOCKET_RUNTIME_FILE_SYSTEM_WATCHER_H -#include "platform.hh" -#include "types.hh" +#include "../platform/platform.hh" namespace SSC { class Core; @@ -66,9 +65,6 @@ namespace SSC { AtomicBool isRunning = false; Core* core = nullptr; - // thread state - Mutex mutex; - static void handleEventCallback ( EventHandle* handle, const char* filename, diff --git a/src/core/headers.cc b/src/core/headers.cc new file mode 100644 index 0000000000..5291d57358 --- /dev/null +++ b/src/core/headers.cc @@ -0,0 +1,253 @@ +#include "core.hh" + +namespace SSC { + Headers::Header::Header (const Header& header) { + this->name = toLowerCase(header.name); + this->value = header.value; + } + + Headers::Header::Header (const String& name, const Value& value) { + this->name = toLowerCase(trim(name)); + this->value = trim(value.str()); + } + + bool Headers::Header::operator == (const Header& header) const { + return this->value == header.value; + } + + bool Headers::Header::operator != (const Header& header) const { + return this->value != header.value; + } + + bool Headers::Header::operator == (const String& string) const { + return this->value.string == string; + } + + bool Headers::Header::operator != (const String& string) const { + return this->value.string != string; + } + + Headers::Headers (const String& source) { + for (const auto& entry : split(source, '\n')) { + const auto tuple = split(entry, ':'); + if (tuple.size() == 2) { + set(trim(tuple.front()), trim(tuple.back())); + } + } + } + + Headers::Headers (const Headers& headers) { + this->entries = headers.entries; + } + + Headers::Headers (const Vector<std::map<String, Value>>& entries) { + for (const auto& entry : entries) { + for (const auto& pair : entry) { + this->entries.push_back(Header { pair.first, pair.second }); + } + } + } + + Headers::Headers (const Entries& entries) { + for (const auto& entry : entries) { + this->entries.push_back(entry); + } + } + + void Headers::set (const String& name, const String& value) noexcept { + set(Header { name, value }); + } + + void Headers::set (const Header& header) noexcept { + for (auto& entry : entries) { + if (header.name == entry.name) { + entry.value = header.value; + return; + } + } + + entries.push_back(header); + } + + bool Headers::has (const String& name) const noexcept { + const auto normalizedName = toLowerCase(name); + for (const auto& header : entries) { + if (header.name == normalizedName) { + return true; + } + } + + return false; + } + + const Headers::Header Headers::get (const String& name) const noexcept { + const auto normalizedName = toLowerCase(name); + for (const auto& header : entries) { + if (header.name == normalizedName) { + return header; + } + } + + return Header {}; + } + + Headers::Header& Headers::at (const String& name) { + const auto normalizedName = toLowerCase(name); + for (auto& header : entries) { + if (header.name == normalizedName) { + return header; + } + } + + throw std::out_of_range("Header does not exist"); + } + + size_t Headers::size () const { + return this->entries.size(); + } + + String Headers::str () const { + StringStream headers; + auto remaining = this->size(); + for (const auto& entry : this->entries) { + auto parts = split(entry.name, '-'); + + std::transform( + parts.begin(), + parts.end(), + parts.begin(), + toProperCase + ); + + const auto name = join(parts, '-'); + + headers << name << ": " << entry.value.str(); + if (--remaining > 0) { + headers << "\n"; + } + } + + return headers.str(); + } + + const Headers::Iterator Headers::begin () const noexcept { + return this->entries.begin(); + } + + const Headers::Iterator Headers::end () const noexcept { + return this->entries.end(); + } + + bool Headers::erase (const String& name) noexcept { + for (int i = 0; i < this->entries.size(); ++i) { + const auto& entry = this->entries[i]; + if (entry.name == name) { + this->entries.erase(this->entries.begin() + i); + return true; + } + } + return false; + } + + const bool Headers::clear () noexcept { + if (this->entries.size() == 0) { + return false; + } + this->entries.clear(); + return true; + } + + String& Headers::operator [] (const String& name) { + if (!this->has(name)) { + this->set(name, ""); + } + + return this->at(name).value.string; + } + + const String Headers::operator [] (const String& name) const noexcept { + return this->get(name).value.string; + } + + JSON::Object Headers::json () const noexcept { + JSON::Object::Entries entries; + for (const auto& entry : this->entries) { + entries[entry.name] = entry.value.string; + } + return entries; + } + + Headers::Value::Value (const String& value) { + this->string = trim(value); + } + + Headers::Value::Value (const char* value) { + this->string = value; + } + + Headers::Value::Value (const Value& value) { + this->string = value.string; + } + + Headers::Value::Value (bool value) { + this->string = value ? "true" : "false"; + } + + Headers::Value::Value (int value) { + this->string = std::to_string(value); + } + + Headers::Value::Value (float value) { + this->string = std::to_string(value); + } + + Headers::Value::Value (int64_t value) { + this->string = std::to_string(value); + } + + Headers::Value::Value (uint64_t value) { + this->string = std::to_string(value); + } + + Headers::Value::Value (double_t value) { + this->string = std::to_string(value); + } + +#if SOCKET_RUNTIME_PLATFORM_APPLE + Headers::Value::Value (ssize_t value) { + this->string = std::to_string(value); + } +#endif + + bool Headers::Value::operator == (const Value& value) const { + return this->string == value.string; + } + + bool Headers::Value::operator != (const Value& value) const { + return this->string != value.string; + } + + bool Headers::Value::operator == (const String& string) const { + return this->string == string; + } + + bool Headers::Value::operator != (const String& string) const { + return this->string != string; + } + + const String& Headers::Value::str () const { + return this->string; + } + + const char * Headers::Value::c_str() const { + return this->str().c_str(); + } + + const String toHeaderCase (const String& source) { + Vector<String> parts; + for (const auto& entry : split(trim(source), '-')) { + parts.push_back(toProperCase(entry)); + } + return join(parts, '-'); + } +} diff --git a/src/core/headers.hh b/src/core/headers.hh new file mode 100644 index 0000000000..e358e68f56 --- /dev/null +++ b/src/core/headers.hh @@ -0,0 +1,81 @@ +#ifndef SOCKET_RUNTIME_CORE_HEADERS_H +#define SOCKET_RUNTIME_CORE_HEADERS_H + +#include "json.hh" + +namespace SSC { + class Headers { + public: + class Value { + public: + String string; + Value () = default; + Value (const String& value); + Value (const char* value); + Value (const Value& value); + Value (bool value); + Value (int value); + Value (float value); + Value (int64_t value); + Value (uint64_t value); + Value (double_t value); + #if SOCKET_RUNTIME_PLATFORM_APPLE + Value (ssize_t value); + #endif + bool operator == (const Value&) const; + bool operator != (const Value&) const; + bool operator == (const String&) const; + bool operator != (const String&) const; + + const String& str () const; + const char * c_str() const; + + template <typename T> void set (T value) { + auto v = Value(value); + this->string = v.string; + } + }; + + class Header { + public: + String name; + Value value; + Header () = default; + Header (const Header& header); + Header (const String& name, const Value& value); + bool operator == (const Header&) const; + bool operator != (const Header&) const; + bool operator == (const String&) const; + bool operator != (const String&) const; + }; + + using Entries = Vector<Header>; + using Iterator = Entries::const_iterator; + + Entries entries; + Headers () = default; + Headers (const Headers& headers); + Headers (const String& source); + Headers (const Vector<std::map<String, Value>>& entries); + Headers (const Entries& entries); + size_t size () const; + String str () const; + + void set (const String& name, const String& value) noexcept; + void set (const Header& header) noexcept; + bool has (const String& name) const noexcept; + const Header get (const String& name) const noexcept; + Header& at (const String& name); + const Iterator begin () const noexcept; + const Iterator end () const noexcept; + bool erase (const String& name) noexcept; + const bool clear () noexcept; + String& operator [] (const String& name); + const String operator [] (const String& name) const noexcept; + JSON::Object json () const noexcept; + }; + + const String toHeaderCase (const String& source); +} + +#endif diff --git a/src/core/ini.cc b/src/core/ini.cc index 404ffc434e..f454f832c9 100644 --- a/src/core/ini.cc +++ b/src/core/ini.cc @@ -1,3 +1,4 @@ +#include "../platform/platform.hh" #include "ini.hh" namespace SSC::INI { @@ -48,6 +49,12 @@ namespace SSC::INI { quoted_value = true; value = trim(value.substr(1, closing_quote_index - 1)); } + } else if (value[0] == '\'') { + closing_quote_index = value.find_first_of('\'', 1); + if (closing_quote_index != std::string::npos) { + quoted_value = true; + value = trim(value.substr(1, closing_quote_index - 1)); + } } if (!quoted_value) { @@ -66,8 +73,8 @@ namespace SSC::INI { if (key.ends_with("[]")) { key = trim(key.substr(0, key.size() - 2)); - // handle special configurations - if (key == "webview_headers") { + // handle special headers configurations + if (key.ends_with("_headers")) { // inject '\n' as headers should be stored with // new lines for each entry in the configuration value += "\n"; diff --git a/src/core/ini.hh b/src/core/ini.hh index 138a75391a..63ceebd8a4 100644 --- a/src/core/ini.hh +++ b/src/core/ini.hh @@ -1,8 +1,7 @@ -#ifndef SSC_CORE_INI_H -#define SSC_CORE_INI_H +#ifndef SOCKET_RUNTIME_CORE_INI_H +#define SOCKET_RUNTIME_CORE_INI_H -#include "string.hh" -#include "types.hh" +#include "../platform/types.hh" namespace SSC::INI { Map parse (const String& source); diff --git a/src/core/io.cc b/src/core/io.cc index 36906c8360..f2a014a961 100644 --- a/src/core/io.cc +++ b/src/core/io.cc @@ -1,5 +1,6 @@ #include <iostream> +#include "../platform/platform.hh" #include "../cli/cli.hh" #include "env.hh" #include "io.hh" @@ -12,10 +13,10 @@ namespace SSC::IO { stream << input; - // skip writing newline if running on Windows GHA CI - #if defined(_WIN32) + #if SOCKET_RUNTIME_PLATFORM_WINDOWS if (isGitHubActionsCI) { CLI::notify(); + // skip writing newline if running on Windows GHA CI return; } diff --git a/src/core/io.hh b/src/core/io.hh index fcf2778f29..bbfceabe5c 100644 --- a/src/core/io.hh +++ b/src/core/io.hh @@ -1,7 +1,7 @@ -#ifndef SSC_CORE_IO_H -#define SSC_CORE_IO_H +#ifndef SOCKET_RUNTIME_CORE_IO_H +#define SOCKET_RUNTIME_CORE_IO_H -#include "types.hh" +#include "../platform/types.hh" namespace SSC::IO { void write (const String& input, bool isErrorOutput = false); diff --git a/src/core/ip.hh b/src/core/ip.hh new file mode 100644 index 0000000000..065dd715eb --- /dev/null +++ b/src/core/ip.hh @@ -0,0 +1,25 @@ +#ifndef SOCKET_RUNTIME_CORE_IP_H +#define SOCKET_RUNTIME_CORE_IP_H + +#include "../platform/platform.hh" + +namespace SSC::IP { + static inline String addrToIPv4 (struct sockaddr_in* sin) { + char buf[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &sin->sin_addr, buf, INET_ADDRSTRLEN); + return String(buf); + } + + static inline String addrToIPv6 (struct sockaddr_in6* sin) { + char buf[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &sin->sin6_addr, buf, INET6_ADDRSTRLEN); + return String(buf); + } + + static inline void parseAddress (struct sockaddr *name, int* port, char* address) { + struct sockaddr_in *name_in = (struct sockaddr_in *) name; + *port = ntohs(name_in->sin_port); + uv_ip4_name(name_in, address, 17); + } +} +#endif diff --git a/src/core/javascript.cc b/src/core/javascript.cc index 6a4e82c528..bcdfde2f5e 100644 --- a/src/core/javascript.cc +++ b/src/core/javascript.cc @@ -5,6 +5,7 @@ namespace SSC { String createJavaScript (const String& name, const String& source) { return String( ";(async () => { \n" + " const globals = await import('socket:internal/globals'); \n" " if (!globalThis.__RUNTIME_INIT_NOW__) { \n" " await new Promise((resolve) => { \n" " globalThis.addEventListener('__runtime_init__', resolve, { \n" @@ -19,8 +20,11 @@ namespace SSC { " 'The webview environment may not be initialized correctly.' \n" " ); \n" " \n" - " " + trim(source) + "; \n" + " globals.get('RuntimeExecution').runInAsyncScope(async () => { \n" + " " + trim(source) + "; \n" + " }); \n" "})(); \n" + "undefined; \n" "//# sourceURL=" + name + " \n" ); } @@ -86,7 +90,7 @@ namespace SSC { "} \n" " \n" "if (name === 'dropin' || name === 'drop') { \n" - " return globalThis.dispatchEvent(new CustomEvent('platformdrop', { \n" + " return globalThis.dispatchEvent(new CustomEvent('platformdrop', { \n" " detail: { \n" " ...detail, \n" " files: Array.from(detail?.src || detail?.files || []) \n" @@ -94,7 +98,6 @@ namespace SSC { " } \n" " })); \n" "} \n" - " \n" "const event = new CustomEvent(name, { detail, ...options }); \n" "target.dispatchEvent(event); \n" ); @@ -160,7 +163,6 @@ namespace SSC { " detail = { data: detail }; \n" "} \n" " \n" - " \n" "const event = new CustomEvent(eventName, { detail }); \n" "globalThis.dispatchEvent(event); \n" ); diff --git a/src/core/json.cc b/src/core/json.cc index 3fb0f5a67e..b4d495ecad 100644 --- a/src/core/json.cc +++ b/src/core/json.cc @@ -1,7 +1,4 @@ -#include <regex> - #include "json.hh" -#include "string.hh" namespace SSC::JSON { Null null; @@ -11,7 +8,7 @@ namespace SSC::JSON { this->data = std::stod(string.str()); } - std::string Number::str () const { + SSC::String Number::str () const { if (this->data == 0) { return "0"; } @@ -33,42 +30,42 @@ namespace SSC::JSON { return output; } - std::string Object::str () const { - std::stringstream stream; + SSC::String Object::str () const { + SSC::StringStream stream; auto count = this->data.size(); - stream << std::string("{"); + stream << SSC::String("{"); for (const auto& tuple : this->data) { auto key = replace(tuple.first, "\"","\\\""); auto value = tuple.second.str(); - stream << std::string("\""); + stream << SSC::String("\""); stream << key; - stream << std::string("\":"); + stream << SSC::String("\":"); stream << value; if (--count > 0) { - stream << std::string(","); + stream << SSC::String(","); } } - stream << std::string("}"); + stream << SSC::String("}"); return stream.str(); } - std::string Array::str () const { - std::stringstream stream; + SSC::String Array::str () const { + SSC::StringStream stream; auto count = this->data.size(); - stream << std::string("["); + stream << SSC::String("["); for (const auto& value : this->data) { stream << value.str(); if (--count > 0) { - stream << std::string(","); + stream << SSC::String(","); } } - stream << std::string("]"); + stream << SSC::String("]"); return stream.str(); } @@ -78,119 +75,180 @@ namespace SSC::JSON { SSC::String String::str () const { auto escaped = replace(this->data, "\"", "\\\""); + escaped = replace(escaped, "\\n", "\\\\n"); return "\"" + replace(escaped, "\n", "\\n") + "\""; } Any::Any (const Null null) { - this->pointer = std::shared_ptr<void>(new Null()); + this->pointer = SharedPointer<void>(new Null()); this->type = Type::Null; } Any::Any (std::nullptr_t) { - this->pointer = std::shared_ptr<void>(new Null()); + this->pointer = SharedPointer<void>(new Null()); this->type = Type::Null; } Any::Any (const char *string) { - this->pointer = std::shared_ptr<void>(new String(string)); + this->pointer = SharedPointer<void>(new String(string)); this->type = Type::String; } Any::Any (const char string) { - this->pointer = std::shared_ptr<void>(new String(string)); + this->pointer = SharedPointer<void>(new String(string)); this->type = Type::String; } - Any::Any (const std::string string) { - this->pointer = std::shared_ptr<void>(new String(string)); + Any::Any (const SSC::Path path) + : Any(path.string()) + {} + + Any::Any (const SSC::String string) { + this->pointer = SharedPointer<void>(new String(string)); this->type = Type::String; } Any::Any (const String string) { - this->pointer = std::shared_ptr<void>(new String(string)); + this->pointer = SharedPointer<void>(new String(string)); this->type = Type::String; } Any::Any (bool boolean) { - this->pointer = std::shared_ptr<void>(new Boolean(boolean)); + this->pointer = SharedPointer<void>(new Boolean(boolean)); this->type = Type::Boolean; } Any::Any (const Boolean boolean) { - this->pointer = std::shared_ptr<void>(new Boolean(boolean)); + this->pointer = SharedPointer<void>(new Boolean(boolean)); this->type = Type::Boolean; } Any::Any (int32_t number) { - this->pointer = std::shared_ptr<void>(new Number((double) number)); + this->pointer = SharedPointer<void>(new Number((double) number)); this->type = Type::Number; } Any::Any (uint32_t number) { - this->pointer = std::shared_ptr<void>(new Number((double) number)); + this->pointer = SharedPointer<void>(new Number((double) number)); this->type = Type::Number; } Any::Any (int64_t number) { - this->pointer = std::shared_ptr<void>(new Number((double) number)); + this->pointer = SharedPointer<void>(new Number((double) number)); this->type = Type::Number; } Any::Any (uint64_t number) { - this->pointer = std::shared_ptr<void>(new Number((double) number)); + this->pointer = SharedPointer<void>(new Number((double) number)); this->type = Type::Number; } Any::Any (double number) { - this->pointer = std::shared_ptr<void>(new Number((double) number)); + this->pointer = SharedPointer<void>(new Number((double) number)); this->type = Type::Number; } -#if defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR +#if SOCKET_RUNTIME_PLATFORM_APPLE Any::Any (size_t number) { - this->pointer = std::shared_ptr<void>(new Number((double) number)); + this->pointer = SharedPointer<void>(new Number((double) number)); this->type = Type::Number; } -#endif -#if defined(__APPLE__) Any::Any (ssize_t number) { - this->pointer = std::shared_ptr<void>(new Number((double) number)); + this->pointer = SharedPointer<void>(new Number((double) number)); + this->type = Type::Number; + } +#elif !SOCKET_RUNTIME_PLATFORM_WINDOWS + Any::Any (long long number) { + this->pointer = SharedPointer<void>(new Number((double) number)); this->type = Type::Number; } #endif Any::Any (const Number number) { - this->pointer = std::shared_ptr<void>(new Number(number)); + this->pointer = SharedPointer<void>(new Number(number)); this->type = Type::Number; } Any::Any (const Object object) { - this->pointer = std::shared_ptr<void>(new Object(object)); + this->pointer = SharedPointer<void>(new Object(object)); this->type = Type::Object; } Any::Any (const Object::Entries entries) { - this->pointer = std::shared_ptr<void>(new Object(entries)); + this->pointer = SharedPointer<void>(new Object(entries)); this->type = Type::Object; } Any::Any (const Array array) { - this->pointer = std::shared_ptr<void>(new Array(array)); + this->pointer = SharedPointer<void>(new Array(array)); this->type = Type::Array; } Any::Any (const Array::Entries entries) { - this->pointer = std::shared_ptr<void>(new Array(entries)); + this->pointer = SharedPointer<void>(new Array(entries)); this->type = Type::Array; } Any::Any (const Raw source) { - this->pointer = std::shared_ptr<void>(new Raw(source)); + this->pointer = SharedPointer<void>(new Raw(source)); this->type = Type::Raw; } - std::string Any::str () const { + Any::Any (const Error error) { + this->pointer = SharedPointer<void>(new Error(error)); + this->type = Type::Error; + } + +#if SOCKET_RUNTIME_PLATFORM_APPLE + Any::Any (const NSError* error) { + this->type = Type::Error; + this->pointer = SharedPointer<void>(new Error( + error.domain.UTF8String, + error.localizedDescription.UTF8String, + error.code + )); + } +#elif SOCKET_RUNTIME_PLATFORM_LINUX + Any::Any (const GError* error) { + this->type = Type::Error; + this->pointer = SharedPointer<void>(new Error( + g_quark_to_string(error->domain), + error->message, + error->code + )); + } +#endif + + Any Any::operator[](const SSC::String& key) const { + if (this->type == Type::Object) { + return this->as<Object>()[key]; + } + throw Error("TypeError", "cannot use operator[] on non-object type", __PRETTY_FUNCTION__); + } + + Any& Any::operator[](const SSC::String& key) { + if (this->type == Type::Object) { + return this->as<Object>()[key]; + } + throw Error("TypeError", "cannot use operator[] on non-object type", __PRETTY_FUNCTION__); + } + + Any Any::operator[](const unsigned int index) const { + if (this->type == Type::Array) { + return this->as<Array>()[index]; + } + throw Error("TypeError", "cannot use operator[] on non-array type", __PRETTY_FUNCTION__); + } + + Any& Any::operator[](const unsigned int index) { + if (this->type == Type::Array) { + return this->as<Array>()[index]; + } + throw Error("TypeError", "cannot use operator[] on non-array type", __PRETTY_FUNCTION__); + } + + SSC::String Any::str () const { const auto ptr = this->pointer.get() == nullptr ? reinterpret_cast<const void*>(this) : this->pointer.get(); @@ -205,6 +263,7 @@ namespace SSC::JSON { case Type::Boolean: return reinterpret_cast<const Boolean*>(ptr)->str(); case Type::Number: return reinterpret_cast<const Number*>(ptr)->str(); case Type::String: return reinterpret_cast<const String*>(ptr)->str(); + case Type::Error: return reinterpret_cast<const Error*>(ptr)->str(); } return ""; diff --git a/src/core/json.hh b/src/core/json.hh index 1ddc29fba2..ea61725d9a 100644 --- a/src/core/json.hh +++ b/src/core/json.hh @@ -1,7 +1,7 @@ -#ifndef SSC_SOCKET_JSON_HH -#define SSC_SOCKET_JSON_HH +#ifndef SOCKET_RUNTIME_CORE_JSON_H +#define SOCKET_RUNTIME_CORE_JSON_H -#include "types.hh" +#include "../platform/platform.hh" namespace SSC::JSON { // forward @@ -15,28 +15,7 @@ namespace SSC::JSON { class String; using ObjectEntries = std::map<SSC::String, Any>; - using ArrayEntries = std::vector<Any>; - - class Error : public std::invalid_argument { - public: - SSC::String name; - SSC::String message; - SSC::String location; - - Error ( - const SSC::String& name, - const SSC::String& message, - const SSC::String& location - ) : std::invalid_argument(name + ": " + message + " (from " + location + ")") { - this->name = name; - this->message = message; - this->location = location; - } - - auto str () const { - return SSC::String(name + ": " + message + " (from " + location + ")"); - } - }; + using ArrayEntries = Vector<Any>; enum class Type { Empty = -1, @@ -47,7 +26,8 @@ namespace SSC::JSON { Boolean = 4, Number = 5, String = 6, - Raw = 7 + Raw = 7, + Error = 8 }; template <typename D, Type t> struct Value { @@ -66,10 +46,12 @@ namespace SSC::JSON { case Type::Null: return SSC::String("null"); case Type::Object: return SSC::String("object"); case Type::String: return SSC::String("string"); + case Type::Error: return SSC::String("error"); } } - bool isRaw() const { return this->type == Type::Raw; } + bool isError () const { return this->type == Type::Error; } + bool isRaw () const { return this->type == Type::Raw; } bool isArray () const { return this->type == Type::Array; } bool isBoolean () const { return this->type == Type::Boolean; } bool isNumber () const { return this->type == Type::Number; } @@ -79,6 +61,81 @@ namespace SSC::JSON { bool isEmpty () const { return this->type == Type::Empty; } }; + class Error : public std::invalid_argument, public Value<SSC::String, Type::Error> { + public: + int code = 0; + SSC::String name; + SSC::String message; + SSC::String location; + + Error () : std::invalid_argument("") {}; + Error (const Error& error) : std::invalid_argument(error.str()) { + this->code = error.code; + this->name = error.name; + this->message = error.message; + this->location = error.location; + } + + Error (Error* error) : std::invalid_argument(error->str()) { + this->code = error->code; + this->name = error->name; + this->message = error->message; + this->location = error->location; + } + + Error ( + const SSC::String& name, + const SSC::String& message, + int code = 0 + ) : std::invalid_argument(name + ": " + message) { + this->name = name; + this->code = code; + this->message = message; + } + + Error ( + const SSC::String& message + ) : std::invalid_argument(message) { + this->message = message; + } + + Error ( + const SSC::String& name, + const SSC::String& message, + const SSC::String& location + ) : std::invalid_argument(name + ": " + message + " (from " + location + ")") { + this->name = name; + this->message = message; + this->location = location; + } + + SSC::String value () const { + return this->str(); + } + + const char* what () const noexcept override { + return this->message.c_str(); + } + + const SSC::String str () const { + if (this->name.size() > 0 && this->message.size() > 0 && this->location.size() > 0) { + return this->name + ": " + this->message + " (from " + this->location + ")"; + } else if (this->name.size() > 0 && this->message.size() > 0) { + return this->name + ": " + this->message; + } else if (this->name.size() > 0 && this->location.size() > 0) { + return this->name + " (from " + this->location + ")"; + } else if (this->message.size() > 0 && this->location.size() > 0) { + return this->message + " (from " + this->location + ")"; + } else if (this->name.size() > 0) { + return this->name; + } else if (this->message.size() > 0) { + return this->message; + } + + return ""; + } + }; + class Null : public Value<std::nullptr_t, Type::Null> { public: Null () {} @@ -96,26 +153,19 @@ namespace SSC::JSON { class Any : public Value<void *, Type::Any> { public: - std::shared_ptr<void> pointer = nullptr; + SharedPointer<void> pointer = nullptr; Any () { this->pointer = nullptr; this->type = Type::Null; } - ~Any () { - this->pointer = nullptr; - this->type = Type::Any; - } - - Any (const Any &any) { - this->pointer = any.pointer; + Any (const Any& any) : pointer(any.pointer) { this->type = any.type; } - Any (Type type, std::shared_ptr<void> pointer) { + Any (Type type, SharedPointer<void> pointer) : pointer(pointer) { this->type = type; - this->pointer = pointer; } Any (std::nullptr_t); @@ -127,22 +177,34 @@ namespace SSC::JSON { Any (uint32_t); Any (int32_t); Any (double); - #if defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + #if SOCKET_RUNTIME_PLATFORM_APPLE Any (size_t); - #endif - #if defined(__APPLE__) Any (ssize_t); + #elif !SOCKET_RUNTIME_PLATFORM_WINDOWS + Any (long long); #endif Any (const Number); Any (const char); Any (const char *); Any (const SSC::String); + Any (const SSC::Path); Any (const String); Any (const Object); Any (const ObjectEntries); Any (const Array); Any (const ArrayEntries); - Any (const Raw source); + Any (const Raw); + Any (const Error); + #if SOCKET_RUNTIME_PLATFORM_APPLE + Any (const NSError*); + #elif SOCKET_RUNTIME_PLATFORM_LINUX + Any (const GError*); + #endif + + ~Any () { + this->pointer = nullptr; + this->type = Type::Any; + } SSC::String str () const; @@ -155,6 +217,11 @@ namespace SSC::JSON { throw Error("BadCastError", "cannot cast to null value", __PRETTY_FUNCTION__); } + + Any operator[](const SSC::String& key) const; + Any& operator[](const SSC::String& key); + Any operator[](const unsigned int index) const; + Any& operator[](const unsigned int index); }; class Raw : public Value<SSC::String, Type::Raw> { @@ -230,6 +297,24 @@ namespace SSC::JSON { } } + Object (const Error& error) { + if (error.name.size() > 0) { + this->set("name", error.name); + } + + if (error.message.size() > 0) { + this->set("message", error.message); + } + + if (error.location.size() > 0) { + this->set("location", error.location); + } + + if (error.code != 0) { + this->set("code", error.code); + } + } + SSC::String str () const; const Object::Entries value () const { @@ -449,6 +534,10 @@ namespace SSC::JSON { this->data = boolean.str(); } + String (const Error& error) { + this->data = error.str(); + } + SSC::String str () const; SSC::String value () const { diff --git a/src/core/module.hh b/src/core/module.hh new file mode 100644 index 0000000000..2fb95c4b9f --- /dev/null +++ b/src/core/module.hh @@ -0,0 +1,178 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_H +#define SOCKET_RUNTIME_CORE_MODULE_H + +#include "../platform/platform.hh" +#include "json.hh" +#include "post.hh" + +namespace SSC { + // forward + class Core; + class CoreModule { + public: + using Callback = Function<void(const String, JSON::Any, Post)>; + + struct RequestContext { + String seq; + CoreModule::Callback callback; + RequestContext () = default; + RequestContext (String seq, const CoreModule::Callback& callback) { + this->seq = seq; + this->callback = callback; + } + }; + + template <typename... Types> + class Observer { + public: + using Callback = Function<void(Types...)>; + uint64_t id = 0; + Callback callback; + + Observer () { + this->id = rand64(); + } + + Observer (const Observer& observer) { + this->id = observer.id; + this->callback = observer.callback; + } + + Observer (Observer&& observer) { + this->id = observer.id; + this->callback = observer.callback; + observer.id = 0; + observer.callback = nullptr; + } + + Observer (const Callback& callback) + : callback(callback) + { + this->id = rand64(); + } + + Observer (uint64_t id, const Callback& callback) + : id(id), + callback(callback) + {} + + Observer& operator = (const Observer& observer) { + this->id = observer.id; + this->callback = observer.callback; + } + + Observer& operator = (Observer&& observer) { + this->id = observer.id; + this->callback = observer.callback; + observer.id = 0; + observer.callback = nullptr; + return *this; + } + }; + + template <class Observer> + class Observers { + public: + Vector<Observer> observers; + Mutex mutex; + + bool add (const Observer& observer, const typename Observer::Callback callback = nullptr) { + Lock lock(this->mutex); + if (this->has(observer)) { + auto& existing = this->get(observer.id); + existing.callback = callback; + return true; + } else if (callback != nullptr) { + this->observers.push_back({ observer.id, callback }); + return true; + } else if (observer.callback != nullptr) { + this->observers.push_back(observer); + return true; + } + + return false; + } + + bool remove (const Observer& observer) { + Lock lock(this->mutex); + + if (this->observers.begin() == this->observers.end()) { + return false; + } + + auto iterator = this->observers.begin(); + + do { + if (iterator->id == 0) { + iterator = this->observers.erase(iterator); + } + + if (iterator->id == observer.id) { + iterator = this->observers.erase(iterator); + return true; + } + + } while ( + iterator != this->observers.end() && + ++iterator != this->observers.end() + ); + + return false; + } + + bool has (const Observer& observer) { + Lock lock(this->mutex); + for (const auto& existing : this->observers) { + if (existing.id == observer.id) { + return true; + } + } + + return false; + + } + + Observer& get (const uint64_t id) { + Lock lock(this->mutex); + for (auto& existing : this->observers) { + if (existing.id == id) { + return existing; + } + } + + throw std::out_of_range("Observer for ID does not exist"); + } + + template <typename... Types> + bool dispatch (Types... arguments) { + Lock lock(this->mutex); + + if (this->observers.begin() == this->observers.end()) { + return false; + } + + bool dispatched = false; + auto iterator = this->observers.begin(); + + while (iterator != this->observers.end()) { + if (iterator->id == 0) { + iterator = this->observers.erase(iterator); + } else if (iterator->callback != nullptr) { + iterator->callback(arguments...); + dispatched = true; + } + + iterator++; + } + + return dispatched; + } + }; + + Core *core = nullptr; + CoreModule (Core* core) + : core(core) + {} + }; +} +#endif diff --git a/src/core/modules/ai.cc b/src/core/modules/ai.cc new file mode 100644 index 0000000000..c6f121211a --- /dev/null +++ b/src/core/modules/ai.cc @@ -0,0 +1,789 @@ +#include "../core.hh" +#include "../resource.hh" +#include "ai.hh" + +#if defined (_WIN32) + #ifndef NOMINMAX + #define NOMINMAX + #endif +#endif + + +namespace SSC { + static JSON::Object::Entries ERR_AI_LLM_NOEXISTS ( + const String& source, + CoreAI::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"code", "ERR_AI_LLM_NOEXISTS"}, + {"message", "The requested LLM does not exist"} + }} + }; + } + + static JSON::Object::Entries ERR_AI_LLM_MESSAGE ( + const String& source, + CoreAI::ID id, + const String& message + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"code", "ERR_AI_LLM_MESSAGE"}, + {"message", message} + }} + }; + } + + static JSON::Object::Entries ERR_AI_LLM_EXISTS ( + const String& source, + CoreAI::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"code", "ERR_AI_LLM_EXISTS"}, + {"message", "The requested LLM already exists"} + }} + }; + } + + SharedPointer<LLM> CoreAI::getLLM (ID id) { + Lock lock(this->mutex); + + if (this->llms.contains(id)) { + return this->llms.at(id); + } + + return nullptr; + } + + bool CoreAI::hasLLM (ID id) { + Lock lock(this->mutex); + return this->llms.find(id) != this->llms.end(); + } + + void CoreAI::createLLM( + const String& seq, + ID id, + LLMOptions options, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this] { + if (this->hasLLM(id)) { + auto json = ERR_AI_LLM_EXISTS("ai.llm.create", id); + return callback(seq, json, Post{}); + } + + /* auto log = [&](String message) { + const auto json = JSON::Object::Entries { + {"source", "ai.llm.log"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", message} + }} + }; + + callback("-1", json, Post{}); + }; */ + + auto llm = std::make_shared<LLM>(options); + if (llm->err.size()) { + auto json = ERR_AI_LLM_MESSAGE("ai.llm.create", id, llm->err); + return callback(seq, json, Post{}); + return; + } + + const auto json = JSON::Object::Entries { + {"source", "ai.llm.create"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + }} + }; + + callback(seq, json, Post{}); + Lock lock(this->mutex); + this->llms.emplace(id, llm); + }); + }; + + void CoreAI::chatLLM( + const String& seq, + ID id, + String message, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this] { + if (!this->hasLLM(id)) { + auto json = ERR_AI_LLM_NOEXISTS("ai.llm.chat", id); + return callback(seq, json, Post{}); + } + + auto llm = this->getLLM(id); + + llm->chat(message, [&](auto self, auto token, auto isComplete) { + const auto json = JSON::Object::Entries { + {"source", "ai.llm.chat"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"token", encodeURIComponent(token)}, + {"complete", isComplete} + }} + }; + + callback("-1", json, Post{}); + + return isComplete; + }); + }); + }; + + void CoreAI::destroyLLM( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this] { + if (!this->hasLLM(id)) { + auto json = ERR_AI_LLM_NOEXISTS("ai.llm.destroy", id); + return callback(seq, json, Post{}); + } + + Lock lock(this->mutex); + auto llm = this->getLLM(id); + llm->stopped = true; + + while (llm->interactive) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + this->llms.erase(id); + }); + }; + + void CoreAI::stopLLM( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this] { + if (!this->hasLLM(id)) { + auto json = ERR_AI_LLM_NOEXISTS("ai.llm.stop", id); + return callback(seq, json, Post{}); + } + + auto llm = this->getLLM(id); + llm->stopped = true; // remains stopped until chat is called again. + }); + }; + + void LLM::escape(String& input) { + std::size_t input_len = input.length(); + std::size_t output_idx = 0; + + for (std::size_t input_idx = 0; input_idx < input_len; ++input_idx) { + if (input[input_idx] == '\\' && input_idx + 1 < input_len) { + switch (input[++input_idx]) { + case 'n': input[output_idx++] = '\n'; break; + case 'r': input[output_idx++] = '\r'; break; + case 't': input[output_idx++] = '\t'; break; + case '\'': input[output_idx++] = '\''; break; + case '\"': input[output_idx++] = '\"'; break; + case '\\': input[output_idx++] = '\\'; break; + case 'x': + // Handle \x12, etc + if (input_idx + 2 < input_len) { + const char x[3] = { input[input_idx + 1], input[input_idx + 2], 0 }; + char *err_p = nullptr; + const long val = std::strtol(x, &err_p, 16); + if (err_p == x + 2) { + input_idx += 2; + input[output_idx++] = char(val); + break; + } + } + // fall through + default: { + input[output_idx++] = '\\'; + input[output_idx++] = input[input_idx]; break; + } + } + } else { + input[output_idx++] = input[input_idx]; + } + } + + input.resize(output_idx); + } + + LLM::LLM (LLMOptions options) { + llama_log_set([](ggml_log_level level, const char* message, void* llm) { + debug("LLMSTATUS: %s", message); + // llm.log(message); + }, this); + + // + // set params and init the model and context + // + llama_backend_init(); + llama_numa_init(this->params.numa); + + llama_sampling_params& sparams = this->params.sparams; + this->sampling = llama_sampling_init(sparams); + + if (!this->sampling) this->err = "failed to initialize sampling subsystem"; + if (this->params.seed == LLAMA_DEFAULT_SEED) this->params.seed = time(nullptr); + + this->params.chatml = true; + this->params.verbose_prompt = false; + + if (this->params.chatml) { + this->params.prompt = "<|im_start|>system\n" + options.prompt + "<|im_end|>\n\n"; + } + + this->params.n_ctx = 2048; + + FileResource modelResource(options.path); + + if (!modelResource.exists()) { + this->err = "Unable to access the model file due to permissions"; + return; + } + + this->params.model = options.path; + + std::tie(this->model, this->ctx) = llama_init_from_gpt_params(this->params); + + if (this->ctx == nullptr) { + this->err = "Unable to create the context"; + return; + } + + if (this->model == nullptr) { + this->err = "Unable to create the model"; + return; + } + + this->embd_inp = ::llama_tokenize(this->ctx, this->params.prompt.c_str(), true, true); + + // + // create a guidance context + // + if (sparams.cfg_scale > 1.f) { + struct llama_context_params lparams = llama_context_params_from_gpt_params(this->params); + this->guidance = llama_new_context_with_model(this->model, lparams); + } + + if (this->model == nullptr) { + this->err = "unable to load model"; + return; + } + + // + // determine the capacity of the model + // + const int n_ctx_train = llama_n_ctx_train(this->model); + const int n_ctx = llama_n_ctx(this->ctx); + + if (n_ctx > n_ctx_train) { + LOG("warning: model was trained on only %d context tokens (%d specified)\n", n_ctx_train, n_ctx); + } + + this->n_ctx = n_ctx; + + if (this->guidance) { + this->guidance_inp = ::llama_tokenize(this->guidance, sparams.cfg_negative_prompt, true, true); + std::vector<llama_token> original_inp = ::llama_tokenize(ctx, params.prompt.c_str(), true, true); + original_prompt_len = original_inp.size(); + guidance_offset = (int)this->guidance_inp.size() - original_prompt_len; + } + + // + // number of tokens to keep when resetting context + // + const bool add_bos = llama_should_add_bos_token(this->model); + GGML_ASSERT(llama_add_eos_token(this->model) != 1); + + if (this->params.n_keep < 0 || this->params.n_keep > (int)this->embd_inp.size() || this->params.instruct || this->params.chatml) { + this->params.n_keep = (int)this->embd_inp.size(); + } else if (add_bos) { + this->params.n_keep += add_bos; + } + + if (this->params.instruct) { + this->params.interactive_first = true; + this->params.antiprompt.emplace_back("### Instruction:\n\n"); + } else if (this->params.chatml) { + this->params.interactive_first = true; + this->params.antiprompt.emplace_back("<|im_start|>user\n"); + } else if (this->params.conversation) { + this->params.interactive_first = true; + } + + if (params.interactive_first) { + params.interactive = true; + } + + if (params.interactive) { + if (!params.antiprompt.empty()) { + for (const auto & antiprompt : params.antiprompt) { + LOG("Reverse prompt: '%s'\n", antiprompt.c_str()); + + if (params.verbose_prompt) { + auto tmp = ::llama_tokenize(ctx, antiprompt.c_str(), false, true); + + for (int i = 0; i < (int) tmp.size(); i++) { + LOG("%6d -> '%s'\n", tmp[i], llama_token_to_piece(ctx, tmp[i]).c_str()); + } + } + } + } + + if (params.input_prefix_bos) { + LOG("Input prefix with BOS\n"); + } + + if (!params.input_prefix.empty()) { + LOG("Input prefix: '%s'\n", params.input_prefix.c_str()); + + if (params.verbose_prompt) { + auto tmp = ::llama_tokenize(ctx, params.input_prefix.c_str(), true, true); + + for (int i = 0; i < (int) tmp.size(); i++) { + LOG("%6d -> '%s'\n", tmp[i], llama_token_to_piece(ctx, tmp[i]).c_str()); + } + } + } + + if (!params.input_suffix.empty()) { + LOG("Input suffix: '%s'\n", params.input_suffix.c_str()); + + if (params.verbose_prompt) { + auto tmp = ::llama_tokenize(ctx, params.input_suffix.c_str(), false, true); + + for (int i = 0; i < (int) tmp.size(); i++) { + LOG("%6d -> '%s'\n", tmp[i], llama_token_to_piece(ctx, tmp[i]).c_str()); + } + } + } + } + + // + // initialize any anti-prompts sent in as params + // + this->antiprompt_ids.reserve(this->params.antiprompt.size()); + + for (const String& antiprompt : this->params.antiprompt) { + this->antiprompt_ids.emplace_back(::llama_tokenize(this->ctx, antiprompt.c_str(), false, true)); + } + + this->path_session = params.path_prompt_cache; + }; + + LLM::~LLM () { + llama_free(this->ctx); + llama_free(this->guidance); + llama_free_model(this->model); + llama_sampling_free(this->sampling); + llama_backend_free(); + }; + + void LLM::chat (String buffer, const Cb cb) { + this->stopped = false; + int ga_i = 0; + + const int ga_n = this->params.grp_attn_n; + const int ga_w = this->params.grp_attn_w; + + if (ga_n != 1) { + GGML_ASSERT(ga_n > 0 && "grp_attn_n must be positive"); + GGML_ASSERT(ga_w % ga_n == 0 && "grp_attn_w must be a multiple of grp_attn_n"); + } + + this->interactive = this->params.interactive_first = true; + + bool display = true; + bool is_antiprompt = false; + bool input_echo = false; + int n_remain = this->params.n_predict; + + std::vector<int> input_tokens; + this->input_tokens = &input_tokens; + + std::vector<int> output_tokens; + this->output_tokens = &output_tokens; + + std::ostringstream output_ss; + this->output_ss = &output_ss; + + std::vector<llama_token> embd; + std::vector<llama_token> embd_guidance; + + const int n_ctx = this->n_ctx; + const auto inp_pfx = ::llama_tokenize(ctx, "\n\n### Instruction:\n\n", true, true); + const auto inp_sfx = ::llama_tokenize(ctx, "\n\n### Response:\n\n", false, true); + + LOG("inp_pfx: %s\n", LOG_TOKENS_TOSTR_PRETTY(ctx, inp_pfx).c_str()); + LOG("inp_sfx: %s\n", LOG_TOKENS_TOSTR_PRETTY(ctx, inp_sfx).c_str()); + + const auto cml_pfx = ::llama_tokenize(ctx, "\n<|im_start|>user\n", true, true); + const auto cml_sfx = ::llama_tokenize(ctx, "<|im_end|>\n<|im_start|>assistant\n", false, true); + + while ((n_remain != 0 && !is_antiprompt) || this->params.interactive) { + if (!embd.empty()) { + int max_embd_size = n_ctx - 4; + + if ((int) embd.size() > max_embd_size) { + const int skipped_tokens = (int)embd.size() - max_embd_size; + embd.resize(max_embd_size); + LOG("<<input too long: skipped %d token%s>>", skipped_tokens, skipped_tokens != 1 ? "s" : ""); + } + + if (ga_n == 1) { + if (n_past + (int) embd.size() + std::max<int>(0, guidance_offset) >= n_ctx) { + if (this->params.n_predict == -2) { + LOG("\n\ncontext full and n_predict == -%d => stopping\n", this->params.n_predict); + break; + } + + const int n_left = n_past - this->params.n_keep; + const int n_discard = n_left/2; + + LOG("context full, swapping: n_past = %d, n_left = %d, n_ctx = %d, n_keep = %d, n_discard = %d\n", + n_past, n_left, n_ctx, this->params.n_keep, n_discard); + + llama_kv_cache_seq_rm (ctx, 0, this->params.n_keep, this->params.n_keep + n_discard); + llama_kv_cache_seq_add(ctx, 0, this->params.n_keep + n_discard, n_past, -n_discard); + + n_past -= n_discard; + + if (this->guidance) { + n_past_guidance -= n_discard; + } + + LOG("after swap: n_past = %d, n_past_guidance = %d\n", n_past, n_past_guidance); + LOG("embd: %s\n", LOG_TOKENS_TOSTR_PRETTY(this->ctx, embd).c_str()); + LOG("clear session path\n"); + this->path_session.clear(); + } + } else { + while (n_past >= ga_i + ga_w) { + const int ib = (ga_n*ga_i)/ga_w; + const int bd = (ga_w/ga_n)*(ga_n - 1); + const int dd = (ga_w/ga_n) - ib*bd - ga_w; + + LOG("\n"); + LOG("shift: [%6d, %6d] + %6d -> [%6d, %6d]\n", ga_i, n_past, ib*bd, ga_i + ib*bd, n_past + ib*bd); + LOG("div: [%6d, %6d] / %6d -> [%6d, %6d]\n", ga_i + ib*bd, ga_i + ib*bd + ga_w, ga_n, (ga_i + ib*bd)/ga_n, (ga_i + ib*bd + ga_w)/ga_n); + LOG("shift: [%6d, %6d] + %6d -> [%6d, %6d]\n", ga_i + ib*bd + ga_w, n_past + ib*bd, dd, ga_i + ib*bd + ga_w + dd, n_past + ib*bd + dd); + + llama_kv_cache_seq_add(ctx, 0, ga_i, n_past, ib*bd); + llama_kv_cache_seq_div(ctx, 0, ga_i + ib*bd, ga_i + ib*bd + ga_w, ga_n); + llama_kv_cache_seq_add(ctx, 0, ga_i + ib*bd + ga_w, n_past + ib*bd, dd); + + n_past -= bd; + + ga_i += ga_w/ga_n; + + LOG("\nn_past_old = %d, n_past = %d, ga_i = %d\n\n", n_past + bd, n_past, ga_i); + } + } + + if (n_session_consumed < (int) this->session_tokens.size()) { + size_t i = 0; + + for ( ; i < embd.size(); i++) { + if (embd[i] != this->session_tokens[n_session_consumed]) { + this->session_tokens.resize(n_session_consumed); + break; + } + + n_past++; + n_session_consumed++; + + if (n_session_consumed >= (int) this->session_tokens.size()) { + ++i; + break; + } + } + + if (i > 0) { + embd.erase(embd.begin(), embd.begin() + i); + } + } + + if (this->guidance) { + int input_size = 0; + llama_token * input_buf = nullptr; + + if (n_past_guidance < (int)this->guidance_inp.size()) { + embd_guidance = this->guidance_inp; + + if (embd.begin() + original_prompt_len < embd.end()) { + embd_guidance.insert(embd_guidance.end(), embd.begin() + original_prompt_len, embd.end()); + } + + input_buf = embd_guidance.data(); + input_size = embd_guidance.size(); + + LOG("guidance context: %s\n", LOG_TOKENS_TOSTR_PRETTY(ctx, embd_guidance).c_str()); + } else { + input_buf = embd.data(); + input_size = embd.size(); + } + + for (int i = 0; i < input_size; i += this->params.n_batch) { + if (this->stopped) return; + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + #undef min + #endif + + int n_eval = std::min(input_size - i, this->params.n_batch); + if (llama_decode(this->guidance, llama_batch_get_one(input_buf + i, n_eval, n_past_guidance, 0))) { + LOG("failed to eval\n"); + return; + } + + n_past_guidance += n_eval; + } + } + + for (int i = 0; i < (int) embd.size(); i += this->params.n_batch) { + int n_eval = (int) embd.size() - i; + + if (n_eval > this->params.n_batch) { + n_eval = this->params.n_batch; + } + + LOG("eval: %s\n", LOG_TOKENS_TOSTR_PRETTY(ctx, embd).c_str()); + + if (llama_decode(ctx, llama_batch_get_one(&embd[i], n_eval, n_past, 0))) { + LOG("%s : failed to eval\n", __func__); + return; + } + + n_past += n_eval; + } + + if (!embd.empty() && !this->path_session.empty()) { + this->session_tokens.insert(this->session_tokens.end(), embd.begin(), embd.end()); + n_session_consumed = this->session_tokens.size(); + } + } + + embd.clear(); + embd_guidance.clear(); + + if ((int)this->embd_inp.size() <= n_consumed && !interactive) { + const llama_token id = llama_sampling_sample(this->sampling, this->ctx, this->guidance); + llama_sampling_accept(this->sampling, this->ctx, id, true); + + LOG("last: %s\n", LOG_TOKENS_TOSTR_PRETTY(this->ctx, this->sampling->prev).c_str()); + embd.push_back(id); + + input_echo = true; + --n_remain; + + LOG("n_remain: %d\n", n_remain); + } else { + LOG("embd_inp.size(): %d, n_consumed: %d\n", (int)this->embd_inp.size(), n_consumed); + + while ((int)this->embd_inp.size() > n_consumed) { + embd.push_back(this->embd_inp[n_consumed]); + llama_sampling_accept(this->sampling, this->ctx, this->embd_inp[n_consumed], false); + + ++n_consumed; + if ((int) embd.size() >= this->params.n_batch) { + break; + } + } + } + + if (input_echo && display) { + for (auto id : embd) { + const String token_str = llama_token_to_piece(ctx, id, !this->params.conversation); + if (this->stopped) { + llama_sampling_reset(this->sampling); + this->interactive = false; + return; + } + + cb(this, token_str, false); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + if (embd.size() > 1) { + input_tokens.push_back(id); + } else { + output_tokens.push_back(id); + output_ss << token_str; + } + } + } + + if (input_echo && (int)this->embd_inp.size() == n_consumed) { + display = true; + } + + if ((int)this->embd_inp.size() <= n_consumed) { + if (!this->params.antiprompt.empty()) { + const int n_prev = 32; + const String last_output = llama_sampling_prev_str(this->sampling, this->ctx, n_prev); + + is_antiprompt = false; + + for (String & antiprompt : this->params.antiprompt) { + size_t extra_padding = this->params.interactive ? 0 : 2; + size_t search_start_pos = last_output.length() > static_cast<size_t>(antiprompt.length() + extra_padding) + ? last_output.length() - static_cast<size_t>(antiprompt.length() + extra_padding) + : 0; + + if (last_output.find(antiprompt, search_start_pos) != String::npos) { + if (this->params.interactive) { + this->interactive = true; + } + + is_antiprompt = true; + break; + } + } + + llama_token last_token = llama_sampling_last(this->sampling); + for (std::vector<llama_token> ids : antiprompt_ids) { + if (ids.size() == 1 && last_token == ids[0]) { + if (this->params.interactive) { + this->interactive = true; + } + + is_antiprompt = true; + break; + } + } + + if (is_antiprompt) { + LOG("found antiprompt: %s\n", last_output.c_str()); + } + } + + if (llama_token_is_eog(model, llama_sampling_last(this->sampling))) { + LOG("found an EOG token\n"); + + if (this->params.interactive) { + if (!this->params.antiprompt.empty()) { + const auto first_antiprompt = ::llama_tokenize(this->ctx, this->params.antiprompt.front().c_str(), false, true); + this->embd_inp.insert(this->embd_inp.end(), first_antiprompt.begin(), first_antiprompt.end()); + is_antiprompt = true; + } + + this->interactive = true; + } else if (this->params.instruct || this->params.chatml) { + this->interactive = true; + } + } + + if (n_past > 0 && this->interactive) { + LOG("waiting for user input\n"); + + if (this->params.input_prefix_bos) { + LOG("adding input prefix BOS token\n"); + this->embd_inp.push_back(llama_token_bos(this->model)); + } + + if (!this->params.input_prefix.empty() && !this->params.conversation) { + LOG("appending input prefix: '%s'\n", this->params.input_prefix.c_str()); + } + + if (buffer.length() > 1) { + if (!this->params.input_suffix.empty() && !this->params.conversation) { + LOG("appending input suffix: '%s'\n", this->params.input_suffix.c_str()); + } + + LOG("buffer: '%s'\n", buffer.c_str()); + + const size_t original_size = this->embd_inp.size(); + + if (this->params.instruct && !is_antiprompt) { + LOG("inserting instruction prefix\n"); + n_consumed = this->embd_inp.size(); + embd_inp.insert(this->embd_inp.end(), inp_pfx.begin(), inp_pfx.end()); + } + + if (this->params.chatml && !is_antiprompt) { + LOG("inserting chatml prefix\n"); + n_consumed = this->embd_inp.size(); + embd_inp.insert(this->embd_inp.end(), cml_pfx.begin(), cml_pfx.end()); + } + + if (this->params.escape) { + this->escape(buffer); + } + + const auto line_pfx = ::llama_tokenize(this->ctx, this->params.input_prefix.c_str(), false, true); + const auto line_inp = ::llama_tokenize(this->ctx, buffer.c_str(), false, this->params.interactive_specials); + const auto line_sfx = ::llama_tokenize(this->ctx, this->params.input_suffix.c_str(), false, true); + + LOG("input tokens: %s\n", LOG_TOKENS_TOSTR_PRETTY(this->ctx, line_inp).c_str()); + + this->embd_inp.insert(this->embd_inp.end(), line_pfx.begin(), line_pfx.end()); + this->embd_inp.insert(this->embd_inp.end(), line_inp.begin(), line_inp.end()); + this->embd_inp.insert(this->embd_inp.end(), line_sfx.begin(), line_sfx.end()); + + if (this->params.instruct) { + LOG("inserting instruction suffix\n"); + this->embd_inp.insert(this->embd_inp.end(), inp_sfx.begin(), inp_sfx.end()); + } + + if (this->params.chatml) { + LOG("inserting chatml suffix\n"); + this->embd_inp.insert(this->embd_inp.end(), cml_sfx.begin(), cml_sfx.end()); + } + + for (size_t i = original_size; i < this->embd_inp.size(); ++i) { + const llama_token token = this->embd_inp[i]; + this->output_tokens->push_back(token); + output_ss << llama_token_to_piece(this->ctx, token); + } + + n_remain -= line_inp.size(); + LOG("n_remain: %d\n", n_remain); + } else { + LOG("empty line, passing control back\n"); + } + + input_echo = false; + } + + if (n_past > 0) { + if (this->interactive) { + llama_sampling_reset(this->sampling); + } + this->interactive = false; + } + } + + if (llama_token_is_eog(this->model, embd.back())) { + if (this->stopped) { + llama_sampling_reset(this->sampling); + this->interactive = false; + return; + } + + if (cb(this, "", true)) return; + } + + if (this->params.interactive && n_remain <= 0 && this->params.n_predict >= 0) { + n_remain = this->params.n_predict; + this->interactive = true; + } + } + } +} diff --git a/src/core/modules/ai.hh b/src/core/modules/ai.hh new file mode 100644 index 0000000000..21561891ff --- /dev/null +++ b/src/core/modules/ai.hh @@ -0,0 +1,129 @@ +#ifndef SOCKET_RUNTIME_CORE_AI_H +#define SOCKET_RUNTIME_CORE_AI_H + +#include <llama/common/common.h> +#include <llama/llama.h> + +#include "../module.hh" + +// #include <cassert> +// #include <cinttypes> +// #include <cmath> +// #include <ctime> + +namespace SSC { + class LLM; + class Core; + + struct LLMOptions { + bool conversation = false; + bool chatml = false; + bool instruct = false; + int n_ctx = 0; + int n_keep = 0; + int n_batch = 0; + int n_threads = 0; + int n_gpu_layers = 0; + int n_predict = 0; + int grp_attn_n = 0; + int grp_attn_w = 0; + int seed = 0; + int max_tokens = 0; + int top_k = 0; + float top_p = 0.0; + float min_p = 0.0; + float tfs_z = 0.0; + float typical_p = 0.0; + float temp; + + String path; + String prompt; + String antiprompt; + }; + + class CoreAI : public CoreModule { + public: + using ID = uint64_t; + using LLMs = std::map<ID, SharedPointer<LLM>>; + + Mutex mutex; + LLMs llms; + + void chatLLM ( + const String& seq, + ID id, + String message, + const CoreModule::Callback& callback + ); + + void createLLM ( + const String& seq, + ID id, + LLMOptions options, + const CoreModule::Callback& callback + ); + + void destroyLLM ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void stopLLM ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + bool hasLLM (ID id); + SharedPointer<LLM> getLLM (ID id); + + CoreAI (Core* core) + : CoreModule(core) + {} + }; + + class LLM { + using Cb = std::function<bool(LLM*, String, bool)>; + using Logger = std::function<void(ggml_log_level, const char*, void*)>; + + gpt_params params; + llama_model* model = nullptr; + llama_context* ctx = nullptr; + llama_context* guidance = nullptr; + struct llama_sampling_context* sampling; + + std::vector<llama_token>* input_tokens = nullptr; + std::ostringstream* output_ss = nullptr; + std::vector<llama_token>* output_tokens = nullptr; + std::vector<llama_token> session_tokens; + std::vector<llama_token> embd_inp; + std::vector<llama_token> guidance_inp; + std::vector<std::vector<llama_token>> antiprompt_ids; + + String path_session = ""; + int guidance_offset = 0; + int original_prompt_len = 0; + int n_ctx = 0; + int n_past = 0; + int n_consumed = 0; + int n_session_consumed = 0; + int n_past_guidance = 0; + + public: + String err = ""; + bool stopped = false; + bool interactive = false; + + void chat (String input, const Cb cb); + void escape (String& input); + + LLM(LLMOptions options); + ~LLM(); + + static void tramp(ggml_log_level level, const char* message, void* user_data); + static Logger log; + }; +} + +#endif diff --git a/src/core/modules/child_process.cc b/src/core/modules/child_process.cc new file mode 100644 index 0000000000..4a993422ba --- /dev/null +++ b/src/core/modules/child_process.cc @@ -0,0 +1,473 @@ +#include "../headers.hh" +#include "../codec.hh" +#include "../core.hh" +#include "child_process.hh" +#include "timers.hh" + +namespace SSC { + void CoreChildProcess::shutdown () { + #if !SOCKET_RUNTIME_PLATFORM_IOS + Lock lock(this->mutex); + for (const auto& entry : this->handles) { + auto process = entry.second; + process->kill(); + process->wait(); + } + #endif + + this->handles.clear(); + } + + void CoreChildProcess::kill ( + const String& seq, + ID id, + int signal, + const CoreModule::Callback& callback + ) { + #if SOCKET_RUNTIME_PLATFORM_IOS + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotSupportedError"}, + {"message", "kill() is not supported"} + }} + }; + return callback(seq, json, Post{}); + #else + this->core->dispatchEventLoop([=, this] { + Lock lock(this->mutex); + + if (!this->handles.contains(id)) { + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotFoundError"}, + {"message", "A process with that id does not exist"} + }} + }; + + return this->core->dispatchEventLoop([=, this] () { + callback(seq, json, Post{}); + }); + } + + auto process = this->handles.at(id); + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + process->kill(); + #else + ::kill(-process->id, signal); + #endif + + return this->core->dispatchEventLoop([=, this] () { + callback(seq, JSON::Object{}, Post{}); + }); + }); + #endif + } + + void CoreChildProcess::exec ( + const String& seq, + ID id, + const Vector<String> args, + const ExecOptions options, + const CoreModule::Callback& callback + ) { + #if SOCKET_RUNTIME_PLATFORM_IOS + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotSupportedError"}, + {"message", "exec() is not supported"} + }} + }; + return callback(seq, json, Post{}); + #else + this->core->dispatchEventLoop([=, this] { + Lock lock(this->mutex); + + if (this->handles.contains(id)) { + auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", "A process with that id already exists"} + }} + }; + + return this->core->dispatchEventLoop([=, this] () { + callback(seq, json, Post{}); + }); + } + + SharedPointer<Process> process = nullptr; + CoreTimers::ID timer; + + const auto command = args.size() > 0 ? args.at(0) : String(""); + const auto argv = join(args.size() > 1 ? Vector<String>{ args.begin() + 1, args.end() } : Vector<String>{}, " "); + + auto stdoutBuffer = new StringStream; + auto stderrBuffer = new StringStream; + + const auto onStdout = [=](const String& output) mutable { + if (!options.allowStdout || output.size() == 0) { + return; + } + + if (stdoutBuffer != nullptr) { + *stdoutBuffer << String(output); + } + }; + + const auto onStderr = [=](const String& output) mutable { + if (!options.allowStderr || output.size() == 0) { + return; + } + + if (stderrBuffer != nullptr) { + *stderrBuffer << String(output); + } + }; + + const auto onExit = [=, this](const String& output) mutable { + if (timer > 0) { + this->core->clearTimeout(timer); + } + + this->core->dispatchEventLoop([=, this] () mutable { + Lock lock(this->mutex); + if (this->handles.contains(id)) { + auto process = this->handles.at(id); + const auto pid = process->id; + const auto code = process->wait(); + const auto json = JSON::Object::Entries { + {"source", "child_process.exec"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"pid", std::to_string(pid)}, + {"stdout", encodeURIComponent(stdoutBuffer->str())}, + {"stderr", encodeURIComponent(stderrBuffer->str())}, + {"code", code} + }} + }; + + delete stdoutBuffer; + delete stderrBuffer; + + stdoutBuffer = nullptr; + stderrBuffer = nullptr; + + return this->core->dispatchEventLoop([=, this] () { + callback(seq, json, Post{}); + }); + + this->handles.erase(id); + } + }); + }; + + process.reset(new Process( + command, + argv, + options.env, + options.cwd, + onStdout, + onStderr, + onExit, + false + )); + + this->handles.insert_or_assign(id, process); + + const auto pid = process->open(); + + if (options.timeout > 0) { + timer = this->core->setTimeout(options.timeout, [=, this] () mutable { + Lock lock(this->mutex); + const auto json = JSON::Object::Entries { + {"source", "child_process.exec"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"pid", std::to_string(pid)}, + {"stdout", encodeURIComponent(stdoutBuffer->str())}, + {"stderr", encodeURIComponent(stderrBuffer->str())}, + {"code", "ETIMEDOUT"} + }} + }; + + this->core->dispatchEventLoop([=, this] { + callback(seq, json, Post{}); + }); + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + process->kill(); + #else + ::kill(-process->id, options.killSignal); + #endif + + delete stdoutBuffer; + delete stderrBuffer; + + stdoutBuffer = nullptr; + stderrBuffer = nullptr; + + this->handles.erase(id); + }); + } + }); + #endif + } + + void CoreChildProcess::spawn ( + const String& seq, + ID id, + const Vector<String> args, + const SpawnOptions options, + const CoreModule::Callback& callback + ) { + #if SOCKET_RUNTIME_PLATFORM_IOS + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotSupportedError"}, + {"message", "spawn() is not supported"} + }} + }; + return callback(seq, json, Post{}); + #else + this->core->dispatchEventLoop([=, this] { + Lock lock(this->mutex); + + if (this->handles.contains(id)) { + auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", "A process with that id already exists"} + }} + }; + + return this->core->dispatchEventLoop([=, this] () { + callback(seq, json, Post{}); + }); + } + + SharedPointer<Process> process = nullptr; + + const auto command = args.size() > 0 ? args.at(0) : String(""); + const auto argv = join(args.size() > 1 ? Vector<String>{ args.begin() + 1, args.end() } : Vector<String>{}, " "); + + const auto onStdout = [=](const String& output) { + if (!options.allowStdout || output.size() == 0) { + return; + } + + const auto bytes = new char[output.size()]{0}; + const auto headers = Headers {{ + {"content-type" ,"application/octet-stream"}, + {"content-length", (int) output.size()} + }}; + + memcpy(bytes, output.c_str(), output.size()); + + Post post; + post.id = rand64(); + post.body.reset(bytes); + post.length = (int) output.size(); + post.headers = headers.str(); + + const auto json = JSON::Object::Entries { + {"source", "child_process.spawn"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"source", "stdout"} + }} + }; + + callback("-1", json, post); + }; + + const auto onStderr = [=](const String& output) { + if (!options.allowStderr || output.size() == 0) { + return; + } + + const auto bytes = new char[output.size()]{0}; + const auto headers = Headers {{ + {"content-type" ,"application/octet-stream"}, + {"content-length", (int) output.size()} + }}; + + memcpy(bytes, output.c_str(), output.size()); + + Post post; + post.id = rand64(); + post.body.reset(bytes); + post.length = (int) output.size(); + post.headers = headers.str(); + + const auto json = JSON::Object::Entries { + {"source", "child_process.spawn"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"source", "stderr"} + }} + }; + + callback("-1", json, post); + }; + + const auto onExit = [=, this](const String& output) { + const auto code = output.size() > 0 ? std::stoi(output) : 0; + const auto json = JSON::Object::Entries { + {"source", "child_process.spawn"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"status", "exit"}, + {"code", code} + }} + }; + + callback("-1", json, Post{}); + + this->core->dispatchEventLoop([=, this] { + SharedPointer<Process> process = nullptr; + do { + Lock lock(this->mutex); + if (!this->handles.contains(id)) { + return; + } + + process = this->handles.at(id); + } while (0); + + const auto code = process->wait(); + + this->core->dispatchEventLoop([=, this] { + const auto json = JSON::Object::Entries { + {"source", "child_process.spawn"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"status", "close"}, + {"code", code} + }} + }; + + callback("-1", json, Post{}); + + Lock lock(this->mutex); + this->handles.erase(id); + }); + }); + }; + + process.reset(new Process( + command, + argv, + options.env, + options.cwd, + onStdout, + onStderr, + onExit, + options.allowStdin + )); + + this->handles.insert_or_assign(id, process); + + const auto pid = process->open(); + const auto json = JSON::Object::Entries { + {"source", "child_process.spawn"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"pid", std::to_string(pid)} + }} + }; + + return this->core->dispatchEventLoop([=, this] () { + callback(seq, json, Post{}); + }); + }); + #endif + } + + void CoreChildProcess::write ( + const String& seq, + ID id, + SharedPointer<char[]> buffer, + size_t size, + const CoreModule::Callback& callback + ) { + #if SOCKET_RUNTIME_PLATFORM_IOS + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotSupportedError"}, + {"message", "write() is not supported"} + }} + }; + return callback(seq, json, Post{}); + #else + this->core->dispatchEventLoop([=, this] { + Lock lock(this->mutex); + + if (!this->handles.contains(id)) { + auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotFoundError"}, + {"message", "A process with that id does not exist"} + }} + }; + + return callback(seq, json, Post{}); + } + + bool didWrite = false; + + auto process = this->handles.at(id); + + if (!process->openStdin) { + auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotSupportedError"}, + {"message", "Child process stdin is not opened"} + }} + }; + + callback(seq, json, Post{}); + return; + } + + try { + didWrite = process->write(buffer, size); + } catch (std::exception& e) { + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"message", e.what()} + }} + }; + + callback(seq, json, Post{}); + return; + } + + if (!didWrite) { + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", process->lastWriteStatus != 0 ? "ErrnoError" : "InternalError"}, + {"message", process->lastWriteStatus != 0 ? strerror(process->lastWriteStatus) : "Failed to write to child process"} + }} + }; + + callback(seq, json, Post{}); + return; + } + + callback(seq, JSON::Object{}, Post{}); + return; + }); + #endif + } +} diff --git a/src/core/modules/child_process.hh b/src/core/modules/child_process.hh new file mode 100644 index 0000000000..498547397e --- /dev/null +++ b/src/core/modules/child_process.hh @@ -0,0 +1,75 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_CHILD_PROCESS_H +#define SOCKET_RUNTIME_CORE_MODULE_CHILD_PROCESS_H + +#include "../module.hh" +#include "../process.hh" + +namespace SSC { + class Core; + class CoreChildProcess : public CoreModule { + public: + using ID = uint64_t; + using Handles = std::map<ID, SharedPointer<Process>>; + + struct SpawnOptions { + String cwd; + const Vector<String> env; + bool allowStdin = true; + bool allowStdout = true; + bool allowStderr = true; + }; + + struct ExecOptions { + String cwd; + const Vector<String> env; + bool allowStdout = true; + bool allowStderr = true; + uint64_t timeout = 0; + #if SOCKET_RUNTIME_PLATFORM_WINDOWS || SOCKET_RUNTIME_PLATFORM_IOS + int killSignal = 0; // unused + #else + int killSignal = SIGTERM; + #endif + }; + + Handles handles; + Mutex mutex; + + CoreChildProcess (Core* core) + : CoreModule(core) + {} + + void shutdown (); + void exec ( + const String& seq, + ID id, + const Vector<String> args, + const ExecOptions options, + const CoreModule::Callback& callback + ); + + void spawn ( + const String& seq, + ID id, + const Vector<String> args, + const SpawnOptions options, + const CoreModule::Callback& callback + ); + + void kill ( + const String& seq, + ID id, + int signal, + const CoreModule::Callback& callback + ); + + void write ( + const String& seq, + ID id, + SharedPointer<char[]> buffer, + size_t size, + const CoreModule::Callback& callback + ); + }; +} +#endif diff --git a/src/core/modules/conduit.cc b/src/core/modules/conduit.cc new file mode 100644 index 0000000000..aa5e289c70 --- /dev/null +++ b/src/core/modules/conduit.cc @@ -0,0 +1,749 @@ +#include "../../app/app.hh" +#include "../core.hh" +#include "../codec.hh" + +#include "conduit.hh" + +#define SHA_DIGEST_LENGTH 20 + +namespace SSC { + static constexpr char WS_GUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + static SharedPointer<char[]> vectorToSharedPointer (const Vector<uint8_t>& vector) { + const auto size = vector.size(); + const auto data = vector.data(); + const auto pointer = std::make_shared<char[]>(size); + std::memcpy(pointer.get(), data, size); + return std::move(pointer); + } + + CoreConduit::~CoreConduit () { + this->stop(); + } + + Vector<uint8_t> CoreConduit::encodeMessage ( + const CoreConduit::Options& options, + const Vector<uint8_t>& payload + ) { + Vector<uint8_t> encodedMessage; + + Vector<std::pair<String, String>> sortedOptions(options.begin(), options.end()); + std::sort(sortedOptions.begin(), sortedOptions.end()); + + // the total number of options + encodedMessage.push_back(static_cast<uint8_t>(sortedOptions.size())); + + for (const auto& option : sortedOptions) { + const String& key = option.first; + const String& value = option.second; + + // ket length + encodedMessage.push_back(static_cast<uint8_t>(key.length())); + + // key + encodedMessage.insert(encodedMessage.end(), key.begin(), key.end()); + + // value length + uint16_t valueLength = static_cast<uint16_t>(value.length()); + encodedMessage.push_back(static_cast<uint8_t>((valueLength >> 8) & 0xFF)); + encodedMessage.push_back(static_cast<uint8_t>(valueLength & 0xFF)); + + // value + encodedMessage.insert(encodedMessage.end(), value.begin(), value.end()); + } + + // payload length + uint16_t bodyLength = static_cast<uint16_t>(payload.size()); + encodedMessage.push_back(static_cast<uint8_t>((bodyLength >> 8) & 0xFF)); + encodedMessage.push_back(static_cast<uint8_t>(bodyLength & 0xFF)); + + // actual payload + encodedMessage.insert(encodedMessage.end(), payload.begin(), payload.end()); + return encodedMessage; + } + + CoreConduit::EncodedMessage CoreConduit::decodeMessage ( + const Vector<uint8_t>& data + ) { + EncodedMessage message; + + if (data.size() < 1) return message; + + size_t offset = 0; + + uint8_t numOpts = data[offset++]; + + for (uint8_t i = 0; i < numOpts; ++i) { + if (offset >= data.size()) continue; + + // len + uint8_t keyLength = data[offset++]; + if (offset + keyLength > data.size()) continue; + // key + String key(data.begin() + offset, data.begin() + offset + keyLength); + offset += keyLength; + + if (offset + 2 > data.size()) continue; + + // len + uint16_t valueLength = (data[offset] << 8) | data[offset + 1]; + offset += 2; + + if (offset + valueLength > data.size()) continue; + + // val + String value(data.begin() + offset, data.begin() + offset + valueLength); + offset += valueLength; + + message.options[key] = value; + } + + if (offset + 2 > data.size()) return message; + + // len + uint16_t bodyLength = (data[offset] << 8) | data[offset + 1]; + offset += 2; + + if (offset + bodyLength > data.size()) return message; + + // body + message.payload = Vector<uint8_t>(data.begin() + offset, data.begin() + offset + bodyLength); + + return message; + } + + bool CoreConduit::has (uint64_t id) { + Lock lock(this->mutex); + return this->clients.find(id) != this->clients.end(); + } + + CoreConduit::Client::~Client () { + auto handle = reinterpret_cast<uv_handle_t*>(&this->handle); + + if (frameBuffer) { + delete [] frameBuffer; + } + + if ( + this->isClosing == false && + this->isClosed == false && + handle->loop != nullptr && + !uv_is_closing(handle) && + uv_is_active(handle) + ) { + // XXX(@jwerle): figure out a gracefull close + } + } + + CoreConduit::Client* CoreConduit::get (uint64_t id) { + Lock lock(this->mutex); + const auto it = clients.find(id); + + if (it != clients.end()) { + return it->second; + } + + return nullptr; + } + + void CoreConduit::handshake (CoreConduit::Client *client, const char *request) { + String requestString(request); + + auto reqeol = requestString.find("\r\n"); + if (reqeol == String::npos) return; // nope + + std::istringstream iss(requestString.substr(0, reqeol)); + String method; + String url; + String version; + + iss >> method >> url >> version; + + URL parsed(url); + Headers headers(request); + auto keyHeader = headers["Sec-WebSocket-Key"]; + + if (keyHeader.empty()) { + // debug("Sec-WebSocket-Key is required but missing."); + return; + } + + auto parts = split(parsed.pathname, "/"); + uint64_t socketId = 0; + uint64_t clientId = 0; + + try { + socketId = std::stoull(trim(parts[1])); + } catch (...) { + // debug("Unable to parse socket id"); + } + + try { + clientId = std::stoull(trim(parts[2])); + } catch (...) { + // debug("Unable to parse client id"); + } + + client->id = socketId; + client->clientId = clientId; + + do { + Lock lock(this->mutex); + if (this->clients.contains(socketId)) { + auto existingClient = this->clients.at(socketId); + this->clients.erase(socketId); + existingClient->close([existingClient]() { + if (existingClient->isClosed) { + delete existingClient; + } + }); + } + + this->clients.emplace(socketId, client); + + // std::cout << "added client " << this->clients.size() << std::endl; + } while (0); + + // debug("Received key: %s", keyHeader.c_str()); + + String acceptKey = keyHeader + WS_GUID; + char calculatedHash[SHA_DIGEST_LENGTH]; + shacalc(acceptKey.c_str(), calculatedHash); + + size_t base64_len; + unsigned char *base64_accept_key = base64Encode((unsigned char*)calculatedHash, SHA_DIGEST_LENGTH, &base64_len); + + // debug("Generated Accept Key: %s\n", base64_accept_key); // Debugging statement + + StringStream oss; + oss + << "HTTP/1.1 101 Switching Protocols\r\n" + << "Upgrade: websocket\r\n" + << "Connection: Upgrade\r\n" + << "Sec-WebSocket-Accept: " << base64_accept_key << "\r\n\r\n"; + + const auto response = oss.str(); + const auto size = response.size(); + const auto data = new char[size]{0}; + memcpy(data, response.c_str(), size); + + const auto buf = uv_buf_init(data, size); + auto req = new uv_write_t; + + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(req), + data + ); + + uv_write( + req, + reinterpret_cast<uv_stream_t*>(&client->handle), + &buf, + 1, + [](uv_write_t *req, int status) { + const auto data = reinterpret_cast<char*>( + uv_handle_get_data(reinterpret_cast<uv_handle_t*>(req)) + ); + + if (data != nullptr) { + delete [] data; + } + + delete req; + } + ); + + free(base64_accept_key); + + client->isHandshakeDone = 1; + } + + void CoreConduit::processFrame ( + Client* client, + const char* frame, + ssize_t len + ) { + if (len < 2) return; // Frame too short to be valid + + unsigned char *data = (unsigned char *)frame; + int fin = data[0] & 0x80; + int opcode = data[0] & 0x0F; + int mask = data[1] & 0x80; + uint64_t payload_len = data[1] & 0x7F; + size_t pos = 2; + + if (opcode == 0x08) { + client->close(); + return; + } + + if (payload_len == 126) { + if (len < 4) return; // too short to be valid + payload_len = (data[2] << 8) | data[3]; + pos = 4; + } else if (payload_len == 127) { + if (len < 10) return; // too short to be valid + payload_len = 0; + for (int i = 0; i < 8; i++) { + payload_len = (payload_len << 8) | data[2 + i]; + } + pos = 10; + } + + if (!mask) return; + if (len < pos + 4 + payload_len) return; // too short to be valid + + unsigned char masking_key[4]; + memcpy(masking_key, data + pos, 4); + pos += 4; + + if (payload_len > client->frameBufferSize) { + // TODO(@jwerle): refactor to drop usage of `realloc()` + client->frameBuffer = static_cast<unsigned char *>(realloc(client->frameBuffer, payload_len)); + client->frameBufferSize = payload_len; + } + + unsigned char *payload = client->frameBuffer; + + for (uint64_t i = 0; i < payload_len; i++) { + payload[i] = data[pos + i] ^ masking_key[i % 4]; + } + + pos += payload_len; + + Vector<uint8_t> vec(payload, payload + payload_len); + auto decoded = this->decodeMessage(vec); + + if (!decoded.has("route")) { + // TODO(@jwerle,@heapwolf): handle this + return; + } + + /* const auto uri = URL::Builder() + .setProtocol("ipc") + .setHostname(decoded.pluck("route")) + .setSearchParam("id", client->id) + .setSearchParams(decoded.getOptionsAsMap()) + .build(); */ + + std::stringstream ss; + + ss << "ipc://"; + ss << decoded.pluck("route"); + ss << "/?id=" << std::to_string(client->id); + + for (auto& option : decoded.getOptionsAsMap()) { + auto key = option.first; + auto value = option.second == "value" ? encodeURIComponent(option.second) : option.second; + ss << "&" << key << "=" << value; + } + + const auto uri = ss.str(); + const auto app = App::sharedApplication(); + const auto window = app->windowManager.getWindowForClient({ .id = client->clientId }); + if (window != nullptr) { + const auto bytes = vectorToSharedPointer(decoded.payload); + const auto size = decoded.payload.size(); + app->dispatch([app, window, uri, client, bytes, size]() { + const auto invoked = window->bridge.router.invoke( + uri, + bytes, + size + ); + + if (!invoked) { + // TODO(@jwerle,@heapwolf): handle this + // debug("there was a problem invoking the router %s", ss.str().c_str()); + } + }); + } else { + // TODO(@jwerle,@heapwolf): handle this + } + } + + struct ClientWriteContext { + CoreConduit::Client* client = nullptr; + const Function<void()> callback = nullptr; + }; + + bool CoreConduit::Client::emit ( + const CoreConduit::Options& options, + SharedPointer<char[]> bytes, + size_t length, + int opcode, + const Function<void()> callback + ) { + auto handle = reinterpret_cast<uv_handle_t*>(&this->handle); + + if (!this->conduit) { + return false; + } + + Vector<uint8_t> payload(bytes.get(), bytes.get() + length); + Vector<uint8_t> encodedMessage; + + try { + encodedMessage = this->conduit->encodeMessage(options, payload); + } catch (const std::exception& e) { + debug("CoreConduit::Client: Error - Failed to encode message payload: %s", e.what()); + return false; + } + + this->conduit->core->dispatchEventLoop([this, opcode, callback, handle, encodedMessage = std::move(encodedMessage)]() mutable { + size_t encodedLength = encodedMessage.size(); + Vector<unsigned char> frame; + + if (encodedLength <= 125) { + frame.resize(2 + encodedLength); + frame[1] = static_cast<unsigned char>(encodedLength); + } else if (encodedLength <= 65535) { + frame.resize(4 + encodedLength); + frame[1] = 126; + frame[2] = (encodedLength >> 8) & 0xFF; + frame[3] = encodedLength & 0xFF; + } else { + frame.resize(10 + encodedLength); + frame[1] = 127; + for (int i = 0; i < 8; i++) { + frame[9 - i] = (encodedLength >> (i * 8)) & 0xFF; + } + } + + frame[0] = 0x80 | opcode; // FIN and opcode 2 (binary) + std::memcpy(frame.data() + frame.size() - encodedLength, encodedMessage.data(), encodedLength); + + auto req = new uv_write_t; + auto data = reinterpret_cast<char*>(frame.data()); + auto size = frame.size(); + + uv_buf_t buf = uv_buf_init(data, size); + + if (callback != nullptr) { + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(req), + new ClientWriteContext { this, callback } + ); + } else { + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(req), + nullptr + ); + } + + uv_write( + req, + reinterpret_cast<uv_stream_t*>(&this->handle), + &buf, + 1, + [](uv_write_t* req, int status) { + const auto data = uv_handle_get_data(reinterpret_cast<uv_handle_t*>(req)); + const auto context = static_cast<ClientWriteContext*>(data); + + delete req; + + if (context != nullptr) { + context->client->conduit->core->dispatchEventLoop([=]() mutable { + context->callback(); + delete context; + }); + } + } + ); + }); + + return true; + } + + struct ClientCloseContext { + CoreConduit::Client* client = nullptr; + CoreConduit::Client::CloseCallback callback = nullptr; + }; + + void CoreConduit::Client::close (const CloseCallback& callback) { + auto handle = reinterpret_cast<uv_handle_t*>(&this->handle); + + if (this->isClosing || this->isClosed || !uv_is_active(handle)) { + if (callback != nullptr) { + this->conduit->core->dispatchEventLoop(callback); + } + return; + } + + this->isClosing = true; + + if (handle->loop == nullptr || uv_is_closing(handle)) { + this->isClosed = true; + this->isClosing = false; + + if (uv_is_active(handle)) { + uv_read_stop(reinterpret_cast<uv_stream_t*>(handle)); + } + + if (callback != nullptr) { + this->conduit->core->dispatchEventLoop(callback); + } + return; + } + + const auto closeHandle = [=, this]() { + if (uv_is_closing(handle)) { + this->isClosed = true; + this->isClosing = false; + + if (callback != nullptr) { + this->conduit->core->dispatchEventLoop(callback); + } + return; + } + + do { + Lock lock(this->conduit->mutex); + if (this->conduit->clients.contains(this->id)) { + conduit->clients.erase(this->id); + } + } while (0); + + auto shutdown = new uv_shutdown_t; + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(shutdown), + new ClientCloseContext { this, callback } + ); + + if (uv_is_active(handle)) { + uv_read_stop(reinterpret_cast<uv_stream_t*>(handle)); + } + + uv_shutdown(shutdown, reinterpret_cast<uv_stream_t*>(handle), [](uv_shutdown_t* shutdown, int status) { + auto data = uv_handle_get_data(reinterpret_cast<uv_handle_t*>(shutdown)); + auto context = static_cast<ClientCloseContext*>(data); + auto client = context->client; + auto handle = reinterpret_cast<uv_handle_t*>(&client->handle); + auto callback = context->callback; + + delete shutdown; + + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(handle), + context + ); + + uv_close(handle, [](uv_handle_t* handle) { + auto data = uv_handle_get_data(reinterpret_cast<uv_handle_t*>(handle)); + auto context = static_cast<ClientCloseContext*>(data); + auto client = context->client; + auto conduit = client->conduit; + auto callback = context->callback; + + client->isClosed = true; + client->isClosing = false; + + delete context; + + if (callback != nullptr) { + client->conduit->core->dispatchEventLoop(callback); + } + }); + }); + }; + + this->emit({}, vectorToSharedPointer({ 0x00}), 1, 0x08, closeHandle); + } + + void CoreConduit::start (const StartCallback& callback) { + if (this->isActive() || this->isStarting) { + if (callback != nullptr) { + this->core->dispatchEventLoop(callback); + } + return; + } + + auto loop = this->core->getEventLoop(); + + this->isStarting = true; + + auto ip = Env::get("SOCKET_RUNTIME_CONDUIT_HOSTNAME", "0.0.0.0"); + auto port = this->port.load(); + + if (Env::has("SOCKET_RUNTIME_CONDUIT_PORT")) { + try { + port = std::stoi(Env::get("SOCKET_RUNTIME_CONDUIT_PORT")); + } catch (...) {} + } + + uv_ip4_addr(ip.c_str(), port, &this->addr); + uv_tcp_init(loop, &this->socket); + uv_tcp_bind( + &this->socket, + reinterpret_cast<const struct sockaddr*>(&this->addr), + 0 + ); + + struct sockaddr_in sockname; + int namelen = sizeof(sockname); + uv_tcp_getsockname( + &this->socket, + reinterpret_cast<struct sockaddr *>(&sockname), + reinterpret_cast<int*>(&namelen) + ); + + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(&this->socket), + this + ); + + this->port = ntohs(sockname.sin_port); + const auto result = uv_listen(reinterpret_cast<uv_stream_t*>(&this->socket), 128, [](uv_stream_t* stream, int status) { + if (status < 0) { + // debug("New connection error %s\n", uv_strerror(status)); + return; + } + + auto data = uv_handle_get_data(reinterpret_cast<uv_handle_t*>(stream)); + auto conduit = static_cast<CoreConduit*>(data); + auto client = new CoreConduit::Client(conduit); + auto loop = uv_handle_get_loop(reinterpret_cast<uv_handle_t*>(stream)); + + uv_tcp_init( + loop, + &client->handle + ); + + const auto accepted = uv_accept( + stream, + reinterpret_cast<uv_stream_t*>(&client->handle) + ); + + if (accepted != 0) { + return uv_close( + reinterpret_cast<uv_handle_t *>(&client->handle), + nullptr + ); + } + + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(&client->handle), + client + ); + + uv_read_start( + reinterpret_cast<uv_stream_t*>(&client->handle), + [](uv_handle_t* handle, size_t size, uv_buf_t* buf) { + buf->base = new char[size]{0}; + buf->len = size; + }, + [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { + auto data = uv_handle_get_data(reinterpret_cast<uv_handle_t*>(stream)); + auto client = static_cast<CoreConduit::Client*>(data); + + if (client && !client->isClosing && nread > 0) { + if (client->isHandshakeDone) { + client->conduit->processFrame(client, buf->base, nread); + } else { + client->conduit->handshake(client, buf->base); + } + } else if (nread < 0) { + if (nread != UV_EOF) { + // debug("Read error %s\n", uv_err_name(nread)); + } + + if (!client->isClosing && !client->isClosed) { + client->close([client]() { + if (client->isClosed) { + delete client; + } + }); + } + } + + if (buf->base) { + delete [] buf->base; + } + } + ); + }); + + if (result) { + debug("CoreConduit: Listen error %s\n", uv_strerror(result)); + } + + this->isStarting = false; + + if (callback != nullptr) { + this->core->dispatchEventLoop(callback); + } + } + + void CoreConduit::stop () { + if (!this->isActive()) { + return; + } + + this->core->dispatchEventLoop([this]() { + Lock lock(this->mutex); + auto handle = reinterpret_cast<uv_handle_t*>(&this->socket); + const auto closeHandle = [=, this] () { + if (!uv_is_closing(handle)) { + auto shutdown = new uv_shutdown_t; + uv_handle_set_data( + reinterpret_cast<uv_handle_t*>(shutdown), + this + ); + + uv_shutdown( + shutdown, + reinterpret_cast<uv_stream_t*>(&this->socket), + [](uv_shutdown_t* shutdown, int status) { + auto data = uv_handle_get_data(reinterpret_cast<uv_handle_t*>(shutdown)); + auto conduit = reinterpret_cast<CoreConduit*>(data); + + delete shutdown; + + conduit->core->dispatchEventLoop([=]() { + uv_close( + reinterpret_cast<uv_handle_t*>(&conduit->socket), + nullptr + ); + }); + } + ); + } + }; + + if (this->clients.size() == 0) { + if (!uv_is_closing(handle)) { + closeHandle(); + } + } else { + for (const auto& entry : this->clients) { + auto client = entry.second; + + client->close([=, this] () { + Lock lock(this->mutex); + + for (auto& entry : this->clients) { + if (entry.second && !entry.second->isClosed) { + return; + } + } + + for (auto& entry : this->clients) { + delete entry.second; + } + + this->clients.clear(); + closeHandle(); + }); + } + } + }); + } + + bool CoreConduit::isActive () { + Lock lock(this->mutex); + return ( + this->port > 0 && + uv_is_active(reinterpret_cast<uv_handle_t*>(&this->socket)) && + !uv_is_closing(reinterpret_cast<uv_handle_t*>(&this->socket)) + ); + } +} diff --git a/src/core/modules/conduit.hh b/src/core/modules/conduit.hh new file mode 100644 index 0000000000..5754f87c8a --- /dev/null +++ b/src/core/modules/conduit.hh @@ -0,0 +1,132 @@ +#ifndef SOCKET_RUNTIME_CORE_CONDUIT_H +#define SOCKET_RUNTIME_CORE_CONDUIT_H + +#include "../module.hh" +#include "timers.hh" +#include <iostream> + +namespace SSC { + class Core; + + class CoreConduit : public CoreModule { + public: + using Options = std::unordered_map<String, String>; + using StartCallback = Function<void()>; + + struct EncodedMessage { + Options options; + Vector<uint8_t> payload; + + inline String get (const String& key) const { + const auto it = options.find(key); + if (it != options.end()) { + return it->second; + } + return ""; + } + + inline bool has (const String& key) const { + const auto it = options.find(key); + if (it != options.end()) { + return true; + } + return false; + } + + inline String pluck (const String& key) { + auto it = options.find(key); + if (it != options.end()) { + String value = it->second; + options.erase(it); + return value; + } + return ""; + } + + inline Map getOptionsAsMap () { + Map map; + + for (const auto& pair : this->options) { + map.insert(pair); + } + return map; + } + }; + + class Client { + public: + using CloseCallback = Function<void()>; + using ID = uint64_t; + + // client state + ID id = 0; + ID clientId = 0; + Atomic<bool> isHandshakeDone = false; + Atomic<bool> isClosing = false; + Atomic<bool> isClosed = false; + + // uv state + uv_tcp_t handle; + uv_buf_t buffer; + uv_stream_t* stream = nullptr; + + // websocket frame buffer state + unsigned char *frameBuffer = nullptr; + size_t frameBufferSize = 0; + + CoreConduit* conduit = nullptr; + + Client (CoreConduit* conduit) + : conduit(conduit), + id(0), + clientId(0), + isHandshakeDone(0) + {} + + ~Client (); + + bool emit ( + const CoreConduit::Options& options, + SharedPointer<char[]> payload, + size_t length, + int opcode = 2, + const Function<void()> callback = nullptr + ); + + void close (const CloseCallback& callback = nullptr); + }; + + // state + std::map<uint64_t, Client*> clients; + Atomic<bool> isStarting = false; + Atomic<int> port = 0; + Mutex mutex; + + CoreConduit (Core* core) : CoreModule(core) {}; + ~CoreConduit (); + + // codec + EncodedMessage decodeMessage (const Vector<uint8_t>& data); + Vector<uint8_t> encodeMessage ( + const Options& options, + const Vector<uint8_t>& payload + ); + + // client access + bool has (uint64_t id); + CoreConduit::Client* get (uint64_t id); + + // lifecycle + void start (const StartCallback& callback = nullptr); + void stop (); + bool isActive (); + + private: + uv_tcp_t socket; + struct sockaddr_in addr; + + void handshake (Client* client, const char *request); + void processFrame (Client* client, const char *frame, ssize_t size); + }; +} +#endif diff --git a/src/core/modules/diagnostics.cc b/src/core/modules/diagnostics.cc new file mode 100644 index 0000000000..6e63024ade --- /dev/null +++ b/src/core/modules/diagnostics.cc @@ -0,0 +1,237 @@ +#include "../core.hh" +#include "diagnostics.hh" + +namespace SSC { + JSON::Object CoreDiagnostics::Diagnostic::Handles::json () const { + auto ids = JSON::Array {}; + for (const auto id : this->ids) { + ids.push(std::to_string(id)); + } + return JSON::Object::Entries { + {"count", this->count}, + {"ids", ids} + }; + } + + void CoreDiagnostics::query (const QueryCallback& callback) const { + this->core->dispatchEventLoop([=, this] () mutable { + auto query = QueryDiagnostic {}; + + // posts diagnostics + do { + Lock lock(this->core->mutex); + query.posts.handles.count = this->core->posts.size(); + for (const auto& entry : this->core->posts) { + query.posts.handles.ids.push_back(entry.first); + } + } while (0); + + #if !SOCKET_RUNTIME_PLATFORM_IOS + // `childProcess` diagnostics + do { + Lock lock(this->core->childProcess.mutex); + query.childProcess.handles.count = this->core->childProcess.handles.size(); + for (const auto& entry : this->core->childProcess.handles) { + query.childProcess.handles.ids.push_back(entry.first); + } + } while (0); + #endif + + // ai diagnostics + do { + Lock lock(this->core->ai.mutex); + query.ai.llm.handles.count = this->core->ai.llms.size(); + for (const auto& entry : this->core->ai.llms) { + query.ai.llm.handles.ids.push_back(entry.first); + } + } while (0); + + // fs diagnostics + do { + Lock lock(this->core->fs.mutex); + query.fs.descriptors.handles.count = this->core->fs.descriptors.size(); + query.fs.watchers.handles.count = this->core->fs.watchers.size(); + + for (const auto& entry : this->core->fs.descriptors) { + query.fs.descriptors.handles.ids.push_back(entry.first); + } + + for (const auto& entry : this->core->fs.watchers) { + query.fs.watchers.handles.ids.push_back(entry.first); + } + } while (0); + + // timers diagnostics + do { + Lock lock(this->core->timers.mutex); + for (const auto& entry : this->core->timers.handles) { + const auto id = entry.first; + const auto& timer = entry.second; + if (timer->type == CoreTimers::Timer::Type::Timeout) { + query.timers.timeout.handles.count++; + query.timers.timeout.handles.ids.push_back(entry.first); + } else if (timer->type == CoreTimers::Timer::Type::Interval) { + query.timers.interval.handles.count++; + query.timers.interval.handles.ids.push_back(entry.first); + } else if (timer->type == CoreTimers::Timer::Type::Immediate) { + query.timers.immediate.handles.count++; + query.timers.immediate.handles.ids.push_back(entry.first); + } + } + } while (0); + + // udp + do { + Lock lock(this->core->udp.mutex); + query.udp.handles.count = this->core->udp.sockets.size(); + for (const auto& entry : this->core->udp.sockets) { + query.udp.handles.ids.push_back(entry.first); + } + } while (0); + + // conduit + do { + Lock lock(this->core->conduit.mutex); + query.conduit.handles.count = this->core->conduit.clients.size(); + query.conduit.isActive = this->core->conduit.isActive(); + for (const auto& entry : this->core->conduit.clients) { + query.conduit.handles.ids.push_back(entry.first); + } + } while (0); + + // uv + do { + Lock lock(this->core->mutex); + uv_metrics_info(&this->core->eventLoop, &query.uv.metrics); + query.uv.idleTime = uv_metrics_idle_time(&this->core->eventLoop); + query.uv.handles.count = this->core->eventLoop.active_handles; + query.uv.activeRequests = this->core->eventLoop.active_reqs.count; + } while (0); + + callback(query); + }); + } + + void CoreDiagnostics::query ( + const String& seq, + const CoreModule::Callback& callback + ) const { + this->core->dispatchEventLoop([=, this] () { + this->query([=] (const auto query) { + auto json = JSON::Object::Entries { + {"source", "diagnostics.query"}, + {"data", query.json()} + }; + callback(seq, json, Post {}); + }); + }); + } + + JSON::Object CoreDiagnostics::UVDiagnostic::json () const { + return JSON::Object::Entries { + {"metrics", JSON::Object::Entries { + {"loopCount", this->metrics.loop_count}, + {"events", this->metrics.events}, + {"eventsWaiting", this->metrics.events_waiting}, + }}, + {"idleTime", this->idleTime}, + {"activeRequests", this->activeRequests}, + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::PostsDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::ChildProcessDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::AIDiagnostic::json () const { + return JSON::Object::Entries { + {"llm", this->llm.json()} + }; + } + + JSON::Object CoreDiagnostics::AIDiagnostic::LLMDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::FSDiagnostic::json () const { + return JSON::Object::Entries { + {"watchers", this->watchers.json()}, + {"descriptors", this->descriptors.json()} + }; + } + + JSON::Object CoreDiagnostics::FSDiagnostic::WatchersDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::FSDiagnostic::DescriptorsDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::TimersDiagnostic::json () const { + return JSON::Object::Entries { + {"timeout", this->timeout.json()}, + {"interval", this->interval.json()}, + {"immediate", this->immediate.json()} + }; + } + + JSON::Object CoreDiagnostics::TimersDiagnostic::TimeoutDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::TimersDiagnostic::IntervalDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::TimersDiagnostic::ImmediateDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::UDPDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()} + }; + } + + JSON::Object CoreDiagnostics::ConduitDiagnostic::json () const { + return JSON::Object::Entries { + {"handles", this->handles.json()}, + {"isActive", this->isActive} + }; + } + + JSON::Object CoreDiagnostics::QueryDiagnostic::json () const { + return JSON::Object::Entries { + {"posts", this->posts.json()}, + {"childProcess", this->childProcess.json()}, + {"ai", this->ai.json()}, + {"fs", this->fs.json()}, + {"timers", this->timers.json()}, + {"udp", this->udp.json()}, + {"uv", this->uv.json()}, + {"conduit", this->conduit.json()} + }; + } +} diff --git a/src/core/modules/diagnostics.hh b/src/core/modules/diagnostics.hh new file mode 100644 index 0000000000..0e63cd9ef2 --- /dev/null +++ b/src/core/modules/diagnostics.hh @@ -0,0 +1,127 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_DIAGNOSTICS_H +#define SOCKET_RUNTIME_CORE_MODULE_DIAGNOSTICS_H + +#include "../json.hh" +#include "../module.hh" + +namespace SSC { + class Core; + class CoreDiagnostics : public CoreModule { + public: + struct Diagnostic { + using ID = uint64_t; + struct Handles { + size_t count = 0; + Vector<ID> ids; + JSON::Object json () const; + }; + + virtual JSON::Object json () const = 0; + }; + + struct UVDiagnostic : public Diagnostic { + uv_metrics_t metrics; // various uv metrics + Handles handles; // active uv loop handles + uint64_t idleTime = 0; + uint64_t activeRequests = 0; + JSON::Object json () const override; + }; + + struct PostsDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + struct ChildProcessDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + struct AIDiagnostic : public Diagnostic { + struct LLMDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + LLMDiagnostic llm; + + JSON::Object json () const override; + }; + + struct FSDiagnostic : public Diagnostic { + struct WatchersDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + struct DescriptorsDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + WatchersDiagnostic watchers; + DescriptorsDiagnostic descriptors; + JSON::Object json () const override; + }; + + struct TimersDiagnostic : public Diagnostic { + struct TimeoutDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + struct IntervalDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + struct ImmediateDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + TimeoutDiagnostic timeout; + IntervalDiagnostic interval; + ImmediateDiagnostic immediate; + + JSON::Object json () const override; + }; + + struct UDPDiagnostic : public Diagnostic { + Handles handles; + JSON::Object json () const override; + }; + + struct ConduitDiagnostic : public Diagnostic { + Handles handles; + bool isActive; + JSON::Object json () const override; + }; + + struct QueryDiagnostic : public Diagnostic { + PostsDiagnostic posts; + ChildProcessDiagnostic childProcess; + AIDiagnostic ai; + FSDiagnostic fs; + TimersDiagnostic timers; + UDPDiagnostic udp; + UVDiagnostic uv; + ConduitDiagnostic conduit; + + JSON::Object json () const override; + }; + + using QueryCallback = Function<void(const QueryDiagnostic&)>; + + CoreDiagnostics (Core* core) + : CoreModule(core) + {} + + void query (const QueryCallback& callback) const; + void query ( + const String& seq, + const CoreModule::Callback& callback + ) const; + }; +} +#endif diff --git a/src/core/modules/dns.cc b/src/core/modules/dns.cc new file mode 100644 index 0000000000..18fdc7d470 --- /dev/null +++ b/src/core/modules/dns.cc @@ -0,0 +1,97 @@ +#include "../core.hh" +#include "dns.hh" + +namespace SSC { + void CoreDNS::lookup ( + const String& seq, + const LookupOptions& options, + const CoreModule::Callback& callback + ) const { + this->core->dispatchEventLoop([=, this]() { + auto ctx = new CoreModule::RequestContext(seq, callback); + auto loop = this->core->getEventLoop(); + + struct addrinfo hints = {0}; + + if (options.family == 6) { + hints.ai_family = AF_INET6; + } else if (options.family == 4) { + hints.ai_family = AF_INET; + } else { + hints.ai_family = AF_UNSPEC; + } + + hints.ai_socktype = 0; // `0` for any + hints.ai_protocol = 0; // `0` for any + + auto resolver = new uv_getaddrinfo_t; + resolver->data = ctx; + + auto err = uv_getaddrinfo(loop, resolver, [](uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) { + auto ctx = (RequestContext*) resolver->data; + + if (status < 0) { + auto result = JSON::Object::Entries { + {"source", "dns.lookup"}, + {"err", JSON::Object::Entries { + {"code", std::to_string(status)}, + {"message", String(uv_strerror(status))} + }} + }; + + ctx->callback(ctx->seq, result, Post{}); + uv_freeaddrinfo(res); + delete resolver; + delete ctx; + return; + } + + String address = ""; + + if (res->ai_family == AF_INET) { + char addr[17] = {'\0'}; + uv_ip4_name((struct sockaddr_in*)(res->ai_addr), addr, 16); + address = String(addr, 17); + } else if (res->ai_family == AF_INET6) { + char addr[40] = {'\0'}; + uv_ip6_name((struct sockaddr_in6*)(res->ai_addr), addr, 39); + address = String(addr, 40); + } + + address = address.erase(address.find('\0')); + + auto family = res->ai_family == AF_INET + ? 4 + : res->ai_family == AF_INET6 + ? 6 + : 0; + + auto result = JSON::Object::Entries { + {"source", "dns.lookup"}, + {"data", JSON::Object::Entries { + {"address", address}, + {"family", family} + }} + }; + + ctx->callback(ctx->seq, result, Post{}); + uv_freeaddrinfo(res); + delete resolver; + delete ctx; + }, options.hostname.c_str(), nullptr, &hints); + + if (err < 0) { + auto result = JSON::Object::Entries { + {"source", "dns.lookup"}, + {"err", JSON::Object::Entries { + {"code", std::to_string(err)}, + {"message", String(uv_strerror(err))} + }} + }; + + ctx->callback(seq, result, Post{}); + delete ctx; + } + }); + } +} diff --git a/src/core/modules/dns.hh b/src/core/modules/dns.hh new file mode 100644 index 0000000000..01e4fc9ae1 --- /dev/null +++ b/src/core/modules/dns.hh @@ -0,0 +1,30 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_DNS_H +#define SOCKET_RUNTIME_CORE_MODULE_DNS_H + +#include "../module.hh" + +namespace SSC { + class Core; + class CoreDNS : public CoreModule { + public: + struct LookupOptions { + String hostname; + int family; + // TODO: support these options + // - hints + // - all + // -verbatim + }; + + CoreDNS (Core* core) + : CoreModule(core) + {} + + void lookup ( + const String& seq, + const LookupOptions& options, + const CoreModule::Callback& callback + ) const; + }; +} +#endif diff --git a/src/core/fs.cc b/src/core/modules/fs.cc similarity index 54% rename from src/core/fs.cc rename to src/core/modules/fs.cc index 98a195dfd9..cf72b0de7c 100644 --- a/src/core/fs.cc +++ b/src/core/modules/fs.cc @@ -1,173 +1,178 @@ -#include "core.hh" -namespace SSC { +#include "../core.hh" +#include "../headers.hh" +#include "../json.hh" +#include "../resource.hh" +#include "../trace.hh" - #define SET_CONSTANT(c) constants[#c] = (c); - static std::map<String, int32_t> getFSConstantsMap () { - std::map<String, int32_t> constants; +#include "fs.hh" +namespace SSC { + #define CONSTANT(c) { #c, (c) }, + static const std::map<String, int32_t> FS_CONSTANTS = { #if defined(UV_DIRENT_UNKNOWN) - SET_CONSTANT(UV_DIRENT_UNKNOWN) + CONSTANT(UV_DIRENT_UNKNOWN) #endif #if defined(UV_DIRENT_FILE) - SET_CONSTANT(UV_DIRENT_FILE) + CONSTANT(UV_DIRENT_FILE) #endif #if defined(UV_DIRENT_DIR) - SET_CONSTANT(UV_DIRENT_DIR) + CONSTANT(UV_DIRENT_DIR) #endif #if defined(UV_DIRENT_LINK) - SET_CONSTANT(UV_DIRENT_LINK) + CONSTANT(UV_DIRENT_LINK) #endif #if defined(UV_DIRENT_FIFO) - SET_CONSTANT(UV_DIRENT_FIFO) + CONSTANT(UV_DIRENT_FIFO) #endif #if defined(UV_DIRENT_SOCKET) - SET_CONSTANT(UV_DIRENT_SOCKET) + CONSTANT(UV_DIRENT_SOCKET) #endif #if defined(UV_DIRENT_CHAR) - SET_CONSTANT(UV_DIRENT_CHAR) + CONSTANT(UV_DIRENT_CHAR) #endif #if defined(UV_DIRENT_BLOCK) - SET_CONSTANT(UV_DIRENT_BLOCK) + CONSTANT(UV_DIRENT_BLOCK) + #endif + #if defined(UV_FS_O_FILEMAP) + CONSTANT(UV_FS_O_FILEMAP) #endif #if defined(O_RDONLY) - SET_CONSTANT(O_RDONLY); + CONSTANT(O_RDONLY) #endif #if defined(O_WRONLY) - SET_CONSTANT(O_WRONLY); + CONSTANT(O_WRONLY) #endif #if defined(O_RDWR) - SET_CONSTANT(O_RDWR); + CONSTANT(O_RDWR) #endif #if defined(O_APPEND) - SET_CONSTANT(O_APPEND); + CONSTANT(O_APPEND) #endif #if defined(O_ASYNC) - SET_CONSTANT(O_ASYNC); + CONSTANT(O_ASYNC) #endif #if defined(O_CLOEXEC) - SET_CONSTANT(O_CLOEXEC); + CONSTANT(O_CLOEXEC) #endif #if defined(O_CREAT) - SET_CONSTANT(O_CREAT); + CONSTANT(O_CREAT) #endif #if defined(O_DIRECT) - SET_CONSTANT(O_DIRECT); + CONSTANT(O_DIRECT) #endif #if defined(O_DIRECTORY) - SET_CONSTANT(O_DIRECTORY); + CONSTANT(O_DIRECTORY) #endif #if defined(O_DSYNC) - SET_CONSTANT(O_DSYNC); + CONSTANT(O_DSYNC) #endif #if defined(O_EXCL) - SET_CONSTANT(O_EXCL); + CONSTANT(O_EXCL) #endif #if defined(O_LARGEFILE) - SET_CONSTANT(O_LARGEFILE); + CONSTANT(O_LARGEFILE) #endif #if defined(O_NOATIME) - SET_CONSTANT(O_NOATIME); + CONSTANT(O_NOATIME) #endif #if defined(O_NOCTTY) - SET_CONSTANT(O_NOCTTY); + CONSTANT(O_NOCTTY) #endif #if defined(O_NOFOLLOW) - SET_CONSTANT(O_NOFOLLOW); + CONSTANT(O_NOFOLLOW) #endif #if defined(O_NONBLOCK) - SET_CONSTANT(O_NONBLOCK); + CONSTANT(O_NONBLOCK) #endif #if defined(O_NDELAY) - SET_CONSTANT(O_NDELAY); + CONSTANT(O_NDELAY) #endif #if defined(O_PATH) - SET_CONSTANT(O_PATH); + CONSTANT(O_PATH) #endif #if defined(O_SYNC) - SET_CONSTANT(O_SYNC); + CONSTANT(O_SYNC) #endif #if defined(O_TMPFILE) - SET_CONSTANT(O_TMPFILE); + CONSTANT(O_TMPFILE) #endif #if defined(O_TRUNC) - SET_CONSTANT(O_TRUNC); + CONSTANT(O_TRUNC) #endif #if defined(S_IFMT) - SET_CONSTANT(S_IFMT); + CONSTANT(S_IFMT) #endif #if defined(S_IFREG) - SET_CONSTANT(S_IFREG); + CONSTANT(S_IFREG) #endif #if defined(S_IFDIR) - SET_CONSTANT(S_IFDIR); + CONSTANT(S_IFDIR) #endif #if defined(S_IFCHR) - SET_CONSTANT(S_IFCHR); + CONSTANT(S_IFCHR) #endif #if defined(S_IFBLK) - SET_CONSTANT(S_IFBLK); + CONSTANT(S_IFBLK) #endif #if defined(S_IFIFO) - SET_CONSTANT(S_IFIFO); + CONSTANT(S_IFIFO) #endif #if defined(S_IFLNK) - SET_CONSTANT(S_IFLNK); + CONSTANT(S_IFLNK) #endif #if defined(S_IFSOCK) - SET_CONSTANT(S_IFSOCK); + CONSTANT(S_IFSOCK) #endif #if defined(S_IRWXU) - SET_CONSTANT(S_IRWXU); + CONSTANT(S_IRWXU) #endif #if defined(S_IRUSR) - SET_CONSTANT(S_IRUSR); + CONSTANT(S_IRUSR) #endif #if defined(S_IWUSR) - SET_CONSTANT(S_IWUSR); + CONSTANT(S_IWUSR) #endif #if defined(S_IXUSR) - SET_CONSTANT(S_IXUSR); + CONSTANT(S_IXUSR) #endif #if defined(S_IRWXG) - SET_CONSTANT(S_IRWXG); + CONSTANT(S_IRWXG) #endif #if defined(S_IRGRP) - SET_CONSTANT(S_IRGRP); + CONSTANT(S_IRGRP) #endif #if defined(S_IWGRP) - SET_CONSTANT(S_IWGRP); + CONSTANT(S_IWGRP) #endif #if defined(S_IXGRP) - SET_CONSTANT(S_IXGRP); + CONSTANT(S_IXGRP) #endif #if defined(S_IRWXO) - SET_CONSTANT(S_IRWXO); + CONSTANT(S_IRWXO) #endif #if defined(S_IROTH) - SET_CONSTANT(S_IROTH); + CONSTANT(S_IROTH) #endif #if defined(S_IWOTH) - SET_CONSTANT(S_IWOTH); + CONSTANT(S_IWOTH) #endif #if defined(S_IXOTH) - SET_CONSTANT(S_IXOTH); + CONSTANT(S_IXOTH) #endif #if defined(F_OK) - SET_CONSTANT(F_OK); + CONSTANT(F_OK) #endif #if defined(R_OK) - SET_CONSTANT(R_OK); + CONSTANT(R_OK) #endif #if defined(W_OK) - SET_CONSTANT(W_OK); + CONSTANT(W_OK) #endif #if defined(X_OK) - SET_CONSTANT(X_OK); + CONSTANT(X_OK) #endif - - return constants; - } - #undef SET_CONSTANT + }; + #undef CONSTANT JSON::Object getStatsJSON (const String& source, uv_stat_t* stats) { return JSON::Object::Entries { @@ -205,49 +210,45 @@ namespace SSC { }; } - void Core::FS::RequestContext::setBuffer(char* base, uint32_t len) { - this->buf.base = base; - this->buf.len = len; - } - - void Core::FS::RequestContext::freeBuffer() { - delete[] static_cast<char*>(this->buf.base); - this->buf.base = nullptr; - this->buf.len = 0; - } - - char* Core::FS::RequestContext::getBuffer () { - return this->buf.base; - } - - uint32_t Core::FS::RequestContext::getBufferSize () { - return this->buf.len; + void CoreFS::RequestContext::setBuffer (SharedPointer<char[]> base, uint32_t len) { + this->buffer = base; + this->buf.base = base.get(); + this->buf.len = len; } - Core::FS::Descriptor::Descriptor (Core *core, uint64_t id) { - this->core = core; - this->id = id; - } + CoreFS::Descriptor::Descriptor (CoreFS* fs, ID id, const String& filename) + : resource(filename, { false, fs->core }), + fs(fs), + id(id) + {} - bool Core::FS::Descriptor::isDirectory () { - Lock lock(this->mutex); + bool CoreFS::Descriptor::isDirectory () const { + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (this->isAndroidAssetDirectory) { + return true; + } + #endif return this->dir != nullptr; } - bool Core::FS::Descriptor::isFile () { - Lock lock(this->mutex); + bool CoreFS::Descriptor::isFile () const { + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (this->androidAsset != nullptr) { + return true; + } + #endif return this->fd > 0 && this->dir == nullptr; } - bool Core::FS::Descriptor::isRetained () { + bool CoreFS::Descriptor::isRetained () const { return this->retained; } - bool Core::FS::Descriptor::isStale () { + bool CoreFS::Descriptor::isStale () const { return this->stale; } - Core::FS::Descriptor * Core::FS::getDescriptor (uint64_t id) { + SharedPointer<CoreFS::Descriptor> CoreFS::getDescriptor (ID id) { Lock lock(this->mutex); if (descriptors.find(id) != descriptors.end()) { return descriptors.at(id); @@ -255,22 +256,28 @@ namespace SSC { return nullptr; } - void Core::FS::removeDescriptor (uint64_t id) { + SharedPointer<CoreFS::Descriptor> CoreFS::getDescriptor (ID id) const { + if (descriptors.find(id) != descriptors.end()) { + return descriptors.at(id); + } + return nullptr; + } + + void CoreFS::removeDescriptor (ID id) { Lock lock(this->mutex); if (descriptors.find(id) != descriptors.end()) { descriptors.erase(id); } } - bool Core::FS::hasDescriptor (uint64_t id) { - Lock lock(this->mutex); + bool CoreFS::hasDescriptor (ID id) const { return descriptors.find(id) != descriptors.end(); } - void Core::FS::retainOpenDescriptor ( - const String seq, - uint64_t id, - Module::Callback cb + void CoreFS::retainOpenDescriptor ( + const String& seq, + ID id, + const CoreModule::Callback& callback ) { auto desc = getDescriptor(id); @@ -285,7 +292,7 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } desc->retained = true; @@ -296,32 +303,114 @@ namespace SSC { }} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); } - void Core::FS::access ( - const String seq, - const String path, + void CoreFS::access ( + const String& seq, + const String& path, int mode, - Module::Callback cb + const CoreModule::Callback& callback ) { - this->core->dispatchEventLoop([=, this]() { - auto filename = path.c_str(); + this->core->dispatchEventLoop([=, this]() mutable { auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto desc = std::make_shared<Descriptor>(this, 0, path); + + if (desc->resource.url.scheme == "socket") { + if (desc->resource.access(mode) == mode) { + const auto json = JSON::Object::Entries { + {"source", "fs.access"}, + {"data", JSON::Object::Entries { + {"mode", mode}, + }} + }; + + return callback(seq, json, Post{}); + } + #if SOCKET_RUNTIME_PLATFORM_ANDROID + else if (mode == R_OK || mode == F_OK) { + auto name = desc->resource.name; + + if (name.starts_with("/")) { + name = name.substr(1); + } else if (name.starts_with("./")) { + name = name.substr(2); + } + + const auto attachment = Android::JNIEnvironmentAttachment(desc->fs->core->platform.jvm); + const auto assetManager = CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->core->platform.activity, + "getAssetManager", + "()Landroid/content/res/AssetManager;" + ); + + const auto entries = (jobjectArray) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + assetManager, + "list", + "(Ljava/lang/String;)[Ljava/lang/String;", + attachment.env->NewStringUTF(name.c_str()) + ); + + const auto length = attachment.env->GetArrayLength(entries); + if (length > 0) { + const auto json = JSON::Object::Entries { + {"source", "fs.access"}, + {"data", JSON::Object::Entries { + {"mode", mode}, + }} + }; + + return callback(seq, json, Post{}); + } + } + #endif + } + #if SOCKET_RUNTIME_PLATFORM_ANDROID + else if ( + desc->resource.url.scheme == "content" || + desc->resource.url.scheme == "android.resource" + ) { + if (this->core->platform.contentResolver.hasAccess(desc->resource.url.str())) { + const auto json = JSON::Object::Entries { + {"source", "fs.access"}, + {"data", JSON::Object::Entries { + {"mode", mode}, + }} + }; + + return callback(seq, json, Post{}); + } + } + #endif + + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; - auto err = uv_fs_access(loop, req, filename, mode, [](uv_fs_t* req) { + auto err = uv_fs_access(loop, req, desc->resource.path.string().c_str(), mode, [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; auto json = JSON::Object {}; if (uv_fs_get_result(req) < 0) { - json = JSON::Object::Entries { - {"source", "fs.access"}, - {"err", JSON::Object::Entries { - {"code", req->result}, - {"message", String(uv_strerror((int) req->result))} - }} - }; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (ctx->descriptor->resource.access(req->flags) == req->flags) { + json = JSON::Object::Entries { + {"source", "fs.access"}, + {"data", JSON::Object::Entries { + {"mode", req->flags}, + }} + }; + } else + #endif + { + json = JSON::Object::Entries { + {"source", "fs.access"}, + {"err", JSON::Object::Entries { + {"code", req->result}, + {"message", String(uv_strerror((int) req->result))} + }} + }; + } } else { json = JSON::Object::Entries { {"source", "fs.access"}, @@ -331,7 +420,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; }); @@ -344,22 +433,22 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::chmod ( - const String seq, - const String path, + void CoreFS::chmod ( + const String& seq, + const String& path, int mode, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto filename = path.c_str(); auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(seq, callback); auto req = &ctx->req; auto err = uv_fs_chmod(loop, req, filename, mode, [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; @@ -382,7 +471,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; }); @@ -395,22 +484,22 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::chown ( - const String seq, - const String path, + void CoreFS::chown ( + const String& seq, + const String& path, uv_uid_t uid, uv_gid_t gid, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { core->dispatchEventLoop([=, this]() { - auto ctx = new RequestContext(seq, cb); - auto uv_cb = [](uv_fs_t* req) { + const auto ctx = new RequestContext(seq, callback); + const auto err = uv_fs_chown(&core->eventLoop, &ctx->req, path.c_str(), uid, gid, [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); auto json = JSON::Object{}; @@ -431,12 +520,9 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; - }; - - auto err = uv_fs_chown( - &core->eventLoop, &ctx->req, path.c_str(), uid, gid, uv_cb); + }); if (err < 0) { auto json = JSON::Object::Entries { @@ -446,22 +532,22 @@ namespace SSC { {"message", String(uv_strerror(err))} }} }; - ctx->cb(seq, json, Post{}); + ctx->callback(seq, json, Post{}); delete ctx; } }); } - void Core::FS::lchown ( - const String seq, - const String path, + void CoreFS::lchown ( + const String& seq, + const String& path, uv_uid_t uid, uv_gid_t gid, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { core->dispatchEventLoop([=, this]() { - auto ctx = new RequestContext(seq, cb); - auto uv_cb = [](uv_fs_t* req) { + const auto ctx = new RequestContext(seq, callback); + const auto err = uv_fs_lchown(&core->eventLoop, &ctx->req, path.c_str(), uid, gid, [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); auto json = JSON::Object{}; @@ -482,12 +568,9 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; - }; - - auto err = uv_fs_lchown( - &core->eventLoop, &ctx->req, path.c_str(), uid, gid, uv_cb); + }); if (err < 0) { auto json = JSON::Object::Entries { @@ -497,16 +580,16 @@ namespace SSC { {"message", String(uv_strerror(err))} }} }; - ctx->cb(seq, json, Post{}); + ctx->callback(seq, json, Post{}); delete ctx; } }); } - void Core::FS::close ( - const String seq, - uint64_t id, - Module::Callback cb + void CoreFS::close ( + const String& seq, + ID id, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { auto desc = getDescriptor(id); @@ -522,15 +605,53 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); + } + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->androidAsset != nullptr) { + Lock lock(this->mutex); + AAsset_close(desc->androidAsset); + desc->androidAsset = nullptr; + const auto json = JSON::Object::Entries { + {"source", "fs.close"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)}, + {"fd", desc->fd} + }} + }; + + this->removeDescriptor(desc->id); + return callback(seq, json, Post{}); + } else if ( + desc->resource.url.scheme == "content" || + desc->resource.url.scheme == "android.resource" + ) { + Lock lock(this->mutex); + this->core->platform.contentResolver.closeFileDescriptor( + desc->androidContent + ); + + desc->androidContent = nullptr; + const auto json = JSON::Object::Entries { + {"source", "fs.close"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)}, + {"fd", desc->fd} + }} + }; + + this->removeDescriptor(desc->id); + return callback(seq, json, Post{}); } + #endif auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; auto err = uv_fs_close(loop, req, desc->fd, [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; - auto desc = ctx->desc; + auto desc = ctx->descriptor; auto json = JSON::Object {}; if (uv_fs_get_result(req) < 0) { @@ -551,11 +672,10 @@ namespace SSC { }} }; - desc->core->fs.removeDescriptor(desc->id); - delete desc; + desc->fs->removeDescriptor(desc->id); } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -569,42 +689,140 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::open ( - const String seq, - uint64_t id, - const String path, + void CoreFS::open ( + const String& seq, + ID id, + const String& path, int flags, int mode, - Module::Callback cb + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { - auto filename = path.c_str(); - auto desc = new Descriptor(this->core, id); - auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); - auto req = &ctx->req; - auto err = uv_fs_open(loop, req, filename, flags, mode, [](uv_fs_t* req) { - auto ctx = (RequestContext *) req->data; - auto desc = ctx->desc; - auto json = JSON::Object {}; + auto desc = std::make_shared<Descriptor>(this, id, path); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->resource.url.scheme == "socket") { + auto assetManager = FileResource::getSharedAndroidAssetManager(); + desc->androidAsset = AAssetManager_open( + assetManager, + desc->resource.name.c_str(), + AASSET_MODE_RANDOM + ); + + desc->fd = AAsset_openFileDescriptor( + desc->androidAsset, + &desc->androidAssetOffset, + &desc->androidAssetLength + ); + + const auto json = JSON::Object::Entries { + {"source", "fs.open"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)}, + {"fd", desc->fd} + }} + }; - if (uv_fs_get_result(req) < 0) { - json = JSON::Object::Entries { + // insert into `descriptors` map + Lock lock(desc->fs->mutex); + this->descriptors.insert_or_assign(desc->id, desc); + return callback(seq, json, Post{}); + } else if ( + desc->resource.url.scheme == "content" || + desc->resource.url.scheme == "android.resource" + ) { + auto fileDescriptor = this->core->platform.contentResolver.openFileDescriptor( + desc->resource.url.str(), + &desc->androidContentOffset, + &desc->androidContentLength + ); + + if (fileDescriptor == nullptr) { + const auto json = JSON::Object::Entries { {"source", "fs.open"}, {"err", JSON::Object::Entries { {"id", std::to_string(desc->id)}, - {"code", req->result}, - {"message", String(uv_strerror((int) req->result))} + {"type", "NotFoundError"}, + {"message", "Content does not exist at given URI"} }} }; - delete desc; + return callback(seq, json, Post{}); + } + + desc->fd = this->core->platform.contentResolver.getFileDescriptorFD( + fileDescriptor + ); + + desc->androidContent = fileDescriptor; + + const auto json = JSON::Object::Entries { + {"source", "fs.open"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)}, + {"fd", desc->fd} + }} + }; + + // insert into `descriptors` map + Lock lock(desc->fs->mutex); + this->descriptors.insert_or_assign(desc->id, desc); + return callback(seq, json, Post{}); + } + #endif + + auto loop = &this->core->eventLoop; + auto ctx = new RequestContext(desc, seq, callback); + auto req = &ctx->req; + auto err = uv_fs_open(loop, req, desc->resource.path.string().c_str(), flags, mode, [](uv_fs_t* req) { + auto ctx = (RequestContext *) req->data; + auto desc = ctx->descriptor; + auto json = JSON::Object {}; + + if (uv_fs_get_result(req) < 0) { + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (ctx->descriptor->resource.isAndroidLocalAsset()) { + auto assetManager = FileResource::getSharedAndroidAssetManager(); + desc->androidAsset = AAssetManager_open( + assetManager, + ctx->descriptor->resource.name.c_str(), + AASSET_MODE_RANDOM + ); + + desc->fd = AAsset_openFileDescriptor( + desc->androidAsset, + &desc->androidAssetOffset, + &desc->androidAssetLength + ); + json = JSON::Object::Entries { + {"source", "fs.open"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)}, + {"fd", desc->fd} + }} + }; + + // insert into `descriptors` map + Lock lock(desc->fs->mutex); + desc->fs->descriptors.insert_or_assign(desc->id, desc); + } else + #endif + { + json = JSON::Object::Entries { + {"source", "fs.open"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(desc->id)}, + {"code", req->result}, + {"message", String(uv_strerror((int) req->result))} + }} + }; + } } else { json = JSON::Object::Entries { {"source", "fs.open"}, @@ -616,11 +834,11 @@ namespace SSC { desc->fd = (int) req->result; // insert into `descriptors` map - Lock lock(desc->core->fs.mutex); - desc->core->fs.descriptors.insert_or_assign(desc->id, desc); + Lock lock(desc->fs->mutex); + desc->fs->descriptors.insert_or_assign(desc->id, desc); } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -634,41 +852,173 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); - delete desc; + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::opendir ( - const String seq, - uint64_t id, - const String path, - Module::Callback cb + void CoreFS::opendir ( + const String& seq, + ID id, + const String& path, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { - auto filename = path.c_str(); - auto desc = new Descriptor(this->core, id); + auto desc = std::make_shared<Descriptor>(this, id, path); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->resource.url.scheme == "socket") { + auto name = desc->resource.name; + + if (name.starts_with("/")) { + name = name.substr(1); + } else if (name.starts_with("./")) { + name = name.substr(2); + } + + const auto attachment = Android::JNIEnvironmentAttachment(desc->fs->core->platform.jvm); + const auto assetManager = CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + desc->fs->core->platform.activity, + "getAssetManager", + "()Landroid/content/res/AssetManager;" + ); + + const auto entries = (jobjectArray) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + assetManager, + "list", + "(Ljava/lang/String;)[Ljava/lang/String;", + attachment.env->NewStringUTF(name.c_str()) + ); + + const auto length = attachment.env->GetArrayLength(entries); + + for (int i = 0; i < length; ++i) { + const auto entry = (jstring) attachment.env->GetObjectArrayElement(entries, i); + const auto filename = attachment.env->GetStringUTFChars(entry, nullptr); + if (filename != nullptr) { + desc->androidAssetDirectoryEntries.push(filename); + attachment.env->ReleaseStringUTFChars(entry, filename); + } + } + + if (length > 0) { + desc->isAndroidAssetDirectory = true; + const auto json = JSON::Object::Entries { + {"source", "fs.opendir"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)} + }} + }; + + // insert into `descriptors` map + Lock lock(desc->fs->mutex); + desc->fs->descriptors.insert_or_assign(desc->id, desc); + return callback(seq, json, Post{}); + } + } else if ( + desc->resource.url.scheme == "content" || + desc->resource.url.scheme == "android.resource" + ) { + const auto entries = this->core->platform.contentResolver.getPathnameEntriesFromContentURI( + desc->resource.url.str() + ); + + for (const auto& entry : entries) { + desc->androidContentDirectoryEntries.push(entry); + } + + if (entries.size() > 0) { + desc->isAndroidContentDirectory = true; + const auto json = JSON::Object::Entries { + {"source", "fs.opendir"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)} + }} + }; + + // insert into `descriptors` map + Lock lock(desc->fs->mutex); + desc->fs->descriptors.insert_or_assign(desc->id, desc); + return callback(seq, json, Post{}); + } + } + #endif + auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; - auto err = uv_fs_opendir(loop, req, filename, [](uv_fs_t *req) { + auto err = uv_fs_opendir(loop, req, desc->resource.path.string().c_str(), [](uv_fs_t *req) { auto ctx = (RequestContext *) req->data; - auto desc = ctx->desc; + auto desc = ctx->descriptor; auto json = JSON::Object {}; if (uv_fs_get_result(req) < 0) { - json = JSON::Object::Entries { - {"source", "fs.opendir"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(desc->id)}, - {"code", req->result}, - {"message", String(uv_strerror((int) req->result))} - }} - }; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + auto name = ctx->descriptor->resource.name; + + if (name.starts_with("/")) { + name = name.substr(1); + } else if (name.starts_with("./")) { + name = name.substr(2); + } + + const auto attachment = Android::JNIEnvironmentAttachment(ctx->descriptor->fs->core->platform.jvm); + const auto assetManager = CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + ctx->descriptor->fs->core->platform.activity, + "getAssetManager", + "()Landroid/content/res/AssetManager;" + ); + + const auto entries = (jobjectArray) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + assetManager, + "list", + "(Ljava/lang/String;)[Ljava/lang/String;", + attachment.env->NewStringUTF(name.c_str()) + ); + + const auto length = attachment.env->GetArrayLength(entries); + + for (int i = 0; i < length; ++i) { + const auto entry = (jstring) attachment.env->GetObjectArrayElement(entries, i); + const auto filename = attachment.env->GetStringUTFChars(entry, nullptr); + if (filename != nullptr) { + desc->androidAssetDirectoryEntries.push(filename); + attachment.env->ReleaseStringUTFChars(entry, filename); + } + } + + if (length > 0) { + desc->isAndroidAssetDirectory = true; + } + + if (desc->isAndroidAssetDirectory) { + json = JSON::Object::Entries { + {"source", "fs.opendir"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)} + }} + }; - delete desc; + // insert into `descriptors` map + Lock lock(desc->fs->mutex); + desc->fs->descriptors.insert_or_assign(desc->id, desc); + } else + #endif + { + json = JSON::Object::Entries { + {"source", "fs.opendir"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(desc->id)}, + {"code", req->result}, + {"message", String(uv_strerror((int) req->result))} + }} + }; + } } else { json = JSON::Object::Entries { {"source", "fs.opendir"}, @@ -679,11 +1029,11 @@ namespace SSC { desc->dir = (uv_dir_t *) req->ptr; // insert into `descriptors` map - Lock lock(desc->core->fs.mutex); - desc->core->fs.descriptors.insert_or_assign(desc->id, desc); + Lock lock(desc->fs->mutex); + desc->fs->descriptors.insert_or_assign(desc->id, desc); } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -697,19 +1047,18 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); - delete desc; + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::readdir ( - const String seq, - uint64_t id, + void CoreFS::readdir ( + const String& seq, + ID id, size_t nentries, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto desc = getDescriptor(id); @@ -724,9 +1073,69 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->isAndroidAssetDirectory) { + Vector<JSON::Any> entries; + + for ( + int i = 0; + i < nentries && i < desc->androidAssetDirectoryEntries.size(); + ++i + ) { + const auto name = desc->androidAssetDirectoryEntries.front(); + const auto encodedName = name.ends_with("/") + ? encodeURIComponent(name.substr(0, name.size() - 1)) + : encodeURIComponent(name); + + const auto entry = JSON::Object::Entries { + {"type", name.ends_with("/") ? 2 : 1}, + {"name", encodedName} + }; + + entries.push_back(entry); + desc->androidAssetDirectoryEntries.pop(); + } + + const auto json = JSON::Object::Entries { + {"source", "fs.readdir"}, + {"data", entries} + }; + + return callback(seq, json, Post{}); + } else if (desc->isAndroidContentDirectory) { + Vector<JSON::Any> entries; + + for ( + int i = 0; + i < nentries && i < desc->androidContentDirectoryEntries.size(); + ++i + ) { + const auto name = desc->androidContentDirectoryEntries.front(); + const auto encodedName = name.ends_with("/") + ? encodeURIComponent(name.substr(0, name.size() - 1)) + : encodeURIComponent(name); + + const auto entry = JSON::Object::Entries { + {"type", name.ends_with("/") ? 2 : 1}, + {"name", encodedName} + }; + + entries.push_back(entry); + desc->androidContentDirectoryEntries.pop(); + } + + const auto json = JSON::Object::Entries { + {"source", "fs.readdir"}, + {"data", entries} + }; + + return callback(seq, json, Post{}); + } + #endif + if (!desc->isDirectory()) { auto json = JSON::Object::Entries { {"source", "fs.readdir"}, @@ -737,12 +1146,12 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } Lock lock(desc->mutex); auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; desc->dir->dirents = ctx->dirents; @@ -750,7 +1159,7 @@ namespace SSC { auto err = uv_fs_readdir(loop, req, desc->dir, [](uv_fs_t *req) { auto ctx = (RequestContext *) req->data; - auto desc = ctx->desc; + auto desc = ctx->descriptor; auto json = JSON::Object {}; if (uv_fs_get_result(req) < 0) { @@ -768,7 +1177,7 @@ namespace SSC { for (int i = 0; i < req->result; ++i) { auto entry = JSON::Object::Entries { {"type", desc->dir->dirents[i].type}, - {"name", desc->dir->dirents[i].name} + {"name", encodeURIComponent(desc->dir->dirents[i].name)} }; entries.push_back(entry); @@ -780,7 +1189,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -794,16 +1203,16 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::closedir ( - const String seq, - uint64_t id, - Module::Callback cb + void CoreFS::closedir ( + const String& seq, + ID id, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { auto desc = getDescriptor(id); @@ -819,8 +1228,38 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); + } + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->isAndroidAssetDirectory) { + Lock lock(this->mutex); + desc->isAndroidAssetDirectory = false; + desc->androidAssetDirectoryEntries = {}; + const auto json = JSON::Object::Entries { + {"source", "fs.closedir"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)} + }} + }; + + this->removeDescriptor(desc->id); + return callback(seq, json, Post{}); + } else if (desc->isAndroidContentDirectory) { + Lock lock(this->mutex); + desc->isAndroidContentDirectory = false; + desc->androidContentDirectoryEntries = {}; + const auto json = JSON::Object::Entries { + {"source", "fs.closedir"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(desc->id)} + }} + }; + + this->removeDescriptor(desc->id); + return callback(seq, json, Post{}); } + #endif if (!desc->isDirectory()) { auto json = JSON::Object::Entries { @@ -832,15 +1271,15 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; auto err = uv_fs_closedir(loop, req, desc->dir, [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; - auto desc = ctx->desc; + auto desc = ctx->descriptor; auto json = JSON::Object {}; if (uv_fs_get_result(req) < 0) { @@ -861,11 +1300,10 @@ namespace SSC { }} }; - desc->core->fs.removeDescriptor(desc->id); - delete desc; + desc->fs->removeDescriptor(desc->id); } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -879,16 +1317,16 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::closeOpenDescriptor ( - const String seq, - uint64_t id, - Module::Callback cb + void CoreFS::closeOpenDescriptor ( + const String& seq, + ID id, + const CoreModule::Callback& callback ) { auto desc = getDescriptor(id); @@ -903,24 +1341,24 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } if (desc->isDirectory()) { - this->closedir(seq, id, cb); + this->closedir(seq, id, callback); } else if (desc->isFile()) { - this->close(seq, id, cb); + this->close(seq, id, callback); } } - void Core::FS::closeOpenDescriptors (const String seq, Module::Callback cb) { - return this->closeOpenDescriptors(seq, false, cb); + void CoreFS::closeOpenDescriptors (const String& seq, const CoreModule::Callback& callback) { + return this->closeOpenDescriptors(seq, false, callback); } - void Core::FS::closeOpenDescriptors ( - const String seq, + void CoreFS::closeOpenDescriptors ( + const String& seq, bool preserveRetained, - Module::Callback cb + const CoreModule::Callback& callback ) { Lock lock(this->mutex); @@ -948,34 +1386,34 @@ namespace SSC { if (desc->isDirectory()) { queued++; - this->closedir(seq, id, [pending, cb](auto seq, auto json, auto post) { + this->closedir(seq, id, [pending, callback](auto seq, auto json, auto post) { if (pending == 0) { - cb(seq, json, post); + callback(seq, json, post); } }); } else if (desc->isFile()) { queued++; - this->close(seq, id, [pending, cb](auto seq, auto json, auto post) { + this->close(seq, id, [pending, callback](auto seq, auto json, auto post) { if (pending == 0) { - cb(seq, json, post); + callback(seq, json, post); } }); } } if (queued == 0) { - cb(seq, json, Post{}); + callback(seq, json, Post{}); } } - void Core::FS::read ( - const String seq, - uint64_t id, + void CoreFS::read ( + const String& seq, + ID id, size_t size, size_t offset, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { + const CoreModule::Callback& callback + ) const { + this->core->dispatchEventLoop([=, this]() mutable { auto desc = getDescriptor(id); if (desc == nullptr) { @@ -989,19 +1427,67 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->androidAsset != nullptr) { + const auto length = AAsset_getLength(desc->androidAsset); + + if (offset >= static_cast<size_t>(length)) { + const auto headers = Headers {{ + {"content-type" ,"application/octet-stream"}, + {"content-length", 0} + }}; + + Post post {0}; + post.id = rand64(); + post.body = std::make_shared<char[]>(size); + post.length = 0; + post.headers = headers.str(); + return callback(seq, JSON::Object{}, post); + } else { + if (size > length - offset) { + size = length - offset; + } + + offset += desc->androidAssetOffset; + } + } else if (desc->androidContent != nullptr) { + const auto length = desc->androidContentLength; + + if (offset >= static_cast<size_t>(length)) { + const auto headers = Headers {{ + {"content-type" ,"application/octet-stream"}, + {"content-length", 0} + }}; + + Post post {0}; + post.id = rand64(); + post.body = std::make_shared<char[]>(size); + post.length = 0; + post.headers = headers.str(); + return callback(seq, JSON::Object{}, post); + } else { + if (size > length - offset) { + size = length - offset; + } + + offset += desc->androidContentOffset; + } + } + #endif + + auto bytes = std::make_shared<char[]>(size); auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; - auto bytes = new char[size]{0}; ctx->setBuffer(bytes, size); auto err = uv_fs_read(loop, req, desc->fd, &ctx->buf, 1, offset, [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); - auto desc = ctx->desc; + auto desc = ctx->descriptor; auto json = JSON::Object {}; Post post = {0}; @@ -1014,24 +1500,19 @@ namespace SSC { {"message", String(uv_strerror((int) req->result))} }} }; - - auto bytes = ctx->getBuffer(); - if (bytes != nullptr) { - delete [] bytes; - } } else { auto headers = Headers {{ {"content-type" ,"application/octet-stream"}, {"content-length", req->result} }}; - post.id = SSC::rand64(); - post.body = ctx->getBuffer(); + post.id = rand64(); + post.body = ctx->buffer; post.length = (int) req->result; post.headers = headers.str(); } - ctx->cb(ctx->seq, json, post); + ctx->callback(ctx->seq, json, post); delete ctx; }); @@ -1045,21 +1526,20 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); - delete [] bytes; + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::watch ( - const String seq, - uint64_t id, - const String path, - Module::Callback cb + void CoreFS::watch ( + const String& seq, + ID id, + const String& path, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { - #if defined(__ANDROID__) + #if SOCKET_RUNTIME_PLATFORM_ANDROID auto json = JSON::Object::Entries { {"source", "fs.watch"}, {"err", JSON::Object::Entries { @@ -1067,17 +1547,17 @@ namespace SSC { }} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); return; #else - FileSystemWatcher* watcher; + SharedPointer<FileSystemWatcher> watcher; { Lock lock(this->mutex); watcher = this->watchers[id]; } if (watcher == nullptr) { - watcher = new FileSystemWatcher(path); + watcher.reset(new FileSystemWatcher(path)); watcher->core = this->core; const auto started = watcher->start([=, this]( const auto& changed, @@ -1103,7 +1583,7 @@ namespace SSC { }} }; - cb("-1", json, Post{}); + callback("-1", json, Post{}); }); if (!started) { @@ -1114,7 +1594,7 @@ namespace SSC { }} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); return; } @@ -1131,19 +1611,19 @@ namespace SSC { }} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); #endif }); } - void Core::FS::write ( - const String seq, - uint64_t id, - char *bytes, + void CoreFS::write ( + const String& seq, + ID id, + SharedPointer<char[]> bytes, size_t size, size_t offset, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto desc = getDescriptor(id); @@ -1158,17 +1638,33 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); + } + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->androidAsset != nullptr) { + auto json = JSON::Object::Entries { + {"source", "fs.write"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"code", "EPERM"}, + {"type", "NotAllowedError"}, + {"message", "Cannot write to an Android Asset file descriptor."} + }} + }; + + return callback(seq, json, Post{}); } + #endif auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; ctx->setBuffer(bytes, size); auto err = uv_fs_write(loop, req, desc->fd, &ctx->buf, 1, offset, [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); - auto desc = ctx->desc; + auto desc = ctx->descriptor; auto json = JSON::Object {}; if (uv_fs_get_result(req) < 0) { @@ -1190,7 +1686,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1204,23 +1700,51 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::stat ( - const String seq, - const String path, - Module::Callback cb + void CoreFS::stat ( + const String& seq, + const String& path, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { - auto filename = path.c_str(); + auto desc = std::make_shared<Descriptor>(this, 0, path); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->resource.isAndroidLocalAsset()) { + const auto json = JSON::Object::Entries { + {"source", "fs.stat"}, + {"data", JSON::Object::Entries { + {"st_mode", R_OK}, + {"st_size", desc->resource.size()} + }} + }; + + return callback(seq, json, Post{}); + } else if ( + desc->resource.url.scheme == "content" || + desc->resource.url.scheme == "android.resource" + ) { + const auto json = JSON::Object::Entries { + {"source", "fs.stat"}, + {"data", JSON::Object::Entries { + {"st_mode", R_OK}, + {"st_size", desc->resource.size()} + }} + }; + + return callback(seq, json, Post{}); + } + #endif + auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; - auto err = uv_fs_stat(loop, req, filename, [](uv_fs_t *req) { + auto err = uv_fs_stat(loop, req, desc->resource.path.string().c_str(), [](uv_fs_t *req) { auto ctx = (RequestContext *) req->data; auto json = JSON::Object {}; @@ -1236,7 +1760,7 @@ namespace SSC { json = getStatsJSON("fs.stat", uv_fs_get_statbuf(req)); } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1249,19 +1773,19 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::stopWatch ( - const String seq, - uint64_t id, - Module::Callback cb + void CoreFS::stopWatch ( + const String& seq, + ID id, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { - #if defined(__ANDROID__) + #if SOCKET_RUNTIME_PLATFORM_ANDROID auto json = JSON::Object::Entries { {"source", "fs.stopWatch"}, {"err", JSON::Object::Entries { @@ -1269,21 +1793,20 @@ namespace SSC { }} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); return; #else - auto watcher = this->core->fs.watchers[id]; + auto watcher = this->watchers[id]; if (watcher != nullptr) { watcher->stop(); - delete watcher; - this->core->fs.watchers.erase(id); + this->watchers.erase(id); auto json = JSON::Object::Entries { {"source", "fs.stopWatch"}, {"data", JSON::Object::Entries { {"id", std::to_string(id)}, }} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); } else { auto json = JSON::Object::Entries { {"source", "fs.stat"}, @@ -1293,17 +1816,17 @@ namespace SSC { }} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); } #endif }); } - void Core::FS::fsync ( - const String seq, - uint64_t id, - Module::Callback cb - ) { + void CoreFS::fsync ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto desc = getDescriptor(id); @@ -1318,11 +1841,11 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; auto err = uv_fs_fsync(loop, req, desc->fd, [](uv_fs_t *req) { auto ctx = (RequestContext *) req->data; @@ -1345,7 +1868,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1359,18 +1882,18 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::ftruncate ( - const String seq, - uint64_t id, + void CoreFS::ftruncate ( + const String& seq, + ID id, int64_t offset, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto desc = getDescriptor(id); @@ -1385,11 +1908,11 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); } auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; auto err = uv_fs_ftruncate(loop, req, desc->fd, offset, [](uv_fs_t *req) { auto ctx = (RequestContext *) req->data; @@ -1412,7 +1935,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1426,16 +1949,16 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::fstat ( - const String seq, - uint64_t id, - Module::Callback cb + void CoreFS::fstat ( + const String& seq, + ID id, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { auto desc = getDescriptor(id); @@ -1451,15 +1974,29 @@ namespace SSC { }} }; - return cb(seq, json, Post{}); + return callback(seq, json, Post{}); + } + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (desc->androidAsset != nullptr) { + const auto json = JSON::Object::Entries { + {"source", "fs.stat"}, + {"data", JSON::Object::Entries { + {"st_mode", R_OK}, + {"st_size", desc->resource.size()} + }} + }; + + return callback(seq, json, Post{}); } + #endif auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(desc, seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; auto err = uv_fs_fstat(loop, req, desc->fd, [](uv_fs_t *req) { auto ctx = (RequestContext *) req->data; - auto desc = ctx->desc; + auto desc = ctx->descriptor; auto json = JSON::Object {}; if (uv_fs_get_result(req) < 0) { @@ -1475,7 +2012,7 @@ namespace SSC { json = getStatsJSON("fs.fstat", uv_fs_get_statbuf(req)); } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1489,17 +2026,16 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::getOpenDescriptors ( - const String seq, - Module::Callback cb - ) { - Lock lock(this->mutex); + void CoreFS::getOpenDescriptors ( + const String& seq, + const CoreModule::Callback& callback + ) const { auto entries = Vector<JSON::Any> {}; for (auto const &tuple : descriptors) { @@ -1523,20 +2059,20 @@ namespace SSC { {"data", entries} }; - cb(seq, json, Post{}); + callback(seq, json, Post{}); } - void Core::FS::lstat ( - const String seq, - const String path, - Module::Callback cb + void CoreFS::lstat ( + const String& seq, + const String& path, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { - auto filename = path.c_str(); + auto desc = std::make_shared<Descriptor>(this, 0, path); auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(desc, seq, callback); auto req = &ctx->req; - auto err = uv_fs_lstat(loop, req, filename, [](uv_fs_t* req) { + auto err = uv_fs_lstat(loop, req, desc->resource.path.string().c_str(), [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; auto json = JSON::Object {}; @@ -1552,7 +2088,7 @@ namespace SSC { json = getStatsJSON("fs.lstat", uv_fs_get_statbuf(req)); } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1565,21 +2101,21 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::link ( - const String seq, - const String src, - const String dest, - Module::Callback cb - ) { + void CoreFS::link ( + const String& seq, + const String& src, + const String& dest, + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { - auto ctx = new RequestContext(seq, cb); - auto uv_cb = [](uv_fs_t* req) { + auto ctx = new RequestContext(seq, callback); + auto err = uv_fs_link(&core->eventLoop, &ctx->req, src.c_str(), dest.c_str(), [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); auto json = JSON::Object{}; @@ -1600,12 +2136,9 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; - }; - - auto err = uv_fs_link( - &core->eventLoop, &ctx->req, src.c_str(), dest.c_str(), uv_cb); + }); if (err < 0) { auto json = JSON::Object::Entries { @@ -1615,22 +2148,22 @@ namespace SSC { {"message", String(uv_strerror(err))} }} }; - ctx->cb(seq, json, Post{}); + ctx->callback(seq, json, Post{}); delete ctx; } }); } - void Core::FS::symlink ( - const String seq, - const String src, - const String dest, + void CoreFS::symlink ( + const String& seq, + const String& src, + const String& dest, int flags, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { - auto ctx = new RequestContext(seq, cb); - auto uv_cb = [](uv_fs_t* req) { + auto ctx = new RequestContext(seq, callback); + auto err = uv_fs_symlink(&core->eventLoop, &ctx->req, src.c_str(), dest.c_str(), flags, [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); auto json = JSON::Object{}; @@ -1651,12 +2184,9 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; - }; - - auto err = uv_fs_symlink( - &core->eventLoop, &ctx->req, src.c_str(), dest.c_str(), flags, uv_cb); + }); if (err < 0) { auto json = JSON::Object::Entries { @@ -1666,21 +2196,21 @@ namespace SSC { {"message", String(uv_strerror(err))} }} }; - ctx->cb(seq, json, Post{}); + ctx->callback(seq, json, Post{}); delete ctx; } }); } - void Core::FS::unlink ( - const String seq, - const String path, - Module::Callback cb - ) { + void CoreFS::unlink ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto filename = path.c_str(); auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(seq, callback); auto req = &ctx->req; auto err = uv_fs_unlink(loop, req, filename, [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; @@ -1703,7 +2233,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1716,20 +2246,20 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::readlink ( - const String seq, - const String path, - const Module::Callback cb - ) { + void CoreFS::readlink ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { - auto ctx = new RequestContext(seq, cb); - auto uv_cb = [](uv_fs_t* req) { + auto ctx = new RequestContext(seq, callback); + auto err = uv_fs_readlink(&core->eventLoop, &ctx->req, path.c_str(), [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); auto json = JSON::Object{}; @@ -1750,12 +2280,9 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; - }; - - auto err = uv_fs_readlink( - &core->eventLoop, &ctx->req, path.c_str(), uv_cb); + }); if (err < 0) { auto json = JSON::Object::Entries { @@ -1765,31 +2292,51 @@ namespace SSC { {"message", String(uv_strerror(err))} }} }; - ctx->cb(seq, json, Post{}); + ctx->callback(seq, json, Post{}); delete ctx; } }); } - void Core::FS::realpath ( - const String seq, - const String path, - const Module::Callback cb + void CoreFS::realpath ( + const String& seq, + const String& path, + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { - auto ctx = new RequestContext(seq, cb); - auto uv_cb = [](uv_fs_t* req) { + auto filename = path.c_str(); auto desc = std::make_shared<Descriptor>(this, 0, filename); + auto ctx = new RequestContext(desc, seq, callback); + auto err = uv_fs_realpath(&core->eventLoop, &ctx->req, path.c_str(), [](uv_fs_t* req) { auto ctx = static_cast<RequestContext*>(req->data); auto json = JSON::Object{}; if (uv_fs_get_result(req) < 0) { - json = JSON::Object::Entries { - {"source", "fs.realpath"}, - {"err", JSON::Object::Entries { - {"code", uv_fs_get_result(req)}, - {"message", String(uv_strerror(uv_fs_get_result(req)))} - }} - }; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (ctx->descriptor->resource.isAndroidLocalAsset()) { + json = JSON::Object::Entries { + {"source", "fs.realpath"}, + {"data", JSON::Object::Entries { + {"path", ctx->descriptor->resource.path} + }} + }; + } else if (ctx->descriptor->resource.isAndroidContent()) { + json = JSON::Object::Entries { + {"source", "fs.realpath"}, + {"data", JSON::Object::Entries { + {"path", ctx->descriptor->resource.url.str()} + }} + }; + } else + #endif + { + json = JSON::Object::Entries { + {"source", "fs.realpath"}, + {"err", JSON::Object::Entries { + {"code", uv_fs_get_result(req)}, + {"message", String(uv_strerror(uv_fs_get_result(req)))} + }} + }; + } } else { json = JSON::Object::Entries { {"source", "fs.realpath"}, @@ -1799,12 +2346,9 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post {}); + ctx->callback(ctx->seq, json, Post {}); delete ctx; - }; - - auto err = uv_fs_realpath( - &core->eventLoop, &ctx->req, path.c_str(), uv_cb); + }); if (err < 0) { auto json = JSON::Object::Entries { @@ -1814,21 +2358,21 @@ namespace SSC { {"message", String(uv_strerror(err))} }} }; - ctx->cb(seq, json, Post{}); + ctx->callback(seq, json, Post{}); delete ctx; } }); } - void Core::FS::rename ( - const String seq, - const String pathA, - const String pathB, - const Module::Callback cb - ) { + void CoreFS::rename ( + const String& seq, + const String& pathA, + const String& pathB, + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(seq, callback); auto req = &ctx->req; auto src = pathA.c_str(); auto dst = pathB.c_str(); @@ -1853,7 +2397,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1866,26 +2410,46 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::copyFile ( - const String seq, - const String pathA, - const String pathB, + void CoreFS::copyFile ( + const String& seq, + const String& pathA, + const String& pathB, int flags, - Module::Callback cb + const CoreModule::Callback& callback ) { this->core->dispatchEventLoop([=, this]() { + auto src = std::make_shared<Descriptor>(this, 0, pathA); + auto dst = std::make_shared<Descriptor>(this, 0, pathB); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (src->resource.isAndroidLocalAsset() || src->resource.isAndroidLocalAsset()) { + auto bytes = src->resource.read(); + fs::remove(dst->resource.path.string()); + std::ofstream stream(dst->resource.path.string()); + stream << bytes; + stream.close(); + + auto json = JSON::Object::Entries { + {"source", "fs.copyFile"}, + {"data", JSON::Object::Entries { + {"result", 0} + }} + }; + + return callback(seq, json, Post{}); + } + #endif + auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(seq, callback); auto req = &ctx->req; - auto src = pathA.c_str(); - auto dst = pathB.c_str(); - auto err = uv_fs_copyfile(loop, req, src, dst, flags, [](uv_fs_t* req) { + auto err = uv_fs_copyfile(loop, req, src->resource.path.string().c_str(), dst->resource.path.string().c_str(), flags, [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; auto json = JSON::Object {}; @@ -1906,7 +2470,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1919,21 +2483,21 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::rmdir ( - const String seq, - const String path, - Module::Callback cb - ) { + void CoreFS::rmdir ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { auto filename = path.c_str(); auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(seq, callback); auto req = &ctx->req; auto err = uv_fs_rmdir(loop, req, filename, [](uv_fs_t* req) { auto ctx = (RequestContext *) req->data; @@ -1956,7 +2520,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }); @@ -1969,24 +2533,24 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::mkdir ( - const String seq, - const String path, + void CoreFS::mkdir ( + const String& seq, + const String& path, int mode, bool recursive, - Module::Callback cb - ) { + const CoreModule::Callback& callback + ) const { this->core->dispatchEventLoop([=, this]() { int err = 0; auto filename = path.c_str(); auto loop = &this->core->eventLoop; - auto ctx = new RequestContext(seq, cb); + auto ctx = new RequestContext(seq, callback); ctx->recursive = recursive; auto req = &ctx->req; const auto callback = [](uv_fs_t* req) { @@ -2013,7 +2577,7 @@ namespace SSC { }; } - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; }; @@ -2055,32 +2619,22 @@ namespace SSC { }} }; - ctx->cb(ctx->seq, json, Post{}); + ctx->callback(ctx->seq, json, Post{}); delete ctx; } }); } - void Core::FS::constants (const String seq, Module::Callback cb) { - static auto constants = getFSConstantsMap(); - static auto data = JSON::Object {constants}; - static auto json = JSON::Object::Entries { + void CoreFS::constants ( + const String& seq, + const CoreModule::Callback& callback + ) const { + static const auto data = JSON::Object(FS_CONSTANTS); + static const auto json = JSON::Object::Entries { {"source", "fs.constants"}, {"data", data} }; - static auto headers = Headers {{ - Headers::Header {"Cache-Control", "public, max-age=86400"} - }}; - - static auto post = Post { - .id = 0, - .ttl = 0, - .body = nullptr, - .length = 0, - .headers = headers.str() - }; - - cb(seq, json, post); + callback(seq, json, Post {}); } } diff --git a/src/core/modules/fs.hh b/src/core/modules/fs.hh new file mode 100644 index 0000000000..fd0c5afb5a --- /dev/null +++ b/src/core/modules/fs.hh @@ -0,0 +1,329 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_FS_H +#define SOCKET_RUNTIME_CORE_MODULE_FS_H + +#include "../file_system_watcher.hh" +#include "../module.hh" +#include "../resource.hh" +#include "../trace.hh" + +namespace SSC { + class Core; + class CoreFS : public CoreModule { + public: + using ID = uint64_t; + + struct Descriptor { + ID id; + Atomic<bool> retained = false; + Atomic<bool> stale = false; + FileResource resource; + Mutex mutex; + uv_dir_t *dir = nullptr; + uv_file fd = 0; + CoreFS* fs = nullptr; + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + // asset state + Android::Asset* androidAsset = nullptr; + Queue<String> androidAssetDirectoryEntries; + Queue<String> androidContentDirectoryEntries; + Android::ContentResolver::FileDescriptor androidContent = nullptr; + // type predicates + bool isAndroidAssetDirectory = false; + bool isAndroidContentDirectory = false; + bool isAndroidContent = false; + // descriptor offsets + off_t androidAssetOffset = 0; + off_t androidAssetLength = 0; + off_t androidContentOffset = 0; + off_t androidContentLength = 0; + #endif + + Descriptor (CoreFS* fs, ID id, const String& filename); + bool isDirectory () const; + bool isFile () const; + bool isRetained () const; + bool isStale () const; + }; + + struct RequestContext : CoreModule::RequestContext { + ID id; + SharedPointer<Descriptor> descriptor = nullptr; + SharedPointer<char[]> buffer = nullptr; + Tracer tracer; + uv_fs_t req; + uv_buf_t buf; + // 256 which corresponds to DirectoryHandle.MAX_BUFFER_SIZE + uv_dirent_t dirents[256]; + int offset = 0; + int result = 0; + bool recursive; + + RequestContext () = delete; + RequestContext (SharedPointer<Descriptor> descriptor) + : RequestContext(descriptor, "", nullptr) + {} + + RequestContext (const String& seq, const Callback& callback) + : RequestContext(nullptr, seq, callback) + {} + + RequestContext ( + SharedPointer<Descriptor> descriptor, + const String& seq, + const Callback& callback + ) : tracer("CoreFS::RequestContext") { + this->id = rand64(); + this->seq = seq; + this->req.data = (void*) this; + this->callback = callback; + this->descriptor = descriptor; + this->recursive = false; + this->req.loop = nullptr; + } + + ~RequestContext () { + if (this->req.loop) { + uv_fs_req_cleanup(&this->req); + } + } + + void setBuffer (SharedPointer<char[]> base, uint32_t size); + }; + + std::map<ID, SharedPointer<FileSystemWatcher>> watchers; + std::map<ID, SharedPointer<Descriptor>> descriptors; + Mutex mutex; + + CoreFS (Core* core) + : CoreModule(core) + {} + + SharedPointer<Descriptor> getDescriptor (ID id) const; + SharedPointer<Descriptor> getDescriptor (ID id); + void removeDescriptor (ID id); + bool hasDescriptor (ID id) const; + + void constants ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void access ( + const String& seq, + const String& path, + int mode, + const CoreModule::Callback& callback + ); + + void chmod ( + const String& seq, + const String& path, + int mode, + const CoreModule::Callback& callback + ) const; + + void chown ( + const String& seq, + const String& path, + uv_uid_t uid, + uv_gid_t gid, + const CoreModule::Callback& callback + ) const; + + void lchown ( + const String& seq, + const String& path, + uv_uid_t uid, + uv_gid_t gid, + const CoreModule::Callback& callback + ) const; + + void close ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void copyFile ( + const String& seq, + const String& src, + const String& dst, + int flags, + const CoreModule::Callback& callback + ); + + void closedir ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void closeOpenDescriptor ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void closeOpenDescriptors ( + const String& seq, + const CoreModule::Callback& callback + ); + + void closeOpenDescriptors ( + const String& seq, + bool preserveRetained, + const CoreModule::Callback& callback + ); + + void fstat ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void fsync ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) const; + + void ftruncate ( + const String& seq, + ID id, + int64_t offset, + const CoreModule::Callback& callback + ) const; + + void getOpenDescriptors ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void lstat ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ); + + void link ( + const String& seq, + const String& src, + const String& dest, + const CoreModule::Callback& callback + ) const; + + void symlink ( + const String& seq, + const String& src, + const String& dest, + int flags, + const CoreModule::Callback& callback + ) const; + + void mkdir ( + const String& seq, + const String& path, + int mode, + bool recursive, + const CoreModule::Callback& callback + ) const; + + void readlink ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ) const; + + void realpath ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ); + + void open ( + const String& seq, + ID id, + const String& path, + int flags, + int mode, + const CoreModule::Callback& callback + ); + + void opendir ( + const String& seq, + ID id, + const String& path, + const CoreModule::Callback& callback + ); + + void read ( + const String& seq, + ID id, + size_t len, + size_t offset, + const CoreModule::Callback& callback + ) const; + + void readdir ( + const String& seq, + ID id, + size_t entries, + const CoreModule::Callback& callback + ) const; + + void retainOpenDescriptor ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void rename ( + const String& seq, + const String& src, + const String& dst, + const CoreModule::Callback& callback + ) const; + + void rmdir ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ) const; + + void stat ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ); + + void stopWatch ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void unlink ( + const String& seq, + const String& path, + const CoreModule::Callback& callback + ) const; + + void watch ( + const String& seq, + ID id, + const String& path, + const CoreModule::Callback& callback + ); + + void write ( + const String& seq, + ID id, + SharedPointer<char[]> bytes, + size_t size, + size_t offset, + const CoreModule::Callback& callback + ) const; + }; +} +#endif diff --git a/src/core/modules/geolocation.cc b/src/core/modules/geolocation.cc new file mode 100644 index 0000000000..49c5a95261 --- /dev/null +++ b/src/core/modules/geolocation.cc @@ -0,0 +1,477 @@ +#include "geolocation.hh" +#include "../debug.hh" + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@implementation SSCLocationPositionWatcher ++ (SSCLocationPositionWatcher*) positionWatcherWithIdentifier: (NSInteger) identifier + completion: (void (^)(CLLocation*)) completion +{ + auto watcher= [SSCLocationPositionWatcher new]; + watcher.identifier = identifier; + watcher.completion = [completion copy]; + return watcher; +} +@end + +@implementation SSCLocationObserver +- (id) init { + self = [super init]; + self.delegate = [[SSCLocationManagerDelegate alloc] initWithLocationObserver: self]; + self.isAuthorized = NO; + self.locationWatchers = [NSMutableArray new]; + self.activationCompletions = [NSMutableArray new]; + self.locationRequestCompletions = [NSMutableArray new]; + + self.locationManager = [CLLocationManager new]; + self.locationManager.delegate = self.delegate; + self.locationManager.desiredAccuracy = CLAccuracyAuthorizationFullAccuracy; + self.locationManager.pausesLocationUpdatesAutomatically = NO; + +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + self.locationManager.allowsBackgroundLocationUpdates = YES; + self.locationManager.showsBackgroundLocationIndicator = YES; +#endif + + if ([CLLocationManager locationServicesEnabled]) { + if ( + #if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorized || + #else + self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse || + #endif + self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways + ) { + self.isAuthorized = YES; + } + } + + return self; +} + +- (BOOL) attemptActivation { + if ([CLLocationManager locationServicesEnabled] == NO) { + return NO; + } + + if (self.isAuthorized) { + [self.locationManager requestLocation]; + return YES; + } + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + [self.locationManager requestWhenInUseAuthorization]; +#else + [self.locationManager requestAlwaysAuthorization]; +#endif + + return YES; +} + +- (BOOL) attemptActivationWithCompletion: (void (^)(BOOL)) completion { + if (self.isAuthorized) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(YES); + }); + return YES; + } + + if ([self attemptActivation]) { + [self.activationCompletions addObject: [completion copy]]; + return YES; + } + + return NO; +} + +- (BOOL) getCurrentPositionWithCompletion: (void (^)(NSError*, CLLocation*)) completion { + return [self attemptActivationWithCompletion: ^(BOOL isAuthorized) { + auto userConfig = SSC::getUserConfig(); + if (!isAuthorized) { + auto reason = @("Location observer could not be activated"); + + if (!self.locationManager) { + reason = @("Location observer manager is not initialized"); + } else if (!self.locationManager.location) { + reason = @("Location observer manager could not provide location"); + } + + auto error = [NSError + errorWithDomain: @(userConfig["meta_bundle_identifier"].c_str()) + code: -1 + userInfo: @{ + NSLocalizedDescriptionKey: reason + } + ]; + + return completion(error, nullptr); + } + + auto location = self.locationManager.location; + if (location.timestamp.timeIntervalSince1970 > 0) { + completion(nullptr, self.locationManager.location); + } else { + [self.locationRequestCompletions addObject: [completion copy]]; + } + + [self.locationManager requestLocation]; + }]; +} + +- (int) watchPositionForIdentifier: (NSInteger) identifier + completion: (void (^)(NSError*, CLLocation*)) completion { + SSCLocationPositionWatcher* watcher = nullptr; + BOOL exists = NO; + + for (SSCLocationPositionWatcher* existing in self.locationWatchers) { + if (existing.identifier == identifier) { + watcher = existing; + exists = YES; + break; + } + } + + if (!watcher) { + watcher = [SSCLocationPositionWatcher + positionWatcherWithIdentifier: identifier + completion: ^(CLLocation* location) { + completion(nullptr, location); + }]; + } + + const auto performedActivation = [self attemptActivationWithCompletion: ^(BOOL isAuthorized) { + auto userConfig = SSC::getUserConfig(); + if (!isAuthorized) { + auto error = [NSError + errorWithDomain: @(userConfig["meta_bundle_identifier"].c_str()) + code: -1 + userInfo: @{ + @"Error reason": @("Location observer could not be activated") + } + ]; + + return completion(error, nullptr); + } + + [self.locationManager startUpdatingLocation]; + + if (CLLocationManager.headingAvailable) { + [self.locationManager startUpdatingHeading]; + } + + [self.locationManager startMonitoringSignificantLocationChanges]; + }]; + + if (!performedActivation) { + #if !__has_feature(objc_arc) + [watcher release]; + #endif + return -1; + } + + if (!exists) { + [self.locationWatchers addObject: watcher]; + } + + return identifier; +} + +- (BOOL) clearWatch: (NSInteger) identifier { + for (SSCLocationPositionWatcher* watcher in self.locationWatchers) { + if (watcher.identifier == identifier) { + [self.locationWatchers removeObject: watcher]; + #if !__has_feature(objc_arc) + [watcher release]; + #endif + return YES; + } + } + + return NO; +} +@end + +@implementation SSCLocationManagerDelegate +- (id) initWithLocationObserver: (SSCLocationObserver*) locationObserver { + self = [super init]; + self.locationObserver = locationObserver; + locationObserver.delegate = self; + return self; +} + +- (void) locationManager: (CLLocationManager*) locationManager + didUpdateLocations: (NSArray<CLLocation*>*) locations { + auto locationRequestCompletions = [NSArray arrayWithArray: self.locationObserver.locationRequestCompletions]; + for (id item in locationRequestCompletions) { + auto completion = (void (^)(CLLocation*)) item; + completion(locations.firstObject); + [self.locationObserver.locationRequestCompletions removeObject: item]; + #if !__has_feature(objc_arc) + [completion release]; + #endif + } + + for (SSCLocationPositionWatcher* watcher in self.locationObserver.locationWatchers) { + watcher.completion(locations.firstObject); + } +} + +- (void) locationManager: (CLLocationManager*) locationManager + didFailWithError: (NSError*) error { + // TODO(@jwerle): handle location manager error + debug("locationManager:didFailWithError: %@", error); +} + +- (void) locationManager: (CLLocationManager*) locationManager + didFinishDeferredUpdatesWithError: (NSError*) error { + // TODO(@jwerle): handle deferred error + debug("locationManager:didFinishDeferredUpdatesWithError: %@", error); +} + +- (void) locationManagerDidPauseLocationUpdates: (CLLocationManager*) locationManager { + // TODO(@jwerle): handle pause for updates + debug("locationManagerDidPauseLocationUpdates"); +} + +- (void) locationManagerDidResumeLocationUpdates: (CLLocationManager*) locationManager { + // TODO(@jwerle): handle resume for updates + debug("locationManagerDidResumeLocationUpdates"); +} + +- (void) locationManager: (CLLocationManager*) locationManager + didVisit: (CLVisit*) visit { + auto locations = [NSArray arrayWithObject: locationManager.location]; + [self locationManager: locationManager didUpdateLocations: locations]; +} + +- (void) locationManager: (CLLocationManager*) locationManager + didChangeAuthorizationStatus: (CLAuthorizationStatus) status { + // XXX(@jwerle): this is a legacy callback + [self locationManagerDidChangeAuthorization: locationManager]; +} + +- (void) locationManagerDidChangeAuthorization: (CLLocationManager*) locationManager { + using namespace SSC; + auto activationCompletions = [NSArray arrayWithArray: self.locationObserver.activationCompletions]; + if ( + #if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + locationManager.authorizationStatus == kCLAuthorizationStatusAuthorized || + #else + locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse || + #endif + locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways + ) { + JSON::Object json = JSON::Object::Entries { + {"state", "granted"} + }; + + self.locationObserver.geolocation->permissionChangeObservers.dispatch(json); + self.locationObserver.isAuthorized = YES; + for (id item in activationCompletions) { + auto completion = (void (^)(BOOL)) item; + completion(YES); + [self.locationObserver.activationCompletions removeObject: item]; + #if !__has_feature(objc_arc) + [completion release]; + #endif + } + } else { + JSON::Object json = JSON::Object::Entries { + {"state", locationManager.authorizationStatus == kCLAuthorizationStatusNotDetermined + ? "prompt" + : "denied" + } + }; + + self.locationObserver.geolocation->permissionChangeObservers.dispatch(json); + self.locationObserver.isAuthorized = NO; + for (id item in activationCompletions) { + auto completion = (void (^)(BOOL)) item; + completion(NO); + [self.locationObserver.activationCompletions removeObject: item]; + #if !__has_feature(objc_arc) + [completion release]; + #endif + } + } +} +@end + +#endif + +namespace SSC { + CoreGeolocation::CoreGeolocation (Core* core) + : CoreModule(core), + permissionChangeObservers() + { + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->locationObserver = [SSCLocationObserver new]; + this->locationPositionWatcher = [SSCLocationPositionWatcher new]; + this->locationManagerDelegate = [SSCLocationManagerDelegate new]; + this->locationObserver.geolocation = this; + #endif + } + + CoreGeolocation::~CoreGeolocation () { + #if SOCKET_RUNTIME_PLATFORM_APPLE + #if !__has_feature(objc_arc) + [this->locationObserver release]; + [this->locationPositionWatcher release]; + [this->locationManagerDelegate release]; + #endif + this->locationObserver = nullptr; + this->locationPositionWatcher = nullptr; + this->locationManagerDelegate = nullptr; + #endif + } + + void CoreGeolocation::getCurrentPosition ( + const String& seq, + const CoreModule::Callback& callback + ) const { + bool performedActivation = false; + #if SOCKET_RUNTIME_PLATFORM_APPLE + performedActivation = [this->locationObserver getCurrentPositionWithCompletion: ^(NSError* error, CLLocation* location) { + if (error != nullptr) { + const auto message = String( + error.localizedDescription.UTF8String != nullptr + ? error.localizedDescription.UTF8String + : "An unknown error occurred" + ); + + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"type", "CoreGeolocationPositionError"}, + {"message", message} + }} + }; + + return callback(seq, json, Post {}); + } + + const auto heading = this->locationObserver.locationManager.heading; + const auto json = JSON::Object::Entries { + {"coords", JSON::Object::Entries { + {"latitude", location.coordinate.latitude}, + {"longitude", location.coordinate.longitude}, + {"altitude", location.altitude}, + {"accuracy", location.horizontalAccuracy}, + {"altitudeAccuracy", location.verticalAccuracy}, + {"floorLevel", location.floor.level}, + {"heading", heading.trueHeading}, + {"speed", location.speed} + }} + }; + + callback(seq, json, Post {}); + }]; + #endif + + if (!performedActivation) { + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"type", "CoreGeolocationPositionError"}, + {"message", "Failed to get position"} + }} + }; + + callback(seq, json, Post {}); + } + } + + void CoreGeolocation::watchPosition ( + const String& seq, + WatchID id, + const CoreModule::Callback& callback + ) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + const int identifier = [this->locationObserver watchPositionForIdentifier: id completion: ^(NSError* error, CLLocation* location) { + if (error != nullptr) { + const auto message = String( + error.localizedDescription.UTF8String != nullptr + ? error.localizedDescription.UTF8String + : "An unknown error occurred" + ); + + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"type", "CoreGeolocationPositionError"}, + {"message", message} + }} + }; + + return callback(seq, json, Post {}); + } + + const auto heading = this->locationObserver.locationManager.heading; + const auto json = JSON::Object::Entries { + {"watch", JSON::Object::Entries { + {"identifier", identifier}, + }}, + {"coords", JSON::Object::Entries { + {"latitude", location.coordinate.latitude}, + {"longitude", location.coordinate.longitude}, + {"altitude", location.altitude}, + {"accuracy", location.horizontalAccuracy}, + {"altitudeAccuracy", location.verticalAccuracy}, + {"floorLevel", location.floor.level}, + {"heading", heading.trueHeading}, + {"speed", location.speed} + }} + }; + + callback("-1", json, Post {}); + }]; + + if (identifier != -1) { + const auto json = JSON::Object::Entries { + {"watch", JSON::Object::Entries { + {"identifier", identifier} + }} + }; + + return callback(seq, json, Post {}); + } + #endif + + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"type", "CoreGeolocationPositionError"}, + {"message", "Failed to watch position"} + }} + }; + + callback(seq, json, Post {}); + } + + void CoreGeolocation::clearWatch ( + const String& seq, + WatchID id, + const CoreModule::Callback& callback + ) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + [this->locationObserver clearWatch: id]; + #endif + + callback(seq, JSON::Object {}, Post {}); + } + + template<> bool CoreModule::template Observers<CoreModule::template Observer<JSON::Object>>::add( + const CoreModule::Observer<JSON::Object>&, + CoreModule::Observer<JSON::Object>::Callback + ); + + template<> bool CoreGeolocation::PermissionChangeObservers::remove( + const CoreGeolocation::PermissionChangeObserver& + ); + + bool CoreGeolocation::addPermissionChangeObserver ( + const PermissionChangeObserver& observer, + const PermissionChangeObserver::Callback callback + ) { + return this->permissionChangeObservers.add(observer, callback); + } + + bool CoreGeolocation::removePermissionChangeObserver (const PermissionChangeObserver& observer) { + return this->permissionChangeObservers.remove(observer); + } +} diff --git a/src/core/modules/geolocation.hh b/src/core/modules/geolocation.hh new file mode 100644 index 0000000000..c3ee30a4c3 --- /dev/null +++ b/src/core/modules/geolocation.hh @@ -0,0 +1,108 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_GEOLOCATION_H +#define SOCKET_RUNTIME_CORE_MODULE_GEOLOCATION_H + +#include "../module.hh" + +namespace SSC { + // forward + class Core; + class CoreGeolocation; +} + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@class SSCLocationObserver; + +@interface SSCLocationManagerDelegate : NSObject<CLLocationManagerDelegate> +@property (nonatomic, strong) SSCLocationObserver* locationObserver; + +- (id) initWithLocationObserver: (SSCLocationObserver*) locationObserver; + +- (void) locationManager: (CLLocationManager*) locationManager + didFailWithError: (NSError*) error; + +- (void) locationManager: (CLLocationManager*) locationManager + didUpdateLocations: (NSArray<CLLocation*>*) locations; + +- (void) locationManager: (CLLocationManager*) locationManager + didFinishDeferredUpdatesWithError: (NSError*) error; + +- (void) locationManagerDidPauseLocationUpdates: (CLLocationManager*) locationManager; +- (void) locationManagerDidResumeLocationUpdates: (CLLocationManager*) locationManager; +- (void) locationManager: (CLLocationManager*) locationManager + didVisit: (CLVisit*) visit; + +- (void) locationManager: (CLLocationManager*) locationManager + didChangeAuthorizationStatus: (CLAuthorizationStatus) status; +- (void) locationManagerDidChangeAuthorization: (CLLocationManager*) locationManager; +@end + +@interface SSCLocationPositionWatcher : NSObject +@property (nonatomic, assign) NSInteger identifier; +@property (nonatomic, assign) void(^completion)(CLLocation*); ++ (SSCLocationPositionWatcher*) positionWatcherWithIdentifier: (NSInteger) identifier + completion: (void (^)(CLLocation*)) completion; +@end + +@interface SSCLocationObserver : NSObject +@property (nonatomic, retain) CLLocationManager* locationManager; +@property (nonatomic, retain) SSCLocationManagerDelegate* delegate; +@property (atomic, retain) NSMutableArray* activationCompletions; +@property (atomic, retain) NSMutableArray* locationRequestCompletions; +@property (atomic, retain) NSMutableArray* locationWatchers; +@property (nonatomic) SSC::CoreGeolocation* geolocation; +@property (atomic, assign) BOOL isAuthorized; +- (BOOL) attemptActivation; +- (BOOL) attemptActivationWithCompletion: (void (^)(BOOL)) completion; +- (BOOL) getCurrentPositionWithCompletion: (void (^)(NSError*, CLLocation*)) completion; +- (int) watchPositionForIdentifier: (NSInteger) identifier + completion: (void (^)(NSError*, CLLocation*)) completion; +- (BOOL) clearWatch: (NSInteger) identifier; +@end +#endif + +namespace SSC { + class CoreGeolocation : public CoreModule { + public: + using WatchID = uint64_t; + using PermissionChangeObserver = CoreModule::Observer<JSON::Object>; + using PermissionChangeObservers = CoreModule::Observers<PermissionChangeObserver>; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + SSCLocationObserver* locationObserver = nullptr; + SSCLocationPositionWatcher* locationPositionWatcher = nullptr; + SSCLocationManagerDelegate* locationManagerDelegate = nullptr; + #endif + + PermissionChangeObservers permissionChangeObservers; + + CoreGeolocation (Core* core); + ~CoreGeolocation (); + + void getCurrentPosition ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void watchPosition ( + const String& seq, + const WatchID id, + const CoreModule::Callback& callback + ); + + void clearWatch ( + const String& seq, + const WatchID id, + const CoreModule::Callback& callback + ); + + bool removePermissionChangeObserver ( + const PermissionChangeObserver& observer + ); + + bool addPermissionChangeObserver ( + const PermissionChangeObserver& observer, + const PermissionChangeObserver::Callback callback + ); + }; +} +#endif diff --git a/src/core/modules/media_devices.cc b/src/core/modules/media_devices.cc new file mode 100644 index 0000000000..4344cfc9d1 --- /dev/null +++ b/src/core/modules/media_devices.cc @@ -0,0 +1,31 @@ +#include "media_devices.hh" +#include "../debug.hh" + +namespace SSC { + CoreMediaDevices::CoreMediaDevices (Core* core) + : CoreModule(core), + permissionChangeObservers() + {} + + CoreMediaDevices::~CoreMediaDevices () {} + + template<> bool CoreModule::template Observers<CoreModule::template Observer<JSON::Object>>::add( + const CoreModule::Observer<JSON::Object>&, + CoreModule::Observer<JSON::Object>::Callback + ); + + template<> bool CoreMediaDevices::PermissionChangeObservers::remove( + const CoreMediaDevices::PermissionChangeObserver& + ); + + bool CoreMediaDevices::addPermissionChangeObserver ( + const PermissionChangeObserver& observer, + const PermissionChangeObserver::Callback callback + ) { + return this->permissionChangeObservers.add(observer, callback); + } + + bool CoreMediaDevices::removePermissionChangeObserver (const PermissionChangeObserver& observer) { + return this->permissionChangeObservers.remove(observer); + } +} diff --git a/src/core/modules/media_devices.hh b/src/core/modules/media_devices.hh new file mode 100644 index 0000000000..119bc26c2d --- /dev/null +++ b/src/core/modules/media_devices.hh @@ -0,0 +1,27 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_MEDIA_DEVICES_H +#define SOCKET_RUNTIME_CORE_MODULE_MEDIA_DEVICES_H + +#include "../module.hh" + +namespace SSC { + class CoreMediaDevices : public CoreModule { + public: + using PermissionChangeObserver = CoreModule::Observer<JSON::Object>; + using PermissionChangeObservers = CoreModule::Observers<PermissionChangeObserver>; + + PermissionChangeObservers permissionChangeObservers; + + CoreMediaDevices (Core* core); + ~CoreMediaDevices (); + + bool removePermissionChangeObserver ( + const PermissionChangeObserver& observer + ); + + bool addPermissionChangeObserver ( + const PermissionChangeObserver& observer, + const PermissionChangeObserver::Callback callback + ); + }; +} +#endif diff --git a/src/core/modules/network_status.cc b/src/core/modules/network_status.cc new file mode 100644 index 0000000000..7cd15379c7 --- /dev/null +++ b/src/core/modules/network_status.cc @@ -0,0 +1,128 @@ +#include "network_status.hh" + +namespace SSC { + CoreNetworkStatus::CoreNetworkStatus (Core* core) + : CoreModule(core) + { + #if SOCKET_RUNTIME_PLATFORM_APPLE + dispatch_queue_attr_t attrs = dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_SERIAL, + QOS_CLASS_UTILITY, + DISPATCH_QUEUE_PRIORITY_DEFAULT + ); + + this->queue = dispatch_queue_create( + "socket.runtime.queue.ipc.network-status", + attrs + ); + + #elif SOCKET_RUNTIME_PLATFORM_LINUX + this->monitor = g_network_monitor_get_default(); + #endif + } + + CoreNetworkStatus::~CoreNetworkStatus () { + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->stop(); + dispatch_release(this->queue); + this->monitor = nullptr; + this->queue = nullptr; + #endif + } + + bool CoreNetworkStatus::start () { + this->stop(); + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->monitor = nw_path_monitor_create(); + + nw_path_monitor_set_queue(this->monitor, this->queue); + nw_path_monitor_set_update_handler(this->monitor, ^(nw_path_t path) { + if (path == nullptr) { + return; + } + + nw_path_status_t status = nw_path_get_status(path); + + String name; + String message; + + switch (status) { + case nw_path_status_invalid: { + name = "offline"; + message = "Network path is invalid"; + break; + } + case nw_path_status_satisfied: { + name = "online"; + message = "Network is usable"; + break; + } + case nw_path_status_satisfiable: { + name = "online"; + message = "Network may be usable"; + break; + } + case nw_path_status_unsatisfied: { + name = "offline"; + message = "Network is not usable"; + break; + } + } + + const auto json = JSON::Object::Entries { + {"name", name}, + {"message", message} + }; + + this->observers.dispatch(json); + }); + nw_path_monitor_start(this->monitor); + return true; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + this->signal = g_signal_connect( + this->monitor, + "network-changed", + G_CALLBACK((+[](GNetworkMonitor* monitor, gboolean networkAvailable, gpointer userData) { + auto coreNetworkStatus = reinterpret_cast<CoreNetworkStatus*>(userData); + if (coreNetworkStatus) { + const auto json = JSON::Object::Entries { + {"name", networkAvailable ? "online" : "offline"}, + {"message", networkAvailable ? "Network is usable" : "Network is not usable"} + }; + + coreNetworkStatus->observers.dispatch(json); + } + })), + this + ); + return true; + #endif + return false; + } + + bool CoreNetworkStatus::stop () { + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->monitor) { + nw_path_monitor_cancel(this->monitor); + nw_release(this->monitor); + } + this->monitor = nullptr; + return true; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + if (this->signal) { + g_signal_handler_disconnect(this->monitor, this->signal); + this->signal = 0; + return true; + } + #endif + return false; + } + + bool CoreNetworkStatus::addObserver (const Observer& observer, const Observer::Callback callback) { + return this->observers.add(observer, callback); + } + + bool CoreNetworkStatus::removeObserver (const Observer& observer) { + return this->observers.remove(observer); + } +} diff --git a/src/core/modules/network_status.hh b/src/core/modules/network_status.hh new file mode 100644 index 0000000000..5d59c7e9f9 --- /dev/null +++ b/src/core/modules/network_status.hh @@ -0,0 +1,37 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_NETWORK_STATUS_H +#define SOCKET_RUNTIME_CORE_MODULE_NETWORK_STATUS_H + +#include "../json.hh" +#include "../module.hh" + +namespace SSC { + class CoreNetworkStatus : public CoreModule { + public: + using Observer = CoreModule::Observer<JSON::Object>; + using Observers = CoreModule::Observers<Observer>; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + dispatch_queue_t queue = nullptr; + nw_path_monitor_t monitor = nullptr; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + GNetworkMonitor* monitor = nullptr; + guint signal = 0; + #endif + + Observers observers; + + CoreNetworkStatus (Core*); + ~CoreNetworkStatus (); + + bool start (); + bool stop (); + bool addObserver ( + const Observer& observer, + const Observer::Callback callback = nullptr + ); + + bool removeObserver (const Observer& observer); + }; +} + +#endif diff --git a/src/core/modules/notifications.cc b/src/core/modules/notifications.cc new file mode 100644 index 0000000000..1e8668e9df --- /dev/null +++ b/src/core/modules/notifications.cc @@ -0,0 +1,528 @@ +#include "../resource.hh" +#include "../debug.hh" +#include "../core.hh" +#include "../url.hh" + +#include "notifications.hh" + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@implementation SSCUserNotificationCenterDelegate +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + didReceiveNotificationResponse: (UNNotificationResponse*) response + withCompletionHandler: (void (^)(void)) completionHandler +{ + using namespace SSC; + + const auto id = String(response.notification.request.identifier.UTF8String); + const auto action = ( + [response.actionIdentifier isEqualToString: UNNotificationDefaultActionIdentifier] + ? "default" + : "dismiss" + ); + + const auto json = JSON::Object::Entries { + {"id", id}, + {"action", action} + }; + + self.notifications->notificationResponseObservers.dispatch(json); + + completionHandler(); +} + +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + willPresentNotification: (UNNotification*) notification + withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler +{ + using namespace SSC; + UNNotificationPresentationOptions options = UNNotificationPresentationOptionList; + const auto __block id = String(notification.request.identifier.UTF8String); + + if (notification.request.content.sound != nullptr) { + options |= UNNotificationPresentationOptionSound; + } + + if (notification.request.content.attachments != nullptr) { + if (notification.request.content.attachments.count > 0) { + options |= UNNotificationPresentationOptionBanner; + } + } + + self.notifications->notificationPresentedObservers.dispatch(JSON::Object::Entries { + {"id", id} + }); + + // look for dismissed notification + auto timer = [NSTimer timerWithTimeInterval: 2 repeats: YES block: ^(NSTimer* timer) { + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + // if notification that was presented is not in the delivered notifications then + // then notify that the notification was "dismissed" + [notificationCenter getDeliveredNotificationsWithCompletionHandler: ^(NSArray<UNNotification*> *notifications) { + for (UNNotification* notification in notifications) { + if (String(notification.request.identifier.UTF8String) == id) { + return; + } + } + + [timer invalidate]; + + self.notifications->notificationResponseObservers.dispatch(JSON::Object::Entries { + {"id", id}, + {"action", "dismiss"} + }); + }]; + }]; + + [NSRunLoop.mainRunLoop + addTimer: timer + forMode: NSDefaultRunLoopMode + ]; +} +@end +#endif + +namespace SSC { + const JSON::Object CoreNotifications::Notification::json () const { + return JSON::Object::Entries { + {"id", this->identifier} + }; + } + + CoreNotifications::CoreNotifications (Core* core) + : CoreModule(core), + permissionChangeObservers(), + notificationResponseObservers(), + notificationPresentedObservers() + {} + + CoreNotifications::~CoreNotifications () { + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (!core->options.features.useNotifications) return; + + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + + if (notificationCenter.delegate == this->userNotificationCenterDelegate) { + notificationCenter.delegate = nullptr; + } + + [this->userNotificationCenterPollTimer invalidate]; + + #if !__has_feature(objc_arc) + [this->userNotificationCenterDelegate release]; + #endif + + this->userNotificationCenterDelegate = nullptr; + this->userNotificationCenterPollTimer = nullptr; + #endif + } + + void CoreNotifications::start () { + if (!this->core->options.features.useNotifications) return; + this->stop(); + #if SOCKET_RUNTIME_PLATFORM_APPLE + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + + this->userNotificationCenterDelegate = [SSCUserNotificationCenterDelegate new]; + this->userNotificationCenterDelegate.notifications = this; + + if (!notificationCenter.delegate) { + notificationCenter.delegate = this->userNotificationCenterDelegate; + } + + [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { + this->currentUserNotificationAuthorizationStatus = settings.authorizationStatus; + this->userNotificationCenterPollTimer = [NSTimer timerWithTimeInterval: 2 repeats: YES block: ^(NSTimer* timer) { + // look for authorization status changes + [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { + JSON::Object json; + if (this->currentUserNotificationAuthorizationStatus != settings.authorizationStatus) { + this->currentUserNotificationAuthorizationStatus = settings.authorizationStatus; + + if (settings.authorizationStatus == UNAuthorizationStatusDenied) { + json = JSON::Object::Entries {{"state", "denied"}}; + } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { + json = JSON::Object::Entries {{"state", "prompt"}}; + } else { + json = JSON::Object::Entries {{"state", "granted"}}; + } + + this->permissionChangeObservers.dispatch(json); + } + }]; + }]; + + [NSRunLoop.mainRunLoop + addTimer: this->userNotificationCenterPollTimer + forMode: NSDefaultRunLoopMode + ]; + }]; + #endif + } + + void CoreNotifications::stop () { + if (!this->core->options.features.useNotifications) return; + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->userNotificationCenterPollTimer) { + [this->userNotificationCenterPollTimer invalidate]; + this->userNotificationCenterPollTimer = nullptr; + } + #endif + } + + bool CoreNotifications::addPermissionChangeObserver ( + const PermissionChangeObserver& observer, + const PermissionChangeObserver::Callback& callback + ) { + return this->permissionChangeObservers.add(observer, callback); + } + + bool CoreNotifications::removePermissionChangeObserver (const PermissionChangeObserver& observer) { + return this->permissionChangeObservers.remove(observer); + } + + bool CoreNotifications::addNotificationResponseObserver ( + const NotificationResponseObserver& observer, + const NotificationResponseObserver::Callback& callback + ) { + return this->notificationResponseObservers.add(observer, callback); + } + + bool CoreNotifications::removeNotificationResponseObserver (const NotificationResponseObserver& observer) { + return this->notificationResponseObservers.remove(observer); + } + + bool CoreNotifications::addNotificationPresentedObserver ( + const NotificationPresentedObserver& observer, + const NotificationPresentedObserver::Callback& callback + ) { + return this->notificationPresentedObservers.add(observer, callback); + } + + bool CoreNotifications::removeNotificationPresentedObserver (const NotificationPresentedObserver& observer) { + return this->notificationPresentedObservers.remove(observer); + } + + bool CoreNotifications::show (const ShowOptions& options, const ShowCallback& callback) { + if (options.id.size() == 0) { + callback(ShowResult { "Missing 'id' in CoreNotifications::ShowOptions" }); + return false; + } + + if (!this->core->permissions.hasRuntimePermission("notifications")) { + callback(ShowResult { "Runtime permission is disabled for 'notifications'" }); + return false; + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + auto attachments = [NSMutableArray array]; + auto userInfo = [NSMutableDictionary dictionary]; + auto content = [UNMutableNotificationContent new]; + + NSError* error = nullptr; + + auto id = options.id; + + if (options.tag.size() > 0) { + userInfo[@"tag"] = @(options.tag.c_str()); + content.threadIdentifier = @(options.tag.c_str()); + } + + if (options.lang.size() > 0) { + userInfo[@"lang"] = @(options.lang.c_str()); + } + + if (options.silent == false) { + content.sound = [UNNotificationSound defaultSound]; + } + + if (options.icon.size() > 0) { + NSURL* iconURL = nullptr; + + const auto url = URL(options.icon); + + if (options.icon.starts_with("socket://")) { + const auto path = FileResource::getResourcePath(url.pathname); + iconURL = [NSURL fileURLWithPath: @(path.string().c_str())]; + } else { + iconURL = [NSURL fileURLWithPath: @(url.href.c_str())]; + } + + const auto types = [UTType + typesWithTag: iconURL.pathExtension + tagClass: UTTagClassFilenameExtension + conformingToType: nullptr + ]; + + auto options = [NSMutableDictionary dictionary]; + + if (types.count > 0) { + options[UNNotificationAttachmentOptionsTypeHintKey] = types.firstObject.preferredMIMEType; + }; + + auto attachment = [UNNotificationAttachment + attachmentWithIdentifier: @("") + URL: iconURL + options: options + error: &error + ]; + + if (error != nullptr) { + const auto message = String( + error.localizedDescription.UTF8String != nullptr + ? error.localizedDescription.UTF8String + : "An unknown error occurred" + ); + + callback(ShowResult { message }); + #if !__has_feature(objc_arc) + [content release]; + #endif + return false; + } + + [attachments addObject: attachment]; + } else { + // using an asset from the resources directory will require a code signed application + const auto path = FileResource::getResourcePath(String("icon.png")); + + if (FileResource(path).exists()) { + const auto iconURL = [NSURL fileURLWithPath: @(path.string().c_str())]; + const auto types = [UTType + typesWithTag: iconURL.pathExtension + tagClass: UTTagClassFilenameExtension + conformingToType: nullptr + ]; + + auto options = [NSMutableDictionary dictionary]; + auto attachment = [UNNotificationAttachment + attachmentWithIdentifier: @("") + URL: iconURL + options: options + error: &error + ]; + + if (error != nullptr) { + auto message = String( + error.localizedDescription.UTF8String != nullptr + ? error.localizedDescription.UTF8String + : "An unknown error occurred" + ); + + callback(ShowResult { message }); + #if !__has_feature(objc_arc) + [content release]; + #endif + return false; + } + + [attachments addObject: attachment]; + } + } + + if (options.image.size() > 0) { + NSError* error = nullptr; + NSURL* imageURL = nullptr; + + const auto url = URL(options.image); + + if (options.image.starts_with("socket://")) { + const auto path = FileResource::getResourcePath(url.pathname); + imageURL = [NSURL fileURLWithPath: @(path.string().c_str())]; + } else { + imageURL = [NSURL fileURLWithPath: @(url.href.c_str())]; + } + + const auto types = [UTType + typesWithTag: imageURL.pathExtension + tagClass: UTTagClassFilenameExtension + conformingToType: nullptr + ]; + + auto options = [NSMutableDictionary dictionary]; + + if (types.count > 0) { + options[UNNotificationAttachmentOptionsTypeHintKey] = types.firstObject.preferredMIMEType; + }; + + auto attachment = [UNNotificationAttachment + attachmentWithIdentifier: @("") + URL: imageURL + options: options + error: &error + ]; + + if (error != nullptr) { + const auto message = String( + error.localizedDescription.UTF8String != nullptr + ? error.localizedDescription.UTF8String + : "An unknown error occurred" + ); + + callback(ShowResult { message }); + #if !__has_feature(objc_arc) + [content release]; + #endif + return false; + } + + [attachments addObject: attachment]; + } + + content.attachments = attachments; + content.userInfo = userInfo; + content.title = @(options.title.c_str()); + content.body = @(options.body.c_str()); + + auto request = [UNNotificationRequest + requestWithIdentifier: @(id.c_str()) + content: content + trigger: nil + ]; + + auto cb = callback; + [notificationCenter addNotificationRequest: request withCompletionHandler: ^(NSError* error) { + if (error != nullptr) { + const auto message = String( + error.localizedDescription.UTF8String != nullptr + ? error.localizedDescription.UTF8String + : "An unknown error occurred" + ); + + this->core->dispatchEventLoop([=] () { + cb(ShowResult { message }); + }); + #if !__has_feature(objc_arc) + [content release]; + #endif + return; + } + + this->core->dispatchEventLoop([=] () { + cb(ShowResult { "", id }); + }); + }]; + + return true; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.showNotification( + // id, + // title, + // body, + // tag, + // channel, + // category, + // silent, + // iconURL, + // imageURL, + // vibratePattern + // )` + const auto success = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "showNotification", + "(" + "Ljava/lang/String;" // id + "Ljava/lang/String;" // title + "Ljava/lang/String;" // body + "Ljava/lang/String;" // tag + "Ljava/lang/String;" // channel + "Ljava/lang/String;" // category + "Z" // silent + "Ljava/lang/String;" // iconURL + "Ljava/lang/String;" // imageURL + "Ljava/lang/String;" // vibratePattern + ")Z", + attachment.env->NewStringUTF(options.id.c_str()), + attachment.env->NewStringUTF(options.title.c_str()), + attachment.env->NewStringUTF(options.body.c_str()), + attachment.env->NewStringUTF(options.tag.c_str()), + attachment.env->NewStringUTF(options.channel.c_str()), + attachment.env->NewStringUTF(options.category.c_str()), + options.silent, + attachment.env->NewStringUTF(options.icon.c_str()), + attachment.env->NewStringUTF(options.image.c_str()), + attachment.env->NewStringUTF(options.vibrate.c_str()) + ); + + this->core->dispatchEventLoop([=, this] () { + callback(ShowResult { "", options.id }); + this->notificationPresentedObservers.dispatch(JSON::Object::Entries { + {"id", options.id} + }); + }); + return success; + #endif + return false; + } + + bool CoreNotifications::close (const Notification& notification) { + if (!this->core->permissions.hasRuntimePermission("notifications")) { + return false; + } + #if SOCKET_RUNTIME_PLATFORM_APPLE + const auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + const auto identifiers = @[@(notification.identifier.c_str())]; + const auto json = JSON::Object::Entries { + {"id", notification.identifier}, + {"action", "dismiss"} + }; + + [notificationCenter removePendingNotificationRequestsWithIdentifiers: identifiers]; + [notificationCenter removeDeliveredNotificationsWithIdentifiers: identifiers]; + + this->notificationResponseObservers.dispatch(json); + return true; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.showNotification( + // id, + // tag, + // )` + const auto success = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "closeNotification", + "(" + "Ljava/lang/String;" // id + "Ljava/lang/String;" // tag + ")Z", + attachment.env->NewStringUTF(notification.identifier.c_str()), + attachment.env->NewStringUTF(notification.tag.c_str()) + ); + + this->core->dispatchEventLoop([=, this] () { + const auto json = JSON::Object::Entries { + {"id", notification.identifier}, + {"action", "dismiss"} + }; + + this->notificationResponseObservers.dispatch(json); + }); + return success; + #endif + + return false; + } + + void CoreNotifications::list (const ListCallback& callback) const { + #if SOCKET_RUNTIME_PLATFORM_APPLE + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + [notificationCenter getDeliveredNotificationsWithCompletionHandler: ^(NSArray<UNNotification*> *notifications) { + Vector<Notification> entries; + + for (UNNotification* notification in notifications) { + entries.push_back(Notification { notification.request.identifier.UTF8String }); + } + + callback(entries); + }]; + #else + Vector<Notification> entries; + callback(entries); + #endif + } +} diff --git a/src/core/modules/notifications.hh b/src/core/modules/notifications.hh new file mode 100644 index 0000000000..b2c8b2d77a --- /dev/null +++ b/src/core/modules/notifications.hh @@ -0,0 +1,101 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_NOTIFICATIONS_H +#define SOCKET_RUNTIME_CORE_MODULE_NOTIFICATIONS_H + +#include "../module.hh" + +namespace SSC { + class CoreNotifications; +} + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@interface SSCUserNotificationCenterDelegate : NSObject<UNUserNotificationCenterDelegate> +@property (nonatomic) SSC::CoreNotifications* notifications; +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + didReceiveNotificationResponse: (UNNotificationResponse*) response + withCompletionHandler: (void (^)(void)) completionHandler; + +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + willPresentNotification: (UNNotification*) notification + withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler; +@end +#endif + +namespace SSC { + class CoreNotifications : public CoreModule { + public: + using PermissionChangeObserver = CoreModule::Observer<JSON::Object>; + using PermissionChangeObservers = CoreModule::Observers<PermissionChangeObserver>; + using NotificationResponseObserver = CoreModule::Observer<JSON::Object>; + using NotificationResponseObservers = CoreModule::Observers<NotificationResponseObserver>; + using NotificationPresentedObserver = CoreModule::Observer<JSON::Object>; + using NotificationPresentedObservers = CoreModule::Observers<NotificationPresentedObserver>; + + struct Notification { + String identifier; + String tag; + const JSON::Object json () const; + }; + + struct ShowOptions { + String id; + String title; + String tag; + String lang; + bool silent = false; + String icon; + String image; + String body; + String channel; + String category; + String vibrate; + }; + + struct ShowResult { + String error = ""; + Notification notification; + }; + + using ShowCallback = Function<void(const ShowResult&)>; + using ListCallback = Function<void(const Vector<Notification>&)>; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + SSCUserNotificationCenterDelegate* userNotificationCenterDelegate = nullptr; + NSTimer* userNotificationCenterPollTimer = nullptr; + UNAuthorizationStatus __block currentUserNotificationAuthorizationStatus; + #endif + + Mutex mutex; + + PermissionChangeObservers permissionChangeObservers; + NotificationResponseObservers notificationResponseObservers; + NotificationPresentedObservers notificationPresentedObservers; + + CoreNotifications (Core* core); + ~CoreNotifications (); + void start (); + void stop (); + + bool removePermissionChangeObserver (const PermissionChangeObserver& observer); + bool addPermissionChangeObserver ( + const PermissionChangeObserver& observer, + const PermissionChangeObserver::Callback& callback + ); + + bool removeNotificationResponseObserver (const NotificationResponseObserver& observer); + bool addNotificationResponseObserver ( + const NotificationResponseObserver& observer, + const NotificationResponseObserver::Callback& callback + ); + + bool removeNotificationPresentedObserver (const NotificationPresentedObserver& observer); + bool addNotificationPresentedObserver ( + const NotificationPresentedObserver& observer, + const NotificationPresentedObserver::Callback& callback + ); + + bool show (const ShowOptions& options, const ShowCallback& callback); + bool close (const Notification& notification); + void list (const ListCallback& callback) const; + }; +} +#endif diff --git a/src/core/modules/os.cc b/src/core/modules/os.cc new file mode 100644 index 0000000000..b1c3d90f22 --- /dev/null +++ b/src/core/modules/os.cc @@ -0,0 +1,670 @@ +#include "../core.hh" +#include "../json.hh" +#include "udp.hh" +#include "os.hh" + +namespace SSC { + #define CONSTANT(c) { #c, (c) }, + static const std::map<String, int32_t> OS_CONSTANTS = { + #if defined(E2BIG) + CONSTANT(E2BIG) + #endif + #if defined(EACCES) + CONSTANT(EACCES) + #endif + #if defined(EADDRINUSE) + CONSTANT(EADDRINUSE) + #endif + #if defined(EADDRNOTAVAIL) + CONSTANT(EADDRNOTAVAIL) + #endif + #if defined(EAFNOSUPPORT) + CONSTANT(EAFNOSUPPORT) + #endif + #if defined(EAGAIN) + CONSTANT(EAGAIN) + #endif + #if defined(EALREADY) + CONSTANT(EALREADY) + #endif + #if defined(EBADF) + CONSTANT(EBADF) + #endif + #if defined(EBADMSG) + CONSTANT(EBADMSG) + #endif + #if defined(EBUSY) + CONSTANT(EBUSY) + #endif + #if defined(ECANCELED) + CONSTANT(ECANCELED) + #endif + #if defined(ECHILD) + CONSTANT(ECHILD) + #endif + #if defined(ECONNABORTED) + CONSTANT(ECONNABORTED) + #endif + #if defined(ECONNREFUSED) + CONSTANT(ECONNREFUSED) + #endif + #if defined(ECONNRESET) + CONSTANT(ECONNRESET) + #endif + #if defined(EDEADLK) + CONSTANT(EDEADLK) + #endif + #if defined(EDESTADDRREQ) + CONSTANT(EDESTADDRREQ) + #endif + #if defined(EDOM) + CONSTANT(EDOM) + #endif + #if defined(EDQUOT) + CONSTANT(EDQUOT) + #endif + #if defined(EEXIST) + CONSTANT(EEXIST) + #endif + #if defined(EFAULT) + CONSTANT(EFAULT) + #endif + #if defined(EFBIG) + CONSTANT(EFBIG) + #endif + #if defined(EHOSTUNREACH) + CONSTANT(EHOSTUNREACH) + #endif + #if defined(EIDRM) + CONSTANT(EIDRM) + #endif + #if defined(EILSEQ) + CONSTANT(EILSEQ) + #endif + #if defined(EINPROGRESS) + CONSTANT(EINPROGRESS) + #endif + #if defined(EINTR) + CONSTANT(EINTR) + #endif + #if defined(EINVAL) + CONSTANT(EINVAL) + #endif + #if defined(EIO) + CONSTANT(EIO) + #endif + #if defined(EISCONN) + CONSTANT(EISCONN) + #endif + #if defined(EISDIR) + CONSTANT(EISDIR) + #endif + #if defined(ELOOP) + CONSTANT(ELOOP) + #endif + #if defined(EMFILE) + CONSTANT(EMFILE) + #endif + #if defined(EMLINK) + CONSTANT(EMLINK) + #endif + #if defined(EMSGSIZE) + CONSTANT(EMSGSIZE) + #endif + #if defined(EMULTIHOP) + CONSTANT(EMULTIHOP) + #endif + #if defined(ENAMETOOLONG) + CONSTANT(ENAMETOOLONG) + #endif + #if defined(ENETDOWN) + CONSTANT(ENETDOWN) + #endif + #if defined(ENETRESET) + CONSTANT(ENETRESET) + #endif + #if defined(ENETUNREACH) + CONSTANT(ENETUNREACH) + #endif + #if defined(ENFILE) + CONSTANT(ENFILE) + #endif + #if defined(ENOBUFS) + CONSTANT(ENOBUFS) + #endif + #if defined(ENODATA) + CONSTANT(ENODATA) + #endif + #if defined(ENODEV) + CONSTANT(ENODEV) + #endif + #if defined(ENOENT) + CONSTANT(ENOENT) + #endif + #if defined(ENOEXEC) + CONSTANT(ENOEXEC) + #endif + #if defined(ENOLCK) + CONSTANT(ENOLCK) + #endif + #if defined(ENOLINK) + CONSTANT(ENOLINK) + #endif + #if defined(ENOMEM) + CONSTANT(ENOMEM) + #endif + #if defined(ENOMSG) + CONSTANT(ENOMSG) + #endif + #if defined(ENOPROTOOPT) + CONSTANT(ENOPROTOOPT) + #endif + #if defined(ENOSPC) + CONSTANT(ENOSPC) + #endif + #if defined(ENOSR) + CONSTANT(ENOSR) + #endif + #if defined(ENOSTR) + CONSTANT(ENOSTR) + #endif + #if defined(ENOSYS) + CONSTANT(ENOSYS) + #endif + #if defined(ENOTCONN) + CONSTANT(ENOTCONN) + #endif + #if defined(ENOTDIR) + CONSTANT(ENOTDIR) + #endif + #if defined(ENOTEMPTY) + CONSTANT(ENOTEMPTY) + #endif + #if defined(ENOTSOCK) + CONSTANT(ENOTSOCK) + #endif + #if defined(ENOTSUP) + CONSTANT(ENOTSUP) + #endif + #if defined(ENOTTY) + CONSTANT(ENOTTY) + #endif + #if defined(ENXIO) + CONSTANT(ENXIO) + #endif + #if defined(EOPNOTSUPP) + CONSTANT(EOPNOTSUPP) + #endif + #if defined(EOVERFLOW) + CONSTANT(EOVERFLOW) + #endif + #if defined(EPERM) + CONSTANT(EPERM) + #endif + #if defined(EPIPE) + CONSTANT(EPIPE) + #endif + #if defined(EPROTO) + CONSTANT(EPROTO) + #endif + #if defined(EPROTONOSUPPORT) + CONSTANT(EPROTONOSUPPORT) + #endif + #if defined(EPROTOTYPE) + CONSTANT(EPROTOTYPE) + #endif + #if defined(ERANGE) + CONSTANT(ERANGE) + #endif + #if defined(EROFS) + CONSTANT(EROFS) + #endif + #if defined(ESPIPE) + CONSTANT(ESPIPE) + #endif + #if defined(ESRCH) + CONSTANT(ESRCH) + #endif + #if defined(ESTALE) + CONSTANT(ESTALE) + #endif + #if defined(ETIME) + CONSTANT(ETIME) + #endif + #if defined(ETIMEDOUT) + CONSTANT(ETIMEDOUT) + #endif + #if defined(ETXTBSY) + CONSTANT(ETXTBSY) + #endif + #if defined(EWOULDBLOCK) + CONSTANT(EWOULDBLOCK) + #endif + #if defined(EXDEV) + CONSTANT(EXDEV) + #endif + + #if defined(SIGHUP) + CONSTANT(SIGHUP) + #endif + #if defined(SIGINT) + CONSTANT(SIGINT) + #endif + #if defined(SIGQUIT) + CONSTANT(SIGQUIT) + #endif + #if defined(SIGILL) + CONSTANT(SIGILL) + #endif + #if defined(SIGTRAP) + CONSTANT(SIGTRAP) + #endif + #if defined(SIGABRT) + CONSTANT(SIGABRT) + #endif + #if defined(SIGIOT) + CONSTANT(SIGIOT) + #endif + #if defined(SIGBUS) + CONSTANT(SIGBUS) + #endif + #if defined(SIGFPE) + CONSTANT(SIGFPE) + #endif + #if defined(SIGKILL) + CONSTANT(SIGKILL) + #endif + #if defined(SIGUSR1) + CONSTANT(SIGUSR1) + #endif + #if defined(SIGSEGV) + CONSTANT(SIGSEGV) + #endif + #if defined(SIGUSR2) + CONSTANT(SIGUSR2) + #endif + #if defined(SIGPIPE) + CONSTANT(SIGPIPE) + #endif + #if defined(SIGALRM) + CONSTANT(SIGALRM) + #endif + #if defined(SIGTERM) + CONSTANT(SIGTERM) + #endif + #if defined(SIGCHLD) + CONSTANT(SIGCHLD) + #endif + #if defined(SIGCONT) + CONSTANT(SIGCONT) + #endif + #if defined(SIGSTOP) + CONSTANT(SIGSTOP) + #endif + #if defined(SIGTSTP) + CONSTANT(SIGTSTP) + #endif + #if defined(SIGTTIN) + CONSTANT(SIGTTIN) + #endif + #if defined(SIGTTOU) + CONSTANT(SIGTTOU) + #endif + #if defined(SIGURG) + CONSTANT(SIGURG) + #endif + #if defined(SIGXCPU) + CONSTANT(SIGXCPU) + #endif + #if defined(SIGXFSZ) + CONSTANT(SIGXFSZ) + #endif + #if defined(SIGVTALRM) + CONSTANT(SIGVTALRM) + #endif + #if defined(SIGPROF) + CONSTANT(SIGPROF) + #endif + #if defined(SIGWINCH) + CONSTANT(SIGWINCH) + #endif + #if defined(SIGIO) + CONSTANT(SIGIO) + #endif + #if defined(SIGINFO) + CONSTANT(SIGINFO) + #endif + #if defined(SIGSYS) + CONSTANT(SIGSYS) + #endif + }; + #undef CONSTANT + + void CoreOS::cpus ( + const String& seq, + const CoreModule::Callback& callback + ) const { + this->core->dispatchEventLoop([=, this]() { + #if SOCKET_RUNTIME_PLATFORM_ANDROID + { + auto json = JSON::Object::Entries { + {"source", "os.cpus"}, + {"data", JSON::Array::Entries {}} + }; + + callback(seq, json, Post{}); + return; + } + #endif + + uv_cpu_info_t* infos = nullptr; + int count = 0; + int status = uv_cpu_info(&infos, &count); + + if (status != 0) { + auto json = JSON::Object::Entries { + {"source", "os.cpus"}, + {"err", JSON::Object::Entries { + {"message", uv_strerror(status)} + }} + }; + + callback(seq, json, Post{}); + return; + } + + JSON::Array::Entries entries(count); + for (int i = 0; i < count; ++i) { + auto info = infos[i]; + entries[i] = JSON::Object::Entries { + {"model", info.model}, + {"speed", info.speed}, + {"times", JSON::Object::Entries { + {"user", info.cpu_times.user}, + {"nice", info.cpu_times.nice}, + {"sys", info.cpu_times.sys}, + {"idle", info.cpu_times.idle}, + {"irq", info.cpu_times.irq} + }} + }; + } + + auto json = JSON::Object::Entries { + {"source", "os.cpus"}, + {"data", entries} + }; + + uv_free_cpu_info(infos, count); + callback(seq, json, Post{}); + }); + } + + void CoreOS::networkInterfaces ( + const String& seq, + const CoreModule::Callback& callback + ) const { + uv_interface_address_t *infos = nullptr; + StringStream value; + StringStream v4; + StringStream v6; + int count = 0; + int status = uv_interface_addresses(&infos, &count); + + if (status != 0) { + auto json = JSON::Object(JSON::Object::Entries { + {"source", "os.networkInterfaces"}, + {"err", JSON::Object::Entries { + {"type", "InternalError"}, + {"message", + String("Unable to get network interfaces: ") + String(uv_strerror(status)) + } + }} + }); + + return callback(seq, json, Post{}); + } + + JSON::Object::Entries ipv4; + JSON::Object::Entries ipv6; + JSON::Object::Entries data; + + for (int i = 0; i < count; ++i) { + uv_interface_address_t info = infos[i]; + struct sockaddr_in *addr = (struct sockaddr_in*) &info.address.address4; + char mac[18] = {0}; + snprintf(mac, 18, "%02x:%02x:%02x:%02x:%02x:%02x", + (unsigned char) info.phys_addr[0], + (unsigned char) info.phys_addr[1], + (unsigned char) info.phys_addr[2], + (unsigned char) info.phys_addr[3], + (unsigned char) info.phys_addr[4], + (unsigned char) info.phys_addr[5] + ); + + if (addr->sin_family == AF_INET) { + JSON::Object::Entries entries; + entries["internal"] = info.is_internal == 0 ? "false" : "true"; + entries["address"] = IP::addrToIPv4(addr); + entries["mac"] = String(mac, 17); + ipv4[String(info.name)] = entries; + } + + if (addr->sin_family == AF_INET6) { + JSON::Object::Entries entries; + entries["internal"] = info.is_internal == 0 ? "false" : "true"; + entries["address"] = IP::addrToIPv6((struct sockaddr_in6*) addr); + entries["mac"] = String(mac, 17); + ipv6[String(info.name)] = entries; + } + } + + uv_free_interface_addresses(infos, count); + + data["ipv4"] = ipv4; + data["ipv6"] = ipv6; + + auto json = JSON::Object::Entries { + {"source", "os.networkInterfaces"}, + {"data", data} + }; + + callback(seq, json, Post{}); + } + + void CoreOS::rusage ( + const String& seq, + const CoreModule::Callback& callback + ) const { + uv_rusage_t usage; + auto status = uv_getrusage(&usage); + + if (status != 0) { + auto json = JSON::Object::Entries { + {"source", "os.rusage"}, + {"err", JSON::Object::Entries { + {"message", uv_strerror(status)} + }} + }; + + callback(seq, json, Post{}); + return; + } + + auto json = JSON::Object::Entries { + {"source", "os.rusage"}, + {"data", JSON::Object::Entries { + {"ru_maxrss", usage.ru_maxrss} + }} + }; + + callback(seq, json, Post{}); + } + + void CoreOS::uname ( + const String& seq, + const CoreModule::Callback& callback + ) const { + uv_utsname_t uname; + auto status = uv_os_uname(&uname); + + if (status != 0) { + auto json = JSON::Object::Entries { + {"source", "os.uname"}, + {"err", JSON::Object::Entries { + {"message", uv_strerror(status)} + }} + }; + + callback(seq, json, Post{}); + return; + } + + auto json = JSON::Object::Entries { + {"source", "os.uname"}, + {"data", JSON::Object::Entries { + {"sysname", uname.sysname}, + {"release", uname.release}, + {"version", uname.version}, + {"machine", uname.machine} + }} + }; + + callback(seq, json, Post{}); + } + + void CoreOS::uptime ( + const String& seq, + const CoreModule::Callback& callback + ) const { + double uptime; + auto status = uv_uptime(&uptime); + + if (status != 0) { + auto json = JSON::Object::Entries { + {"source", "os.uptime"}, + {"err", JSON::Object::Entries { + {"message", uv_strerror(status)} + }} + }; + + callback(seq, json, Post{}); + return; + } + + auto json = JSON::Object::Entries { + {"source", "os.uptime"}, + {"data", uptime * 1000} // in milliseconds + }; + + callback(seq, json, Post{}); + } + + void CoreOS::hrtime ( + const String& seq, + const CoreModule::Callback& callback + ) const { + auto hrtime = uv_hrtime(); + auto bytes = toBytes(hrtime); + auto size = bytes.size(); + auto post = Post {}; + auto body = new char[size]{0}; + auto json = JSON::Object {}; + post.body.reset(body); + post.length = size; + memcpy(body, bytes.data(), size); + callback(seq, json, post); + } + + void CoreOS::availableMemory ( + const String& seq, + const CoreModule::Callback& callback + ) const { + auto memory = uv_get_available_memory(); + auto bytes = toBytes(memory); + auto size = bytes.size(); + auto post = Post {}; + auto body = new char[size]{0}; + auto json = JSON::Object {}; + post.body.reset(body); + post.length = size; + memcpy(body, bytes.data(), size); + callback(seq, json, post); + } + + void CoreOS::bufferSize ( + const String& seq, + CoreUDP::ID id, + size_t size, + int buffer, + const CoreModule::Callback& callback + ) const { + if (buffer == 0) { + buffer = CoreOS::SEND_BUFFER; + } else if (buffer == 1) { + buffer = CoreOS::RECV_BUFFER; + } + + this->core->dispatchEventLoop([=, this]() { + auto socket = this->core->udp.getSocket(id); + + if (socket == nullptr) { + auto json = JSON::Object::Entries { + {"source", "bufferSize"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"code", "NOT_FOUND_ERR"}, + {"type", "NotFoundError"}, + {"message", "No socket with specified id"} + }} + }; + + callback(seq, json, Post{}); + return; + } + + Lock lock(socket->mutex); + auto handle = (uv_handle_t*) &socket->handle; + auto err = buffer == RECV_BUFFER + ? uv_recv_buffer_size(handle, (int *) &size) + : uv_send_buffer_size(handle, (int *) &size); + + if (err < 0) { + auto json = JSON::Object::Entries { + {"source", "bufferSize"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"code", "NOT_FOUND_ERR"}, + {"type", "NotFoundError"}, + {"message", String(uv_strerror(err))} + }} + }; + + callback(seq, json, Post{}); + return; + } + + auto json = JSON::Object::Entries { + {"source", "bufferSize"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"size", (int) size} + }} + }; + + callback(seq, json, Post{}); + }); + } + + void CoreOS::constants ( + const String& seq, + const CoreModule::Callback& callback + ) const { + static const auto data = JSON::Object(OS_CONSTANTS); + static const auto json = JSON::Object::Entries { + {"source", "os.constants"}, + {"data", data} + }; + + callback(seq, json, Post {}); + } +} diff --git a/src/core/modules/os.hh b/src/core/modules/os.hh new file mode 100644 index 0000000000..af8266dfdd --- /dev/null +++ b/src/core/modules/os.hh @@ -0,0 +1,66 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_OS_H +#define SOCKET_RUNTIME_CORE_MODULE_OS_H + +#include "../module.hh" + +namespace SSC { + class Core; + class CoreOS : public CoreModule { + public: + static const int RECV_BUFFER = 1; + static const int SEND_BUFFER = 0; + + CoreOS (Core* core) + : CoreModule(core) + {} + + void constants ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void bufferSize ( + const String& seq, + uint64_t peerId, + size_t size, + int buffer, // RECV_BUFFER, SEND_BUFFER + const CoreModule::Callback& callback + ) const; + + void cpus ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void networkInterfaces ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void rusage ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void uname ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void uptime ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void hrtime ( + const String& seq, + const CoreModule::Callback& callback + ) const; + + void availableMemory ( + const String& seq, + const CoreModule::Callback& callback + ) const; + }; +} +#endif diff --git a/src/core/modules/permissions.cc b/src/core/modules/permissions.cc new file mode 100644 index 0000000000..50e26c77b3 --- /dev/null +++ b/src/core/modules/permissions.cc @@ -0,0 +1,638 @@ +#include "../core.hh" +#include "permissions.hh" + +namespace SSC { + bool CorePermissions::hasRuntimePermission (const String& permission) const { + static const auto userConfig = getUserConfig(); + const auto key = String("permissions_allow_") + replace(permission, "-", "_"); + + if (!userConfig.contains(key)) { + return true; + } + + return userConfig.at(key) != "false"; + } + + void CorePermissions::query ( + const String& seq, + const String& name, + const Callback& callback + ) const { + this->core->dispatchEventLoop([=, this]() { + if (!this->hasRuntimePermission(name)) { + JSON::Object json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "denied"}, + {"reason", "Runtime permission is disabled for '" + name + "'"} + }} + }; + callback(seq, json, Post{}); + return; + } + + if (name == "geolocation") { + JSON::Object json; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->core->geolocation.locationObserver.isAuthorized) { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + } else if (this->core->geolocation.locationObserver.locationManager) { + const auto authorizationStatus = ( + this->core->geolocation.locationObserver.locationManager.authorizationStatus + ); + + if (authorizationStatus == kCLAuthorizationStatusDenied) { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "denied"}} + } + }; + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + } + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasCoarseLocation = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.ACCESS_COARSE_LOCATION") + ); + + // `activity.checkPermission(permission)` + const auto hasFineLocation = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.ACCESS_FINE_LOCATION") + ); + + if (!hasCoarseLocation || !hasFineLocation) { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + } + #endif + + callback(seq, json, Post{}); + } + + if (name == "notifications") { + JSON::Object json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "denied"} + }} + }; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + const auto notificationCenter = UNUserNotificationCenter.currentNotificationCenter; + [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings* settings) { + JSON::Object json; + + if (!settings) { + json["err"] = JSON::Object::Entries {{ "message", "Failed to reach user notification settings" }}; + } else if (settings.authorizationStatus == UNAuthorizationStatusDenied) { + json["data"] = JSON::Object::Entries {{"state", "denied"}}; + } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { + json["data"] = JSON::Object::Entries {{"state", "prompt"}}; + } else { + json["data"] = JSON::Object::Entries {{"state", "granted"}}; + } + + callback(seq, json, Post{}); + }]; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasPostNotifications = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.POST_NOTIFICATIONS") + ); + + // `activity.isNotificationManagerEnabled()` + const auto isNotificationManagerEnabled = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "isNotificationManagerEnabled", + "()Z" + ); + + if (!hasPostNotifications || !isNotificationManagerEnabled) { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + } + + callback(seq, json, Post{}); + #else + callback(seq, json, Post{}); + #endif + } + + if (name == "camera") { + JSON::Object json; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasCameraPermission = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.CAMERA") + ); + + if (!hasCameraPermission) { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + } + + callback(seq, json, Post{}); + #else + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + callback(seq, json, Post{}); + #endif + } + + if (name == "microphone") { + JSON::Object json; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasRecordAudioPermission = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.RECORD_AUDIO") + ); + + if (!hasRecordAudioPermission) { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + } + + callback(seq, json, Post{}); + #else + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + callback(seq, json, Post{}); + #endif + } + }); + } + + void CorePermissions::request ( + const String& seq, + const String& name, + const Map& options, + const Callback& callback + ) const { + this->core->dispatchEventLoop([=, this]() { + if (!this->hasRuntimePermission(name)) { + JSON::Object json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "denied"}} + } + }; + callback(seq, json, Post{}); + return; + } + + if (name == "geolocation") { + JSON::Object json; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + const auto performedActivation = [this->core->geolocation.locationObserver attemptActivationWithCompletion: ^(BOOL isAuthorized) { + if (!isAuthorized) { + auto reason = @("Location observer could not be activated"); + + if (!this->core->geolocation.locationObserver.locationManager) { + reason = @("Location observer manager is not initialized"); + } else if (!this->core->geolocation.locationObserver.locationManager.location) { + reason = @("Location observer manager could not provide location"); + } + + debug("%s", reason.UTF8String); + } + + if (isAuthorized) { + json["data"] = JSON::Object::Entries {{"state", "granted"}}; + } else if (this->core->geolocation.locationObserver.locationManager.authorizationStatus == kCLAuthorizationStatusNotDetermined) { + json["data"] = JSON::Object::Entries {{"state", "prompt"}}; + } else { + json["data"] = JSON::Object::Entries {{"state", "denied"}}; + } + + callback(seq, json, Post{}); + }]; + + if (!performedActivation) { + auto err = JSON::Object::Entries {{ "message", "Location observer could not be activated" }}; + err["type"] = "GeolocationPositionError"; + json["err"] = err; + return callback(seq, json, Post{}); + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasCoarseLocation = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.ACCESS_COARSE_LOCATION") + ); + + // `activity.checkPermission(permission)` + const auto hasFineLocation = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.ACCESS_FINE_LOCATION") + ); + + if (!hasCoarseLocation || !hasFineLocation) { + CoreGeolocation::PermissionChangeObserver observer; + auto permissions = attachment.env->NewObjectArray( + 2, + attachment.env->FindClass("java/lang/String"), + 0 + ); + + attachment.env->SetObjectArrayElement( + permissions, + 0, + attachment.env->NewStringUTF("android.permission.ACCESS_COARSE_LOCATION") + ); + + attachment.env->SetObjectArrayElement( + permissions, + 1, + attachment.env->NewStringUTF("android.permission.ACCESS_FINE_LOCATION") + ); + + this->core->geolocation.addPermissionChangeObserver(observer, [seq, observer, callback, this](auto result) mutable { + JSON::Object json = JSON::Object::Entries { + {"data", result} + }; + callback(seq, json, Post{}); + this->core->dispatchEventLoop([=] () { + this->core->geolocation.removePermissionChangeObserver(observer); + }); + }); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->core->platform.activity, + "requestPermissions", + "([Ljava/lang/String;)V", + permissions + ); + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + callback(seq, json, Post{}); + } + #endif + } + + if (name == "notifications") { + JSON::Object json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "denied"} + }} + }; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + UNAuthorizationOptions requestOptions = UNAuthorizationOptionProvisional; + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + auto requestAlert = options.contains("alert") && options.at("alert") == "true"; + auto requestBadge = options.contains("badge") && options.at("badge") == "true"; + auto requestSound = options.contains("sound") && options.at("sound") == "true"; + + if (requestAlert) { + requestOptions |= UNAuthorizationOptionAlert; + } + + if (requestBadge) { + requestOptions |= UNAuthorizationOptionBadge; + } + + if (requestSound) { + requestOptions |= UNAuthorizationOptionSound; + } + + if (requestAlert && requestSound) { + requestOptions |= UNAuthorizationOptionCriticalAlert; + } + + [notificationCenter + requestAuthorizationWithOptions: requestOptions + completionHandler: ^(BOOL granted, NSError *error) + { + [notificationCenter + getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) + { + JSON::Object json; + + if (settings.authorizationStatus == UNAuthorizationStatusDenied) { + json["data"] = JSON::Object::Entries {{"state", "denied"}}; + } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { + if (error) { + const auto message = String( + error.localizedDescription.UTF8String != nullptr + ? error.localizedDescription.UTF8String + : "An unknown error occurred" + ); + + json["err"] = JSON::Object::Entries {{"message", message}}; + } else { + json["data"] = JSON::Object::Entries { + {"state", granted ? "granted" : "denied" } + }; + } + } else { + json["data"] = JSON::Object::Entries {{"state", "granted"}}; + } + + callback(seq, json, Post{}); + }]; + }]; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasPostNotifications = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.POST_NOTIFICATIONS") + ); + + // `activity.isNotificationManagerEnabled()` + const auto isNotificationManagerEnabled = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "isNotificationManagerEnabled", + "()Z" + ); + + if (!hasPostNotifications || !isNotificationManagerEnabled) { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "prompt"}} + } + }; + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + } + + if (!hasPostNotifications || !isNotificationManagerEnabled) { + CoreNotifications::PermissionChangeObserver observer; + auto permissions = attachment.env->NewObjectArray( + 1, + attachment.env->FindClass("java/lang/String"), + 0 + ); + + attachment.env->SetObjectArrayElement( + permissions, + 0, + attachment.env->NewStringUTF("android.permission.POST_NOTIFICATIONS") + ); + + this->core->notifications.addPermissionChangeObserver(observer, [=](auto result) mutable { + JSON::Object json = JSON::Object::Entries { + {"data", result} + }; + callback(seq, json, Post{}); + this->core->dispatchEventLoop([=]() { + this->core->notifications.removePermissionChangeObserver(observer); + }); + }); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->core->platform.activity, + "requestPermissions", + "([Ljava/lang/String;)V", + permissions + ); + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + callback(seq, json, Post{}); + } + #else + callback(seq, json, Post{}); + #endif + } + + if (name == "camera") { + JSON::Object json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "denied"} + }} + }; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasCameraPermission = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.CAMERA") + ); + + if (!hasCameraPermission) { + CoreMediaDevices::PermissionChangeObserver observer; + auto permissions = attachment.env->NewObjectArray( + 1, + attachment.env->FindClass("java/lang/String"), + 0 + ); + + attachment.env->SetObjectArrayElement( + permissions, + 0, + attachment.env->NewStringUTF("android.permission.CAMERA") + ); + + this->core->mediaDevices.addPermissionChangeObserver(observer, [=](JSON::Object result) mutable { + if (result.get("name").str() == "camera") { + JSON::Object json = JSON::Object::Entries { + {"data", result} + }; + callback(seq, json, Post{}); + this->core->dispatchEventLoop([=]() { + this->core->mediaDevices.removePermissionChangeObserver(observer); + }); + } + }); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->core->platform.activity, + "requestPermissions", + "([Ljava/lang/String;)V", + permissions + ); + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + callback(seq, json, Post{}); + } + #else + callback(seq, json, Post{}); + #endif + } + + if (name == "microphone") { + JSON::Object json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "denied"} + }} + }; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(this->core->platform.jvm); + // `activity.checkPermission(permission)` + const auto hasRecordAudioPermission = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->core->platform.activity, + "checkPermission", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF("android.permission.RECORD_AUDIO") + ); + + if (!hasRecordAudioPermission) { + CoreMediaDevices::PermissionChangeObserver observer; + auto permissions = attachment.env->NewObjectArray( + 1, + attachment.env->FindClass("java/lang/String"), + 0 + ); + + attachment.env->SetObjectArrayElement( + permissions, + 0, + attachment.env->NewStringUTF("android.permission.RECORD_AUDIO") + ); + + this->core->mediaDevices.addPermissionChangeObserver(observer, [=](JSON::Object result) mutable { + if (result.get("name").str() == "microphone") { + JSON::Object json = JSON::Object::Entries { + {"data", result} + }; + callback(seq, json, Post{}); + this->core->dispatchEventLoop([=]() { + this->core->mediaDevices.removePermissionChangeObserver(observer); + }); + } + }); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->core->platform.activity, + "requestPermissions", + "([Ljava/lang/String;)V", + permissions + ); + } else { + json = JSON::Object::Entries { + {"data", JSON::Object::Entries { + {"state", "granted"}} + } + }; + callback(seq, json, Post{}); + } + #else + callback(seq, json, Post{}); + #endif + } + }); + } +} diff --git a/src/core/modules/permissions.hh b/src/core/modules/permissions.hh new file mode 100644 index 0000000000..b43616f880 --- /dev/null +++ b/src/core/modules/permissions.hh @@ -0,0 +1,29 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_PERMISSIONS_H +#define SOCKET_RUNTIME_CORE_MODULE_PERMISSIONS_H + +#include "../module.hh" + +namespace SSC { + class CorePermissions : public CoreModule { + public: + CorePermissions (Core* core) + : CoreModule(core) + {} + + bool hasRuntimePermission (const String& permission) const; + + void query ( + const String& seq, + const String& name, + const Callback& callback + ) const; + + void request ( + const String& seq, + const String& name, + const Map& options, + const Callback& callback + ) const; + }; +} +#endif diff --git a/src/core/modules/platform.cc b/src/core/modules/platform.cc new file mode 100644 index 0000000000..2e36e54d02 --- /dev/null +++ b/src/core/modules/platform.cc @@ -0,0 +1,233 @@ +#include "../process.hh" +#include "../codec.hh" +#include "../core.hh" +#include "../json.hh" +#include "platform.hh" + +namespace SSC { +#if SOCKET_RUNTIME_PLATFORM_ANDROID + void CorePlatform::configureAndroidContext ( + Android::JVMEnvironment jvm, + Android::Activity activity + ) { + this->jvm = jvm; + this->activity = activity; + this->contentResolver.activity = activity; + this->contentResolver.jvm = jvm; + } + +#endif + void CorePlatform::event ( + const String& seq, + const String& event, + const String& data, + const String& frameType, + const String& frameSource, + const CoreModule::Callback& callback + ) { + if (event == "domcontentloaded") { + this->wasFirstDOMContentLoadedEventDispatched = true; + } + + const auto json = JSON::Object::Entries { + {"source", "platform.event"}, + {"data", JSON::Object {}} + }; + + callback(seq, json, Post{}); + } + + void CorePlatform::revealFile ( + const String& seq, + const String& value, + const CoreModule::Callback& callback + ) const { + String errorMessage = "Failed to open external file"; + String pathToFile = decodeURIComponent(value); + bool success = false; + auto json = JSON::Object(JSON::Object::Entries { + {"source", "platform.revealFile"} + }); + + #if SOCKET_RUNTIME_PLATFORM_MACOS + success = [NSWorkspace.sharedWorkspace + selectFile: nil + inFileViewerRootedAtPath: @(pathToFile.c_str()) + ]; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + const auto result = exec("xdg-open " + pathToFile); + success = result.exitCode == 0; + errorMessage = result.output; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + const auto result = exec("explorer.exe \"" + pathToFile + "\""); + success = result.exitCode == 0; + errorMessage = result.output; + #endif + + if (!success) { + json["err"] = JSON::Object::Entries { + {"message", errorMessage} + }; + } else { + json["data"] = JSON::Object {}; + } + + callback(seq, json, Post{}); + } + + void CorePlatform::openExternal ( + const String& seq, + const String& value, + const CoreModule::Callback& callback + ) const { + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->core->dispatchEventLoop([=]() { + __block const auto url = [NSURL URLWithString: @(value.c_str())]; + + #if SOCKET_RUNTIME_PLATFORM_IOS + [UIApplication.sharedApplication openURL: url options: @{} completionHandler: ^(BOOL success) { + JSON::Object json; + if (!success) { + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"err", JSON::Object::Entries { + {"message", "Failed to open external URL"} + }} + }; + } else { + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"data", JSON::Object::Entries {{"url", url.absoluteString.UTF8String}}} + }; + } + + callback(seq, json, Post{}); + }]; + #else + auto workspace = [NSWorkspace sharedWorkspace]; + auto configuration = [NSWorkspaceOpenConfiguration configuration]; + [workspace openURL: url + configuration: configuration + completionHandler: ^(NSRunningApplication *app, NSError *error) + { + JSON::Object json; + if (error) { + if (error.debugDescription.UTF8String) { + debug("%s", error.debugDescription.UTF8String); + } + + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"err", JSON::Object::Entries { + {"message", error.localizedDescription.UTF8String} + }} + }; + } else { + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"data", JSON::Object::Entries {{ "url", url.absoluteString.UTF8String}}} + }; + } + + callback(seq, json, Post{}); + }]; + #endif + }); + #elif SOCKET_RUNTIME_PLATFORM_LINUX + auto list = gtk_window_list_toplevels(); + auto json = JSON::Object {}; + + // initial state is a failure + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"err", JSON::Object::Entries { + {"message", "Failed to open external URL"} + }} + }; + + if (list != nullptr) { + for (auto entry = list; entry != nullptr; entry = entry->next) { + auto window = GTK_WINDOW(entry->data); + + if (window != nullptr && gtk_window_is_active(window)) { + auto err = (GError*) nullptr; + auto uri = value.c_str(); + auto ts = GDK_CURRENT_TIME; + + /** + * GTK may print a error in the terminal that looks like: + * + * libva error: vaGetDriverNameByIndex() failed with unknown libva error, driver_name = (null) + * + * It doesn't prevent the URI from being opened. + * See https://github.com/intel/media-driver/issues/1349 for more info + */ + auto success = gtk_show_uri_on_window(window, uri, ts, &err); + + if (success) { + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"data", JSON::Object::Entries {}} + }; + } else if (err != nullptr) { + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"err", JSON::Object::Entries { + {"message", err->message} + }} + }; + } + + break; + } + } + + g_list_free(list); + } + + callback(seq, json, Post{}); + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + auto uri = value.c_str(); + ShellExecute(nullptr, "Open", uri, nullptr, nullptr, SW_SHOWNORMAL); + // TODO how to detect success here. do we care? + callback(seq, JSON::Object{}, Post{}); + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + JSON::Object json; + const auto attachment = Android::JNIEnvironmentAttachment(this->jvm); + // `activity.openExternal(url)` + CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + this->activity, + "openExternal", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF(value.c_str()) + ); + + if (attachment.hasException()) { + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"err", JSON::Object::Entries { + {"message", "Failed to open external URL"} + }} + }; + } else { + json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"data", JSON::Object::Entries {{ "url", value}}} + }; + } + + callback(seq, json, Post{}); + #else + const auto json = JSON::Object::Entries { + {"source", "platform.openExternal"}, + {"err", JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Operation not supported"} + }} + }; + callback(seq, json, Post{}); + #endif + } +} diff --git a/src/core/modules/platform.hh b/src/core/modules/platform.hh new file mode 100644 index 0000000000..6fc80bd158 --- /dev/null +++ b/src/core/modules/platform.hh @@ -0,0 +1,51 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_PLATFORM_H +#define SOCKET_RUNTIME_CORE_MODULE_PLATFORM_H + +#include "../module.hh" + +namespace SSC { + class Core; + class CorePlatform : public CoreModule { + public: + Atomic<bool> wasFirstDOMContentLoadedEventDispatched = false; + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + Android::JVMEnvironment jvm; + Android::Activity activity = nullptr; + Android::ContentResolver contentResolver; + #endif + + CorePlatform (Core* core) + : CoreModule(core) + {} + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + void configureAndroidContext ( + Android::JVMEnvironment jvm, + Android::Activity activity + ); + #endif + + void event ( + const String& seq, + const String& event, + const String& data, + const String& frameType, + const String& frameSource, + const CoreModule::Callback& callback + ); + + void openExternal ( + const String& seq, + const String& value, + const CoreModule::Callback& callback + ) const; + + void revealFile ( + const String& seq, + const String& value, + const CoreModule::Callback& callback + ) const; + }; +} +#endif diff --git a/src/core/modules/timers.cc b/src/core/modules/timers.cc new file mode 100644 index 0000000000..66e861d3c2 --- /dev/null +++ b/src/core/modules/timers.cc @@ -0,0 +1,157 @@ +#include "../core.hh" +#include "timers.hh" + +namespace SSC { + CoreTimers::Timer::Timer (CoreTimers* timers, ID id, Callback callback) + : timers(timers), + id(id), + callback(callback) + {} + + const CoreTimers::ID CoreTimers::createTimer ( + uint64_t timeout, + uint64_t interval, + const Callback& callback + ) { + Lock lock(this->mutex); + + auto id = rand64(); + auto loop = this->core->getEventLoop(); + auto handle = std::make_shared<Timer>( + this, + id, + callback + ); + + if (interval > 0) { + handle->repeat = true; + } + + this->handles.emplace(handle->id, handle); + + this->core->dispatchEventLoop([=, this]() { + Lock lock(this->mutex); + if (this->handles.contains(id)) { + auto handle = this->handles.at(id); + uv_handle_set_data((uv_handle_t*) &handle->timer, (void*) id); + uv_timer_init(loop, &handle->timer); + uv_timer_start( + &handle->timer, + [](uv_timer_t* timer) { + auto loop = uv_handle_get_loop(reinterpret_cast<uv_handle_t*>(timer)); + auto core = reinterpret_cast<Core*>(uv_loop_get_data(loop)); + auto id = reinterpret_cast<ID>(uv_handle_get_data(reinterpret_cast<uv_handle_t*>(timer))); + + // bad state + if (core == nullptr) { + uv_timer_stop(timer); + return; + } + + Lock lock(core->timers.mutex); + + // cancelled (removed from 'handles') + if (!core->timers.handles.contains(id)) { + uv_timer_stop(timer); + return; + } + + auto handle = core->timers.handles.at(id); + + // bad ref + if (handle == nullptr) { + uv_timer_stop(timer); + return; + } + + // `callback` to timer callback is a "cancel" function + handle->callback([=] () { + core->timers.cancelTimer(id); + }); + + if (!handle->repeat) { + if (core->timers.handles.contains(id)) { + core->timers.handles.erase(id); + } + } + }, + timeout, + interval + ); + } + }); + + return id; + } + + bool CoreTimers::cancelTimer (const ID id) { + Lock lock(this->mutex); + + if (!this->handles.contains(id)) { + return false; + } + + auto handle = this->handles.at(id); + handle->cancelled = true; + uv_timer_stop(&handle->timer); + this->handles.erase(id); + return true; + } + + const CoreTimers::ID CoreTimers::setTimeout ( + uint64_t timeout, + const TimeoutCallback& callback + ) { + Lock lock(this->mutex); + const auto id = this->createTimer(timeout, 0, [callback] (auto _) { + callback(); + }); + + if (this->handles.contains(id)) { + this->handles.at(id)->type = Timer::Type::Timeout; + } + + return id; + } + + bool CoreTimers::clearTimeout (const ID id) { + return this->cancelTimer(id); + } + + const CoreTimers::ID CoreTimers::setInterval ( + uint64_t interval, + const IntervalCallback& callback + ) { + Lock lock(this->mutex); + + const auto id = this->createTimer(interval, interval, callback); + + if (this->handles.contains(id)) { + this->handles.at(id)->type = Timer::Type::Interval; + } + + return id; + } + + bool CoreTimers::clearInterval (const ID id) { + return this->cancelTimer(id); + } + + const CoreTimers::ID CoreTimers::setImmediate (const ImmediateCallback& callback) { + Lock lock(this->mutex); + + const auto id = this->createTimer(0, 0, [callback] (auto _) { + callback(); + }); + + if (this->handles.contains(id)) { + this->handles.at(id)->type = Timer::Type::Immediate; + } + + return id; + } + + bool CoreTimers::clearImmediate (const ID id) { + return this->clearTimeout(id); + } +} diff --git a/src/core/modules/timers.hh b/src/core/modules/timers.hh new file mode 100644 index 0000000000..ce4f4bf0c2 --- /dev/null +++ b/src/core/modules/timers.hh @@ -0,0 +1,55 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_TIMERS_H +#define SOCKET_RUNTIME_CORE_MODULE_TIMERS_H + +#include "../module.hh" + +namespace SSC { + class Core; + class CoreTimers : public CoreModule { + public: + using ID = uint64_t; + using CancelCallback = Function<void()>; + using Callback = Function<void(CancelCallback)>; + using TimeoutCallback = Function<void()>; + using IntervalCallback = Callback; + using ImmediateCallback = TimeoutCallback; + + struct Timer { + enum class Type { Timeout, Interval, Immediate }; + + CoreTimers* timers = nullptr; + ID id = 0; + Callback callback = nullptr; + bool repeat = false; + bool cancelled = false; + uv_timer_t timer; + Type type; + Timer (CoreTimers* timers, ID id, Callback callback); + }; + + using Handles = std::map<ID, SharedPointer<Timer>>; + + Handles handles; + Mutex mutex; + + CoreTimers (Core* core) + : CoreModule (core) + {} + + const ID createTimer ( + uint64_t timeout, + uint64_t interval, + const Callback& callback + ); + + const ID setTimeout (uint64_t timeout, const TimeoutCallback& callback); + const ID setInterval (uint64_t interval, const IntervalCallback& callback); + const ID setImmediate (const ImmediateCallback& callback); + + bool cancelTimer (const ID id); + bool clearTimeout (const ID id); + bool clearInterval (const ID id); + bool clearImmediate (const ID id); + }; +} +#endif diff --git a/src/core/modules/udp.cc b/src/core/modules/udp.cc new file mode 100644 index 0000000000..01e6b60a2b --- /dev/null +++ b/src/core/modules/udp.cc @@ -0,0 +1,678 @@ +#include "../headers.hh" +#include "../core.hh" +#include "../ip.hh" +#include "udp.hh" + +namespace SSC { + static JSON::Object::Entries ERR_SOCKET_ALREADY_BOUND ( + const String& source, + CoreUDP::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"code", "ERR_SOCKET_ALREADY_BOUND"}, + {"message", "Socket is already bound"} + }} + }; + } + + static JSON::Object::Entries ERR_SOCKET_DGRAM_IS_CONNECTED ( + const String& source, + CoreUDP::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"code", "ERR_SOCKET_DGRAM_IS_CONNECTED"}, + {"message", "Already connected"} + }} + }; + } + + static JSON::Object::Entries ERR_SOCKET_DGRAM_NOT_CONNECTED ( + const String& source, + CoreUDP::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"code", "ERR_SOCKET_DGRAM_NOT_CONNECTED"}, + {"message", "Not connected"} + }} + }; + } + + static JSON::Object::Entries ERR_SOCKET_DGRAM_CLOSED ( + const String& source, + CoreUDP::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "InternalError"}, + {"code", "ERR_SOCKET_DGRAM_CLOSED"}, + {"message", "Socket is closed"} + }} + }; + } + + static JSON::Object::Entries ERR_SOCKET_DGRAM_CLOSING ( + const String& source, + CoreUDP::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotFoundError"}, + {"code", "ERR_SOCKET_DGRAM_CLOSING"}, + {"message", "Socket is closing"} + }} + }; + } + + static JSON::Object::Entries ERR_SOCKET_DGRAM_NOT_RUNNING ( + const String& source, + CoreUDP::ID id + ) { + return JSON::Object::Entries { + {"source", source}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotFoundError"}, + {"code", "ERR_SOCKET_DGRAM_NOT_RUNNING"}, + {"message", "Not running"} + }} + }; + } + + void CoreUDP::resumeAllSockets () { + this->core->dispatchEventLoop([=, this]() { + for (auto const &tuple : this->sockets) { + auto socket = tuple.second; + if (socket != nullptr && (socket->isBound() || socket->isConnected())) { + socket->resume(); + } + } + }); + } + + void CoreUDP::pauseAllSockets () { + for (auto const &tuple : this->sockets) { + auto socket = tuple.second; + if (socket != nullptr && (socket->isBound() || socket->isConnected())) { + socket->pause(); + } + } + } + + bool CoreUDP::hasSocket (ID id) { + Lock lock(this->mutex); + return this->sockets.find(id) != this->sockets.end(); + } + + void CoreUDP::removeSocket (ID id) { + return this->removeSocket(id, false); + } + + void CoreUDP::removeSocket (ID id, bool autoClose) { + if (this->hasSocket(id)) { + if (autoClose) { + auto socket = this->getSocket(id); + if (socket != nullptr) { + socket->close(); + } + } + + Lock lock(this->mutex); + this->sockets.erase(id); + } + } + + SharedPointer<Socket> CoreUDP::getSocket (ID id) { + if (!this->hasSocket(id)) return nullptr; + Lock lock(this->mutex); + return this->sockets.at(id); + } + + SharedPointer<Socket> CoreUDP::createSocket (socket_type_t socketType, ID id) { + return this->createSocket(socketType, id, false); + } + + SharedPointer<Socket> CoreUDP::createSocket ( + socket_type_t socketType, + ID id, + bool isEphemeral + ) { + if (this->hasSocket(id)) { + auto socket = this->getSocket(id); + if (socket != nullptr) { + if (isEphemeral) { + Lock lock(socket->mutex); + socket->flags = (socket_flag_t) (socket->flags | SOCKET_FLAG_EPHEMERAL); + } + } + + return socket; + } + + Lock lock(this->mutex); + this->sockets[id].reset(new Socket(this->core, socketType, id, isEphemeral)); + return this->sockets.at(id); + } + + void CoreUDP::bind ( + const String& seq, + ID id, + const CoreUDP::BindOptions& options, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this]() { + if (this->hasSocket(id)) { + if (this->getSocket(id)->isBound()) { + auto json = ERR_SOCKET_ALREADY_BOUND("udp.bind", id); + return callback(seq, json, Post{}); + } + } + + auto socket = this->createSocket(SOCKET_TYPE_UDP, id); + auto err = socket->bind(options.address, options.port, options.reuseAddr); + + if (err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.bind"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto info = socket->getLocalPeerInfo(); + + if (info->err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.bind"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(info->err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.bind"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"port", (int) info->port}, + {"event" , "listening"}, + {"family", info->family}, + {"address", info->address} + }} + }; + + callback(seq, json, Post{}); + }); + } + + void CoreUDP::connect ( + const String& seq, + ID id, + const CoreUDP::ConnectOptions& options, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this]() { + auto socket = this->createSocket(SOCKET_TYPE_UDP, id); + + if (socket->isConnected()) { + auto json = ERR_SOCKET_DGRAM_IS_CONNECTED("udp.connect", id); + return callback(seq, json, Post{}); + } + + auto err = socket->connect(options.address, options.port); + + if (err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.connect"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto info = socket->getRemotePeerInfo(); + + if (info->err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.connect"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(info->err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.connect"}, + {"data", JSON::Object::Entries { + {"address", info->address}, + {"family", info->family}, + {"port", (int) info->port}, + {"id", std::to_string(id)} + }} + }; + + callback(seq, json, Post{}); + }); + } + + void CoreUDP::disconnect ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this]() { + if (!this->hasSocket(id)) { + auto json = ERR_SOCKET_DGRAM_NOT_CONNECTED("udp.disconnect", id); + return callback(seq, json, Post{}); + } + + auto socket = this->getSocket(id); + auto err = socket->disconnect(); + + if (err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.disconnect"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.disconnect"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)} + }} + }; + + callback(seq, json, Post{}); + }); + } + + void CoreUDP::getPeerName ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) { + if (!this->hasSocket(id)) { + auto json = ERR_SOCKET_DGRAM_NOT_CONNECTED("udp.getPeerName", id); + return callback(seq, json, Post{}); + } + + auto socket = this->getSocket(id); + auto info = socket->getRemotePeerInfo(); + + if (info->err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.getPeerName"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(info->err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.getPeerName"}, + {"data", JSON::Object::Entries { + {"address", info->address}, + {"family", info->family}, + {"port", (int) info->port}, + {"id", std::to_string(id)} + }} + }; + + callback(seq, json, Post{}); + } + + void CoreUDP::getSockName ( + const String& seq, + ID id, + const Callback& callback + ) { + if (!this->hasSocket(id)) { + auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.getSockName", id); + return callback(seq, json, Post{}); + } + + auto socket = this->getSocket(id); + auto info = socket->getLocalPeerInfo(); + + if (info->err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.getSockName"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(info->err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.getSockName"}, + {"data", JSON::Object::Entries { + {"address", info->address}, + {"family", info->family}, + {"port", (int) info->port}, + {"id", std::to_string(id)} + }} + }; + + callback(seq, json, Post{}); + } + + void CoreUDP::getState ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) { + if (!this->hasSocket(id)) { + auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.getState", id); + return callback(seq, json, Post{}); + } + + auto socket = this->getSocket(id); + + if (!socket->isUDP()) { + auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.getState", id); + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.getState"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "udp"}, + {"bound", socket->isBound()}, + {"active", socket->isActive()}, + {"closed", socket->isClosed()}, + {"closing", socket->isClosing()}, + {"connected", socket->isConnected()}, + {"ephemeral", socket->isEphemeral()} + }} + }; + + callback(seq, json, Post{}); + } + + void CoreUDP::send ( + const String& seq, + ID id, + const CoreUDP::SendOptions& options, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this] { + auto socket = this->createSocket(SOCKET_TYPE_UDP, id, options.ephemeral); + auto size = options.size; // @TODO(jwerle): validate MTU + auto port = options.port; + auto bytes = options.bytes; + auto address = options.address; + socket->send(bytes, size, port, address, [=](auto status, auto post) { + if (status < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.send"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(status))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.send"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"status", status} + }} + }; + + callback(seq, json, Post{}); + }); + }); + } + + void CoreUDP::readStart (const String& seq, ID id, const CoreModule::Callback& callback) { + if (!this->hasSocket(id)) { + auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.readStart", id); + return callback(seq, json, Post{}); + } + + auto socket = this->getSocket(id); + + if (socket->isClosed()) { + auto json = ERR_SOCKET_DGRAM_CLOSED("udp.readStart", id); + return callback(seq, json, Post{}); + } + + if (socket->isClosing()) { + auto json = ERR_SOCKET_DGRAM_CLOSING("udp.readStart", id); + return callback(seq, json, Post{}); + } + + if (socket->hasState(SOCKET_STATE_UDP_RECV_STARTED)) { + auto json = JSON::Object::Entries { + {"source", "udp.readStart"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", "Socket is already receiving"} + }} + }; + + return callback(seq, json, Post{}); + } + + if (socket->isActive()) { + auto json = JSON::Object::Entries { + {"source", "udp.readStart"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)} + }} + }; + + return callback(seq, json, Post{}); + } + + auto err = socket->recvstart([=](auto nread, auto buf, auto addr) { + if (nread == UV_EOF) { + auto json = JSON::Object::Entries { + {"source", "udp.readStart"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"EOF", true} + }} + }; + + callback("-1", json, Post{}); + } else if (nread > 0 && buf && buf->base) { + char address[17] = {0}; + Post post; + int port; + + IP::parseAddress((struct sockaddr *) addr, &port, address); + + const auto headers = Headers {{ + {"content-type" ,"application/octet-stream"}, + {"content-length", nread} + }}; + + post.id = rand64(); + post.body.reset(buf->base); + post.length = (int) nread; + post.headers = headers.str(); + + const auto json = JSON::Object::Entries { + {"source", "udp.readStart"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"port", port}, + {"bytes", std::to_string(post.length)}, + {"address", address} + }} + }; + + callback("-1", json, post); + } + }); + + // `UV_EALREADY || UV_EBUSY` could mean there might be + // active IO on the underlying handle + if (err < 0 && err != UV_EALREADY && err != UV_EBUSY) { + auto json = JSON::Object::Entries { + {"source", "udp.readStart"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.readStart"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)} + }} + }; + + callback(seq, json, Post {}); + } + + void CoreUDP::readStop ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this] { + if (!this->hasSocket(id)) { + auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.readStop", id); + return callback(seq, json, Post{}); + } + + auto socket = this->getSocket(id); + + if (socket->isClosed()) { + auto json = ERR_SOCKET_DGRAM_CLOSED("udp.readStop", id); + return callback(seq, json, Post{}); + } + + if (socket->isClosing()) { + auto json = ERR_SOCKET_DGRAM_CLOSING("udp.readStop", id); + return callback(seq, json, Post{}); + } + + if (!socket->hasState(SOCKET_STATE_UDP_RECV_STARTED)) { + auto json = JSON::Object::Entries { + {"source", "udp.readStop"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", "Socket is not receiving"} + }} + }; + + return callback(seq, json, Post{}); + } + + auto err = socket->recvstop(); + + if (err < 0) { + auto json = JSON::Object::Entries { + {"source", "udp.readStop"}, + {"err", JSON::Object::Entries { + {"id", std::to_string(id)}, + {"message", String(uv_strerror(err))} + }} + }; + + return callback(seq, json, Post{}); + } + + auto json = JSON::Object::Entries { + {"source", "udp.readStop"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)} + }} + }; + + callback(seq, json, Post {}); + }); + } + + void CoreUDP::close ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ) { + this->core->dispatchEventLoop([=, this]() { + if (!this->hasSocket(id)) { + auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.close", id); + return callback(seq, json, Post{}); + } + + auto socket = this->getSocket(id); + + if (!socket->isUDP()) { + auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.close", id); + return callback(seq, json, Post{}); + } + + if (socket->isClosed()) { + auto json = ERR_SOCKET_DGRAM_CLOSED("udp.close", id); + return callback(seq, json, Post{}); + } + + if (socket->isClosing()) { + auto json = ERR_SOCKET_DGRAM_CLOSING("udp.close", id); + return callback(seq, json, Post{}); + } + + socket->close([=, this]() { + auto json = JSON::Object::Entries { + {"source", "udp.close"}, + {"data", JSON::Object::Entries { + {"id", std::to_string(id)} + }} + }; + + callback(seq, json, Post{}); + }); + }); + } +} diff --git a/src/core/modules/udp.hh b/src/core/modules/udp.hh new file mode 100644 index 0000000000..7723fe2c31 --- /dev/null +++ b/src/core/modules/udp.hh @@ -0,0 +1,113 @@ +#ifndef SOCKET_RUNTIME_CORE_MODULE_UDP_H +#define SOCKET_RUNTIME_CORE_MODULE_UDP_H + +#include "../module.hh" +#include "../socket.hh" + +namespace SSC { + class Core; + class CoreUDP : public CoreModule { + public: + using ID = uint64_t; + using Sockets = std::map<ID, SharedPointer<Socket>>; + + struct BindOptions { + String address; + int port; + bool reuseAddr = false; + }; + + struct ConnectOptions { + String address; + int port; + }; + + struct SendOptions { + String address = ""; + int port = 0; + SharedPointer<char[]> bytes = nullptr; + size_t size = 0; + bool ephemeral = false; + }; + + Mutex mutex; + Sockets sockets; + + CoreUDP (Core* core) + : CoreModule(core) + {} + + void bind ( + const String& seq, + ID id, + const BindOptions& options, + const CoreModule::Callback& callback + ); + + void close ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void connect ( + const String& seq, + ID id, + const ConnectOptions& options, + const CoreModule::Callback& callback + ); + + void disconnect ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void getPeerName ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void getSockName ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void getState ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void readStart ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void readStop ( + const String& seq, + ID id, + const CoreModule::Callback& callback + ); + + void send ( + const String& seq, + ID id, + const SendOptions& options, + const CoreModule::Callback& callback + ); + + void resumeAllSockets (); + void pauseAllSockets (); + bool hasSocket (ID id); + void removeSocket (ID id); + void removeSocket (ID id, bool autoClose); + SharedPointer<Socket> getSocket (ID id); + SharedPointer<Socket> createSocket (socket_type_t type, ID id); + SharedPointer<Socket> createSocket (socket_type_t type, ID id, bool isEphemeral); + }; +} +#endif diff --git a/src/core/options.hh b/src/core/options.hh new file mode 100644 index 0000000000..37bf17ee8f --- /dev/null +++ b/src/core/options.hh @@ -0,0 +1,14 @@ +#ifndef SOCKET_RUNTIME_CORE_OPTIONS_H +#define SOCKET_RUNTIME_CORE_OPTIONS_H + +#include "../platform/types.hh" + +namespace SSC { + struct Options { + template <typename T> const T& as () const { + static_assert(std::is_base_of<Options, T>::value); + return *reinterpret_cast<const T*>(this); + } + }; +} +#endif diff --git a/src/core/post.hh b/src/core/post.hh new file mode 100644 index 0000000000..fda395226d --- /dev/null +++ b/src/core/post.hh @@ -0,0 +1,32 @@ +#ifndef SOCKET_RUNTIME_CORE_POST_H +#define SOCKET_RUNTIME_CORE_POST_H + +#include "../platform/types.hh" + +namespace SSC { + struct Post { + using EventStreamCallback = Function<bool( + const char*, + const char*, + bool + )>; + + using ChunkStreamCallback = Function<bool( + const char*, + size_t, + bool + )>; + + uint64_t id = 0; + uint64_t ttl = 0; + SharedPointer<char[]> body = nullptr; + size_t length = 0; + String headers = ""; + String workerId = ""; + SharedPointer<EventStreamCallback> eventStream = nullptr; + SharedPointer<ChunkStreamCallback> chunkStream = nullptr; + }; + + using Posts = std::map<uint64_t, Post>; +} +#endif diff --git a/src/core/preload.cc b/src/core/preload.cc deleted file mode 100644 index a0f737d81a..0000000000 --- a/src/core/preload.cc +++ /dev/null @@ -1,201 +0,0 @@ -#include "codec.hh" -#include "preload.hh" -#include "string.hh" - -namespace SSC { - String createPreload ( - const WindowOptions opts, - const PreloadOptions preloadOptions - ) { - auto argv = opts.argv; - #ifdef _WIN32 - // Escape backslashes in paths. - size_t last_pos = 0; - while ((last_pos = argv.find('\\', last_pos)) != std::string::npos) { - argv.replace(last_pos, 1, "\\\\"); - last_pos += 2; - } - #endif - - auto preload = String( - ";(() => { \n" - " if (globalThis.__args) return; \n" - " globalThis.__args = {} \n" - " const env = '" + opts.env + "'; \n" - " Object.defineProperties(globalThis.__args, { \n" - " argv: { \n" - " value: [" +argv + "], \n" - " enumerable: true \n" - " }, \n" - " config: { \n" - " value: {}, \n" - " enumerable: true, \n" - " writable: true, \n" - " configurable: true \n" - " }, \n" - " debug: { \n" - " value: Boolean(" + std::to_string(opts.debug) + "), \n" - " enumerable: true \n" - " }, \n" - " headless: { \n" - " value: Boolean(" + std::to_string((int) opts.headless) + "), \n" - " enumerable: true \n" - " }, \n" - " env: { \n" - " value: Object.fromEntries(new URLSearchParams(env)), \n" - " enumerable: true \n" - " }, \n" - " index: { \n" - " value: Number('" + std::to_string(opts.index) + "'), \n" - " enumerable: true \n" - " } \n" - "}); \n" - " \n" - ); - - if (opts.index == 0) { - const auto start = argv.find("--test="); - if (start != std::string::npos) { - auto end = argv.find("'", start); - if (end == std::string::npos) { - end = argv.size(); - } - const auto file = argv.substr(start + 7, end - start - 7); - if (file.size() > 0) { - preload += ( - " globalThis.RUNTIME_TEST_FILENAME = `" + file + "`; \n" - " document.addEventListener('DOMContentLoaded', () => { \n" - " const script = document.createElement('script') \n" - " script.setAttribute('type', 'module') \n" - " script.setAttribute('src', `" + file + "`) \n" - " document.head.appendChild(script) \n" - " }); \n" - ); - } - } - } - - // buffer `applicationurl` events - preload += ( - " Object.defineProperty( \n" - " globalThis, \n" - " 'APPLICATION_URL_EVENT_BACKLOG', \n" - " { enumerable: false, configurable: false, value: [] } \n" - " ); \n" - " \n" - " globalThis.addEventListener('applicationurl', (e) => { \n" - " if (document.readyState !== 'complete') { \n" - " APPLICATION_URL_EVENT_BACKLOG.push(e); \n" - " } \n" - " }); \n" - " \n" - " globalThis.addEventListener(' __runtime_init__', () => { \n" - " if (Array.isArray(APPLICATION_URL_EVENT_BACKLOG)) { \n" - " for (const event of APPLICATION_URL_EVENT_BACKLOG) { \n" - " globalThis.dispatchEvent(event); \n" - " } \n" - " } \n" - " }, { once: true }); \n" - ); - - if (opts.appData.contains("webview_watch") && opts.appData.at("webview_watch") == "true") { - if ( - !opts.appData.contains("webview_watch_reload") || - opts.appData.at("webview_watch_reload") != "false" - ) { - preload += ( - " globalThis.addEventListener('filedidchange', () => { \n" - " location.reload() \n" - " }); \n" - ); - } - } - - // fill in the config - for (auto const &tuple : opts.appData) { - auto key = trim(tuple.first); - auto value = trim(tuple.second); - - // skip empty key/value and comments - if (key.size() == 0 || value.size() == 0 || key.rfind(";", 0) == 0) { - continue; - } - - preload += ( - " ;(() => { \n" - " let key = decodeURIComponent( \n" - " '" + encodeURIComponent(key) + "' \n" - " ) \n" - ); - - if (key.starts_with("env_")) { - preload += ( - " const k = key.slice(4); \n" - " const value = `" + value + "`; \n" - " globalThis.__args.env[k] = value; \n" - " globalThis.__args.config[key] = value; \n" - ); - } else if (value == "true" || value == "false") { - preload += ( - " const k = key.toLowerCase(); \n" - " globalThis.__args.config[k] = " + value + " \n" - ); - } else { - preload += ( - " const k = key.toLowerCase(); \n" - " const value = '" + encodeURIComponent(value) + "' \n" - " if (!isNaN(value) && !Number.isNaN(parseFloat(value))) { \n" - " globalThis.__args.config[k] = parseFloat(value) ; \n" - " } else { \n" - " let val = decodeURIComponent(value); \n" - " try { val = JSON.parse(val) } catch (err) {} \n" - " globalThis.__args.config[k] = val; \n" - " } \n" - ); - } - - preload += " })();\n"; - } - - preload += ( - " Object.freeze(globalThis.__args.config); \n" - " Object.freeze(globalThis.__args.argv); \n" - " Object.freeze(globalThis.__args.env); \n" - " \n" - " try { \n" - " const event = '__runtime_init__'; \n" - " let onload = null \n" - " Object.defineProperty(globalThis, 'onload', { \n" - " get: () => onload, \n" - " set (value) { \n" - " const opts = { once: true }; \n" - " if (onload) { \n" - " globalThis.removeEventListener(event, onload, opts); \n" - " onload = null; \n" - " } \n" - " \n" - " if (typeof value === 'function') { \n" - " onload = value; \n" - " globalThis.addEventListener(event, onload, opts); \n" - " } \n" - " } \n" - " }); \n" - " } catch {} \n" - "})(); \n" - ); - - preload += ( - "if (document.readyState === 'complete') { \n" - " import('socket:internal/init').catch(console.error); \n" - "} else { \n" - " document.addEventListener('readystatechange', () => { \n" - " if (/interactive|complete/.test(document.readyState)) { \n" - " import('socket:internal/init').catch(console.error); \n" - " } \n" - " }, { once: true }); \n" - "} \n" - ); - - return preload; - } -} diff --git a/src/core/preload.hh b/src/core/preload.hh deleted file mode 100644 index 871408ef63..0000000000 --- a/src/core/preload.hh +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef CORE_PRELOAD_HH -#define CORE_PRELOAD_HH - -#include "../window/options.hh" - -namespace SSC { - struct PreloadOptions { - bool module = false; - }; - - String createPreload ( - const WindowOptions opts, - const PreloadOptions preloadOptions - ); - - inline SSC::String createPreload (WindowOptions opts) { - return createPreload(opts, PreloadOptions {}); - } -} -#endif diff --git a/src/core/process.hh b/src/core/process.hh new file mode 100644 index 0000000000..f7f925bf42 --- /dev/null +++ b/src/core/process.hh @@ -0,0 +1,260 @@ +#ifndef SOCKET_RUNTIME_CORE_PROCESS_H +#define SOCKET_RUNTIME_CORE_PROCESS_H + +#include "../platform/platform.hh" + +#if SOCKET_RUNTIME_PLATFORM_WINDOWS +#include <tlhelp32.h> +#endif + +#if !SOCKET_RUNTIME_PLATFORM_IOS +#include <iostream> +#endif + +#ifndef WIFEXITED +#define WIFEXITED(w) ((w) & 0x7f) +#endif + +#ifndef WEXITSTATUS +#define WEXITSTATUS(w) (((w) & 0xff00) >> 8) +#endif + +namespace SSC { + struct ExecOutput { + String output; + int exitCode = 0; + }; + + // Additional parameters to Process constructors. + struct ProcessConfig { + // Buffer size for reading stdout and stderr. Default is 131072 (128 kB). + size_t bufferSize = 131072; + // Set to true to inherit file descriptors from parent process. Default is false. + // On Windows: has no effect unless readStdout==nullptr, readStderr==nullptr and openStdin==false. + bool inheritFDs = false; + + // On Windows only: controls how the process is started, mimics STARTUPINFO's wShowWindow. + // See: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/ns-processthreadsapi-startupinfoa + // and https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-showwindow + enum class ShowWindow { + hide = 0, + show_normal = 1, + show_minimized = 2, + maximize = 3, + show_maximized = 3, + show_no_activate = 4, + show = 5, + minimize = 6, + show_min_no_active = 7, + show_na = 8, + restore = 9, + show_default = 10, + force_minimize = 11 + }; + // On Windows only: controls how the window is shown. + ShowWindow show_window{ShowWindow::show_default}; + }; + +#if !SOCKET_RUNTIME_PLATFORM_IOS + inline ExecOutput exec (String command) { + command = command + " 2>&1"; + + ExecOutput eo; + FILE* pipe; + size_t count; + int exitCode = 0; + const int bufsize = 128; + Array<char, 128> buffer; + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + // + // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/popen-wpopen?view=msvc-160 + // _popen works fine in a console application... ok fine that's all we need it for... thanks. + // + pipe = _popen((const char*) command.c_str(), "rt"); + #else + pipe = popen((const char*) command.c_str(), "r"); + #endif + + if (pipe == NULL) { + std::cout << "error: unable to open the command" << std::endl; + exit(1); + } + + do { + if ((count = fread(buffer.data(), 1, bufsize, pipe)) > 0) { + eo.output.insert(eo.output.end(), std::begin(buffer), std::next(std::begin(buffer), count)); + } + } while (count > 0); + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + exitCode = _pclose(pipe); + #else + exitCode = pclose(pipe); + #endif + + if (!WIFEXITED(exitCode) || exitCode != 0) { + auto status = WEXITSTATUS(exitCode); + if (status && exitCode) { + exitCode = status; + } + } + + eo.exitCode = exitCode; + + return eo; + } +#endif + + // Platform independent class for creating processes. + // Note on Windows: it seems not possible to specify which pipes to redirect. + // Thus, at the moment, if readStdout==nullptr, readStderr==nullptr and openStdin==false, + // the stdout, stderr and stdin are sent to the parent process instead. + class Process { + public: + static constexpr auto PROCESS_WAIT_TIMEOUT = 256; + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + typedef unsigned long PID; // process id (pid) type + typedef void *FD; // file descriptor type + #elif SOCKET_RUNTIME_PLATFORM_IOS + typedef int PID; + typedef int FD; + #else + typedef pid_t PID; + typedef int FD; + #endif + + String command; + String argv; + String path; + Vector<String> env; + Atomic<bool> closed = true; + Atomic<int> status = -1; + Atomic<int> lastWriteStatus = 0; + bool detached = false; + bool openStdin; + PID id = 0; + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + String shell = ""; + #else + String shell = "/bin/sh"; + #endif + + private: + + class Data { + public: + PID id; + int exitStatus = -1; + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + void* handle = nullptr; + #endif + + Data() noexcept; + }; + + public: + Process( + const String &command, + const String &argv, + const SSC::Vector<SSC::String> &env, + const String &path = String(""), + MessageCallback readStdout = nullptr, + MessageCallback readStderr = nullptr, + MessageCallback onExit = nullptr, + bool openStdin = true, + const ProcessConfig &config = {} + ) noexcept; + + Process( + const String &command, + const String &argv, + const String &path = String(""), + MessageCallback readStdout = nullptr, + MessageCallback readStderr = nullptr, + MessageCallback onExit = nullptr, + bool openStdin = true, + const ProcessConfig &config = {} + ) noexcept; + + #if !SOCKET_RUNTIME_PLATFORM_WINDOWS + // Starts a process with the environment of the calling process. + // Supported on Unix-like systems only. + Process ( + const std::function<int()> &function, + MessageCallback readStdout = nullptr, + MessageCallback readStderr = nullptr, + MessageCallback onExit = nullptr, + bool openStdin = true, + const ProcessConfig &config = {} + ) noexcept; + #endif + + ~Process () noexcept { + closeFDs(); + }; + + // Get the process id of the started process. + PID getPID () const noexcept { + return data.id; + } + + // Write to stdin. + bool write (const char *bytes, size_t size); + bool write (const SharedPointer<char[]> bytes, size_t size) { + return write(bytes.get(), size); + } + + // Write to stdin. Convenience function using write(const char *, size_t). + bool write (const String &string) { + return write(string.c_str(), string.size()); + } + + // Close stdin. If the process takes parameters from stdin, use this to + // notify that all parameters have been sent. + void closeStdin () noexcept; + PID open () noexcept { + if (this->command.size() == 0) return 0; + auto str = trim(this->command + " " + this->argv); + auto pid = open(str, this->path); + read(); + return pid; + } + + void kill (PID id) noexcept; + void kill () noexcept { + this->kill(this->getPID()); + } + + int wait (); + + private: + Data data; + std::mutex closeMutex; + MessageCallback readStdout; + MessageCallback readStderr; + MessageCallback onExit; + Mutex stdinMutex; + Mutex stdoutMutex; + Mutex stderrMutex; + ProcessConfig config; + + #if !SOCKET_RUNTIME_PLATFORM_WINDOWS + Thread stdoutAndStderrThread; + #else + Thread stdoutThread, stderrThread; + #endif + + UniquePointer<FD> stdoutFD, stderrFD, stdinFD; + + void read () noexcept; + void closeFDs () noexcept; + PID open (const String &command, const String &path) noexcept; + #if !SOCKET_RUNTIME_PLATFORM_WINDOWS + PID open (const Function<int()> &function) noexcept; + #endif + }; +} +#endif diff --git a/src/core/process/unix.cc b/src/core/process/unix.cc new file mode 100644 index 0000000000..307bb115cc --- /dev/null +++ b/src/core/process/unix.cc @@ -0,0 +1,484 @@ +#include <algorithm> +#include <bitset> +#include <cstdlib> +#include <cstring> +#include <fcntl.h> +#include <poll.h> +#include <set> +#include <signal.h> +#include <sstream> +#include <stdexcept> +#include <sys/wait.h> +#include <unistd.h> + +#include "../process.hh" +#include "../debug.hh" +#include "../modules/timers.hh" + +extern char **environ; + +namespace SSC { + static StringStream initial; + + Process::Data::Data () noexcept + : id(-1) + {} + + Process::Process ( + const String &command, + const String &argv, + const Vector<String> &env, + const String &path, + MessageCallback readStdout, + MessageCallback readStderr, + MessageCallback onExit, + bool openStdin, + const ProcessConfig &config + ) noexcept + : openStdin(true), + readStdout(std::move(readStdout)), + readStderr(std::move(readStderr)), + onExit(std::move(onExit)), + env(env), + command(command), + argv(argv), + path(path) + {} + + Process::Process ( + const String &command, + const String &argv, + const String &path, + MessageCallback readStdout, + MessageCallback readStderr, + MessageCallback onExit, + bool openStdin, + const ProcessConfig &config + ) noexcept + : openStdin(true), + readStdout(std::move(readStdout)), + readStderr(std::move(readStderr)), + onExit(std::move(onExit)), + command(command), + argv(argv), + path(path) + {} + + Process::Process ( + const Function<int()> &function, + MessageCallback readStdout, + MessageCallback readStderr, + MessageCallback onExit, + bool openStdin, + const ProcessConfig &config + ) noexcept + : readStdout(std::move(readStdout)), + readStderr(std::move(readStderr)), + onExit(std::move(onExit)), + openStdin(openStdin), + config(config) + { + #if !SOCKET_RUNTIME_PLATFORM_IOS + open(function); + read(); + #endif + } + + Process::PID Process::open (const Function<int()> &function) noexcept { + #if SOCKET_RUNTIME_PLATFORM_IOS + return -1; // -EPERM + #else + + if (openStdin) { + stdinFD = UniquePointer<FD>(new FD); + } + + if (readStdout) { + stdoutFD = UniquePointer<FD>(new FD); + } + + if (readStderr) { + stderrFD = UniquePointer<FD>(new FD); + } + + int stdin_p[2]; + int stdout_p[2]; + int stderr_p[2]; + + if (stdinFD && pipe(stdin_p) != 0) { + return -1; + } + + if (stdoutFD && pipe(stdout_p) != 0) { + if (stdinFD) { + close(stdin_p[0]); + close(stdin_p[1]); + } + return -1; + } + + if (stderrFD && pipe(stderr_p) != 0) { + if (stdinFD) { + close(stdin_p[0]); + close(stdin_p[1]); + } + + if (stdoutFD) { + close(stdout_p[0]); + close(stdout_p[1]); + } + + return -1; + } + + PID pid = fork(); + + if (pid < 0) { + if (stdinFD) { + close(stdin_p[0]); + close(stdin_p[1]); + } + + if (stdoutFD) { + close(stdout_p[0]); + close(stdout_p[1]); + } + + if (stderrFD) { + close(stderr_p[0]); + close(stderr_p[1]); + } + + return pid; + } + + closed = false; + id = pid; + + if (pid > 0) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + setpgid(pid, getpgid(0)); + #endif + + auto thread = Thread([this] { + int code = 0; + waitpid(this->id, &code, 0); + + this->status = WEXITSTATUS(code); + this->closed = true; + + if (this->onExit != nullptr) { + this->onExit(std::to_string(status)); + } + }); + + thread.detach(); + } else if (pid == 0) { + if (stdinFD) { + dup2(stdin_p[0], 0); + } + + if (stdoutFD) { + dup2(stdout_p[1], 1); + } + + if (stderrFD) { + dup2(stderr_p[1], 2); + } + + if (stdinFD) { + close(stdin_p[0]); + close(stdin_p[1]); + } + + if (stdoutFD) { + close(stdout_p[0]); + close(stdout_p[1]); + } + + if (stderrFD) { + close(stderr_p[0]); + close(stderr_p[1]); + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + setpgid(0, 0); + #endif + + if (function) { + function(); + } + + _exit(EXIT_FAILURE); + } + + if (stdinFD) { + close(stdin_p[0]); + } + + if (stdoutFD) { + close(stdout_p[1]); + } + + if (stderrFD) { + close(stderr_p[1]); + } + + if (stdinFD) { + *stdinFD = stdin_p[1]; + } + + if (stdoutFD) { + *stdoutFD = stdout_p[0]; + } + + if (stderrFD) { + *stderrFD = stderr_p[0]; + } + + data.id = pid; + return pid; + #endif + } + + Process::PID Process::open (const String &command, const String &path) noexcept { + #if SOCKET_RUNTIME_PLATFORM_IOS + return -1; // -EPERM + #else + + std::vector<char*> newEnv; + + for (char** env = environ; *env != nullptr; ++env) { + newEnv.push_back(strdup(*env)); + } + + for (const auto& str : this->env) { + newEnv.push_back(const_cast<char*>(str.c_str())); + } + + newEnv.push_back(nullptr); + + return open([&command, &path, &newEnv, this] { + auto command_c_str = command.c_str(); + String cd_path_and_command; + + if (!path.empty()) { + auto path_escaped = path; + size_t pos = 0; + + while ((pos = path_escaped.find('\'', pos)) != String::npos) { + path_escaped.replace(pos, 1, "'\\''"); + pos += 4; + } + + cd_path_and_command = "cd '" + path_escaped + "' && " + command; // To avoid resolving symbolic links + command_c_str = cd_path_and_command.c_str(); + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + setpgid(0, 0); + #endif + + if (this->shell.size() > 0) { + return execle(this->shell.c_str(), this->shell.c_str(), "-c", command_c_str, (char*)nullptr, newEnv.data()); + } else { + return execle("/bin/sh", "/bin/sh", "-c", command_c_str, (char*)nullptr, newEnv.data()); + } + }); + #endif + } + + int Process::wait () { + #if SOCKET_RUNTIME_PLATFORM_IOS + return -1; // -EPERM + #else + do { + msleep(Process::PROCESS_WAIT_TIMEOUT); + } while (this->closed == false); + + return this->status; + #endif + } + + void Process::read() noexcept { + #if !SOCKET_RUNTIME_PLATFORM_IOS + if (data.id <= 0 || (!stdoutFD && !stderrFD)) { + return; + } + + stdoutAndStderrThread = Thread([this] { + Vector<pollfd> pollfds; + std::bitset<2> fd_is_stdout; + + if (stdoutFD) { + fd_is_stdout.set(pollfds.size()); + pollfds.emplace_back(); + pollfds.back().fd = fcntl(*stdoutFD, F_SETFL, fcntl(*stdoutFD, F_GETFL) | O_NONBLOCK) == 0 ? *stdoutFD : -1; + pollfds.back().events = POLLIN; + } + + if (stderrFD) { + pollfds.emplace_back(); + pollfds.back().fd = fcntl(*stderrFD, F_SETFL, fcntl(*stderrFD, F_GETFL) | O_NONBLOCK) == 0 ? *stderrFD : -1; + pollfds.back().events = POLLIN; + } + + auto buffer = UniquePointer<char[]>(new char[config.bufferSize]); + bool any_open = !pollfds.empty(); + StringStream ss; + + while (any_open && (poll(pollfds.data(), static_cast<nfds_t>(pollfds.size()), -1) > 0 || errno == EINTR)) { + any_open = false; + + for (size_t i = 0; i < pollfds.size(); ++i) { + if (!(pollfds[i].fd >= 0)) continue; + if (pollfds[i].revents & POLLIN) { + memset(buffer.get(), 0, config.bufferSize); + const ssize_t n = ::read(pollfds[i].fd, buffer.get(), config.bufferSize); + + if (n > 0) { + if (fd_is_stdout[i]) { + Lock lock(stdoutMutex); + auto b = String(buffer.get()); + auto parts = splitc(b, '\n'); + + if (parts.size() > 1) { + for (int i = 0; i < parts.size() - 1; i++) { + ss << parts[i]; + + String s(ss.str()); + readStdout(s); + + ss.str(String()); + ss.clear(); + ss.copyfmt(initial); + } + ss << parts[parts.size() - 1]; + } else { + ss << b; + } + } else { + Lock lock(stderrMutex); + readStderr(String(buffer.get())); + } + } else if (n < 0 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { + pollfds[i].fd = -1; + continue; + } + } + + if (pollfds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) { + pollfds[i].fd = -1; + continue; + } + + any_open = true; + } + } + }); + #endif + } + + void Process::closeFDs () noexcept { + #if !SOCKET_RUNTIME_PLATFORM_IOS + if (stdoutAndStderrThread.joinable()) { + stdoutAndStderrThread.join(); + } + + if (stdinFD) { + closeStdin(); + } + + if (stdoutFD) { + if (data.id > 0) { + close(*stdoutFD); + } + + stdoutFD.reset(); + } + + if (stderrFD) { + if (data.id > 0) { + close(*stderrFD); + } + + stderrFD.reset(); + } + #endif + } + + bool Process::write (const char *bytes, size_t n) { + #if SOCKET_RUNTIME_PLATFORM_IOS + return false; + #else + Lock lock(stdinMutex); + + this->lastWriteStatus = 0; + + if (stdinFD) { + String b(bytes); + + while (true && (b.size() > 0)) { + int bytesWritten = ::write(*stdinFD, b.c_str(), b.size()); + + if (bytesWritten == -1) { + this->lastWriteStatus = errno; + return false; + } + + if (bytesWritten >= b.size()) { + break; + } + + b = b.substr(bytesWritten, b.size()); + } + + int bytesWritten = ::write(*stdinFD, "\n", 1); + if (bytesWritten == -1) { + this->lastWriteStatus = errno; + return false; + } + + return true; + } + + return false; + #endif + } + + void Process::closeStdin () noexcept { + #if !SOCKET_RUNTIME_PLATFORM_IOS + Lock lock(stdinMutex); + + if (stdinFD) { + if (data.id > 0) { + close(*stdinFD); + } + + stdinFD.reset(); + } + #endif + } + + void Process::kill (PID id) noexcept { + #if !SOCKET_RUNTIME_PLATFORM_IOS + if (id <= 0 || ::kill(-id, 0) != 0) { + return; + } + + auto r = ::kill(-id, SIGTERM); + + if (r != 0 && ::kill(-id, 0) == 0) { + r = ::kill(-id, SIGINT); + + if (r != 0 && ::kill(-id, 0) == 0 ) { + r = ::kill(-id, SIGKILL); + + if (r != 0 && ::kill(-id, 0) == 0) { + debug("Process: Failed to kill process %d", id); + } + } + } + #endif + } +} diff --git a/src/process/win.cc b/src/core/process/win.cc similarity index 57% rename from src/process/win.cc rename to src/core/process/win.cc index fa5ab71378..ec8ea5c454 100644 --- a/src/process/win.cc +++ b/src/core/process/win.cc @@ -1,35 +1,14 @@ -#include "process.hh" - #include <cstring> #include <iostream> #include <stdexcept> -#include <tlhelp32.h> #include <limits.h> -namespace SSC { +#include "../process.hh" +#include "../env.hh" -#if defined(SSC_CLI) -SSC::String FormatError(DWORD error, SSC::String source) { - SSC::StringStream message; - LPVOID lpMsgBuf; - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &lpMsgBuf, - 0, NULL ); - - message << "Error " << error << " in " << source << ": " << (LPTSTR)lpMsgBuf; - LocalFree(lpMsgBuf); - - return message.str(); -} -#endif +namespace SSC { -const static SSC::StringStream initial; +const static StringStream initial; Process::Data::Data() noexcept : id(0) {} @@ -37,22 +16,51 @@ Process::Process( const String &command, const String &argv, const String &path, - MessageCallback read_stdout, - MessageCallback read_stderr, - MessageCallback on_exit, - bool open_stdin, + MessageCallback readStdout, + MessageCallback readStderr, + MessageCallback onExit, + bool openStdin, const ProcessConfig &config ) noexcept : - open_stdin(true), - read_stdout(std::move(read_stdout)), - read_stderr(std::move(read_stderr)), - on_exit(std::move(on_exit)) + openStdin(true), + readStdout(std::move(readStdout)), + readStderr(std::move(readStderr)), + onExit(std::move(onExit)) { this->command = command; this->argv = argv; this->path = path; } +Process::Process ( + const String &command, + const String &argv, + const Vector<String> &env, + const String &path, + MessageCallback readStdout, + MessageCallback readStderr, + MessageCallback onExit, + bool openStdin, + const ProcessConfig &config + ) noexcept + : openStdin(true), + readStdout(std::move(readStdout)), + readStderr(std::move(readStderr)), + onExit(std::move(onExit)), + env(env), + command(command), + argv(argv), + path(path) +{} + +int Process::wait () { + do { + msleep(Process::PROCESS_WAIT_TIMEOUT); + } while (this->closed == false); + + return this->status; +} + // Simple HANDLE wrapper to close it automatically from the destructor. class Handle { public: @@ -82,20 +90,20 @@ class Handle { }; //Based on the discussion thread: https://www.reddit.com/r/cpp/comments/3vpjqg/a_new_platform_independent_process_library_for_c11/cxq1wsj -std::mutex create_process_mutex; +std::recursive_mutex create_processMutex; //Based on the example at https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx. -Process::id_type Process::open(const SSC::String &command, const SSC::String &path) noexcept { - if (open_stdin) { - stdin_fd = std::unique_ptr<fd_type>(new fd_type(nullptr)); +Process::PID Process::open (const String &command, const String &path) noexcept { + if (openStdin) { + stdinFD = UniquePointer<Process::FD>(new Process::FD(nullptr)); } - if (read_stdout) { - stdout_fd = std::unique_ptr<fd_type>(new fd_type(nullptr)); + if (readStdout) { + stdoutFD = UniquePointer<Process::FD>(new Process::FD(nullptr)); } - if (read_stderr) { - stderr_fd = std::unique_ptr<fd_type>(new fd_type(nullptr)); + if (readStderr) { + stderrFD = UniquePointer<Process::FD>(new Process::FD(nullptr)); } Handle stdin_rd_p; @@ -111,22 +119,22 @@ Process::id_type Process::open(const SSC::String &command, const SSC::String &pa security_attributes.bInheritHandle = TRUE; security_attributes.lpSecurityDescriptor = nullptr; - std::lock_guard<std::mutex> lock(create_process_mutex); + Lock lock(create_processMutex); - if (stdin_fd) { + if (stdinFD) { if (!CreatePipe(&stdin_rd_p, &stdin_wr_p, &security_attributes, 0) || !SetHandleInformation(stdin_wr_p, HANDLE_FLAG_INHERIT, 0)) return 0; } - if (stdout_fd) { + if (stdoutFD) { if (!CreatePipe(&stdout_rd_p, &stdout_wr_p, &security_attributes, 0) || !SetHandleInformation(stdout_rd_p, HANDLE_FLAG_INHERIT, 0)) { return 0; } } - if (stderr_fd) { + if (stderrFD) { if (!CreatePipe(&stderr_rd_p, &stderr_wr_p, &security_attributes, 0) || !SetHandleInformation(stderr_rd_p, HANDLE_FLAG_INHERIT, 0)) { return 0; @@ -137,14 +145,14 @@ Process::id_type Process::open(const SSC::String &command, const SSC::String &pa STARTUPINFO startup_info; ZeroMemory(&process_info, sizeof(PROCESS_INFORMATION)); - ZeroMemory(&startup_info, sizeof(STARTUPINFO)); + startup_info.cb = sizeof(STARTUPINFO); startup_info.hStdInput = stdin_rd_p; startup_info.hStdOutput = stdout_wr_p; startup_info.hStdError = stderr_wr_p; - if (stdin_fd || stdout_fd || stderr_fd) + if (stdinFD || stdoutFD || stderrFD) startup_info.dwFlags |= STARTF_USESTDHANDLES; if (config.show_window != ProcessConfig::ShowWindow::show_default) { @@ -155,12 +163,12 @@ Process::id_type Process::open(const SSC::String &command, const SSC::String &pa auto process_command = command; #ifdef MSYS_PROCESS_USE_SH size_t pos = 0; - while((pos = process_command.find('\\', pos)) != string_type::npos) { + while((pos = process_command.find('\\', pos)) != String::npos) { process_command.replace(pos, 1, "\\\\\\\\"); pos += 4; } pos = 0; - while((pos = process_command.find('\"', pos)) != string_type::npos) { + while((pos = process_command.find('\"', pos)) != String::npos) { process_command.replace(pos, 1, "\\\""); pos += 2; } @@ -168,13 +176,25 @@ Process::id_type Process::open(const SSC::String &command, const SSC::String &pa process_command += "\""; #endif + auto comspec = Env::get("COMSPEC"); + auto shell = this->shell; + + if (shell == "cmd.exe" && comspec.size() > 0) { + shell = comspec; + } + + auto cmd = ( + (this->shell == "cmd.exe" ? String("/d /s /c ") : "") + + (process_command.empty() ? "": &process_command[0]) + ).c_str(); + BOOL bSuccess = CreateProcess( - nullptr, - process_command.empty() ? nullptr : &process_command[0], + (shell.size() > 0 ? shell.c_str() : nullptr), + const_cast<LPSTR>(cmd), nullptr, nullptr, - stdin_fd || stdout_fd || stderr_fd || config.inherit_file_descriptors, // Cannot be false when stdout, stderr or stdin is used - stdin_fd || stdout_fd || stderr_fd ? CREATE_NO_WINDOW : 0, // CREATE_NO_WINDOW cannot be used when stdout or stderr is redirected to parent process + stdinFD || stdoutFD || stderrFD || config.inheritFDs, // Cannot be false when stdout, stderr or stdin is used + stdinFD || stdoutFD || stderrFD ? CREATE_NO_WINDOW : 0, // CREATE_NO_WINDOW cannot be used when stdout or stderr is redirected to parent process nullptr, path.empty() ? nullptr : path.c_str(), &startup_info, @@ -182,33 +202,33 @@ Process::id_type Process::open(const SSC::String &command, const SSC::String &pa ); if (!bSuccess) { - auto msg = SSC::String("Unable to execute: " + process_command); + auto msg = String("Unable to execute: " + process_command); MessageBoxA(nullptr, &msg[0], "Alert", MB_OK | MB_ICONSTOP); return 0; } else { CloseHandle(process_info.hThread); } - if (stdin_fd) { - *stdin_fd = stdin_wr_p.detach(); + if (stdinFD) { + *stdinFD = stdin_wr_p.detach(); } - if (stdout_fd) { - *stdout_fd = stdout_rd_p.detach(); + if (stdoutFD) { + *stdoutFD = stdout_rd_p.detach(); } - if (stderr_fd) { - *stderr_fd = stderr_rd_p.detach(); + if (stderrFD) { + *stderrFD = stderr_rd_p.detach(); } auto processHandle = process_info.hProcess; - auto t = std::thread([&](HANDLE _processHandle) { + auto t = Thread([&](HANDLE _processHandle) { DWORD exitCode = 0; try { WaitForSingleObject(_processHandle, INFINITE); if (GetExitCodeProcess(_processHandle, &exitCode) == 0) { - std::cerr << FormatError(GetLastError(), "SSC::Process::open() GetExitCodeProcess()") << std::endl; + std::cerr << formatWindowsError(GetLastError(), "Process::open() GetExitCodeProcess()") << std::endl; exitCode = -1; } @@ -220,10 +240,10 @@ Process::id_type Process::open(const SSC::String &command, const SSC::String &pa this->status = (exitCode <= UINT_MAX ? exitCode : WEXITSTATUS(exitCode)); this->closed = true; - if (this->on_exit != nullptr) - this->on_exit(std::to_string(this->status)); + if (this->onExit != nullptr) + this->onExit(std::to_string(this->status)); } catch (std::exception e) { - std::cerr << "SSC::Process thread exception: " << e.what() << std::endl; + std::cerr << "Process thread exception: " << e.what() << std::endl; this->closed = true; } }, processHandle); @@ -244,32 +264,32 @@ void Process::read() noexcept { return; } - if (stdout_fd) { - stdout_thread = std::thread([this]() { + if (stdoutFD) { + stdoutThread = Thread([this]() { DWORD n; - std::unique_ptr<char[]> buffer(new char[config.buffer_size]); - SSC::StringStream ss; + UniquePointer<char[]> buffer(new char[config.bufferSize]); + StringStream ss; for (;;) { - memset(buffer.get(), 0, config.buffer_size); - BOOL bSuccess = ReadFile(*stdout_fd, static_cast<CHAR *>(buffer.get()), static_cast<DWORD>(config.buffer_size), &n, nullptr); + memset(buffer.get(), 0, config.bufferSize); + BOOL bSuccess = ReadFile(*stdoutFD, static_cast<CHAR *>(buffer.get()), static_cast<DWORD>(config.bufferSize), &n, nullptr); if (!bSuccess || n == 0) { break; } - auto b = SSC::String(buffer.get()); + auto b = String(buffer.get()); auto parts = splitc(b, '\n'); if (parts.size() > 1) { - std::lock_guard<std::mutex> lock(stdout_mutex); + Lock lock(stdoutMutex); for (int i = 0; i < parts.size() - 1; i++) { ss << parts[i]; - SSC::String s(ss.str()); - read_stdout(s); - ss.str(SSC::String()); + String s(ss.str()); + readStdout(s); + ss.str(String()); ss.clear(); ss.copyfmt(initial); } @@ -281,64 +301,64 @@ void Process::read() noexcept { }); } - if (stderr_fd) { - stderr_thread = std::thread([this]() { + if (stderrFD) { + stderrThread = Thread([this]() { DWORD n; - std::unique_ptr<char[]> buffer(new char[config.buffer_size]); + auto buffer = std::make_unique<char[]>(config.bufferSize); for (;;) { - BOOL bSuccess = ReadFile(*stderr_fd, static_cast<CHAR *>(buffer.get()), static_cast<DWORD>(config.buffer_size), &n, nullptr); + BOOL bSuccess = ReadFile(*stderrFD, static_cast<CHAR *>(buffer.get()), static_cast<DWORD>(config.bufferSize), &n, nullptr); if (!bSuccess || n == 0) break; - std::lock_guard<std::mutex> lock(stderr_mutex); - read_stderr(SSC::String(buffer.get())); + Lock lock(stderrMutex); + readStderr(String(buffer.get())); } }); } } -void Process::close_fds() noexcept { - if (stdout_thread.joinable()) { - stdout_thread.join(); +void Process::closeFDs() noexcept { + if (stdoutThread.joinable()) { + stdoutThread.join(); } - if (stderr_thread.joinable()) { - stderr_thread.join(); + if (stderrThread.joinable()) { + stderrThread.join(); } - if (stdin_fd) { - close_stdin(); + if (stdinFD) { + closeStdin(); } - if (stdout_fd) { - if (*stdout_fd != nullptr) { - CloseHandle(*stdout_fd); + if (stdoutFD) { + if (*stdoutFD != nullptr) { + CloseHandle(*stdoutFD); } - stdout_fd.reset(); + stdoutFD.reset(); } - if (stderr_fd) { - if (*stderr_fd != nullptr) { - CloseHandle(*stderr_fd); + if (stderrFD) { + if (*stderrFD != nullptr) { + CloseHandle(*stderrFD); } - stderr_fd.reset(); + stderrFD.reset(); } } bool Process::write(const char *bytes, size_t n) { - if (!open_stdin) { - throw std::invalid_argument("Can't write to an unopened stdin pipe. Please set open_stdin=true when constructing the process."); + if (!openStdin) { + throw std::invalid_argument("Can't write to an unopened stdin pipe. Please set openStdin=true when constructing the process."); } - std::lock_guard<std::mutex> lock(stdin_mutex); - if (stdin_fd) { - SSC::String b(bytes); + Lock lock(stdinMutex); + if (stdinFD) { + String b(bytes); while (true && (b.size() > 0)) { DWORD bytesWritten; DWORD size = static_cast<DWORD>(b.size()); - BOOL bSuccess = WriteFile(*stdin_fd, b.c_str(), size, &bytesWritten, nullptr); + BOOL bSuccess = WriteFile(*stdinFD, b.c_str(), size, &bytesWritten, nullptr); if (bytesWritten >= size || bSuccess) { break; @@ -348,7 +368,7 @@ bool Process::write(const char *bytes, size_t n) { } DWORD bytesWritten; - BOOL bSuccess = WriteFile(*stdin_fd, L"\n", static_cast<DWORD>(2), &bytesWritten, nullptr); + BOOL bSuccess = WriteFile(*stdinFD, L"\n", static_cast<DWORD>(2), &bytesWritten, nullptr); if (!bSuccess || bytesWritten == 0) { return false; @@ -360,26 +380,24 @@ bool Process::write(const char *bytes, size_t n) { return false; } -void Process::close_stdin() noexcept { - std::lock_guard<std::mutex> lock(stdin_mutex); +void Process::closeStdin () noexcept { + Lock lock(stdinMutex); - if (stdin_fd) { - if (*stdin_fd != nullptr) { - CloseHandle(*stdin_fd); + if (stdinFD) { + if (*stdinFD != nullptr) { + CloseHandle(*stdinFD); } - stdin_fd.reset(); + stdinFD.reset(); } } //Based on http://stackoverflow.com/a/1173396 -void Process::kill(id_type id) noexcept { +void Process::kill (PID id) noexcept { if (id == 0) { return; } - this->closed = true; - HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot) { @@ -401,6 +419,8 @@ void Process::kill(id_type id) noexcept { } CloseHandle(snapshot); + + this->closed = true; } HANDLE process_handle = OpenProcess(PROCESS_TERMINATE, FALSE, id); diff --git a/src/core/resource.cc b/src/core/resource.cc new file mode 100644 index 0000000000..9869da15cc --- /dev/null +++ b/src/core/resource.cc @@ -0,0 +1,1392 @@ +#include "config.hh" +#include "core.hh" +#include "debug.hh" +#include "resource.hh" + +#include "../platform/platform.hh" + +#if SOCKET_RUNTIME_PLATFORM_ANDROID +#include <fstream> +#endif + +namespace SSC { + static std::map<String, FileResource::Cache> caches; + static Mutex mutex; + static FileResource::WellKnownPaths defaultWellKnownPaths; + +#if SOCKET_RUNTIME_PLATFORM_ANDROID + static Android::AssetManager* sharedAndroidAssetManager = nullptr; + static Path externalAndroidStorageDirectory; + static Path externalAndroidFilesDirectory; + static Path externalAndroidCacheDirectory; +#endif + + std::map<String, Set<String>> FileResource::mimeTypes = { + {"application/font-woff", { ".woff" }}, + {"application/font-woff2", { ".woff2" }}, + {"application/x-font-opentype", { ".otf" }}, + {"application/x-font-ttf", { ".ttf" }}, + {"application/json", { ".json" }}, + {"application/ld+json", { ".jsonld" }}, + {"application/typescript", { ".ts", ".tsx" }}, + {"application/wasm", { ".wasm" }}, + {"audio/opus", { ".opux" }}, + {"audio/ogg", { ".oga" }}, + {"audio/mp3", { ".mp3" }}, + {"image/jpeg", { ".jpg", ".jpeg" }}, + {"image/png", { ".png" }}, + {"image/svg+xml", { ".svg" }}, + {"image/vnd.microsoft.icon", { ".ico" }}, + {"text/css", { ".css" }}, + {"text/html", { ".html" }}, + {"text/javascript", { ".js", ".cjs", ".mjs" }}, + {"text/plain", { ".txt", ".text" }}, + {"video/mp4", { ".mp4" }}, + {"video/mpeg", { ".mpeg" }}, + {"video/ogg", { ".ogv" }} + }; + + Resource::Resource (const String& type, const String& name) + : name(name), + type(type), + tracer(name) + {} + + bool Resource::hasAccess () const noexcept { + return this->accessing; + } + +#if SOCKET_RUNTIME_PLATFORM_ANDROID + static Path getRelativeAndroidAssetManagerPath (const Path& resourcePath) { + auto resourcesPath = FileResource::getResourcesPath(); + auto assetPath = replace(resourcePath.string(), resourcesPath.string(), ""); + + if (assetPath.starts_with("/")) { + assetPath = assetPath.substr(1); + } else if (assetPath.starts_with("./")) { + assetPath = assetPath.substr(2); + } + + return Path(assetPath); + } +#endif + +#if SOCKET_RUNTIME_PLATFORM_ANDROID + void FileResource::setSharedAndroidAssetManager (Android::AssetManager* assetManager) { + Lock lock(mutex); + sharedAndroidAssetManager = assetManager; + } + + Android::AssetManager* FileResource::getSharedAndroidAssetManager () { + return sharedAndroidAssetManager; + } + + void FileResource::setExternalAndroidStorageDirectory (const Path& directory) { + externalAndroidStorageDirectory = directory; + } + + Path FileResource::getExternalAndroidStorageDirectory () { + return externalAndroidStorageDirectory; + } + + void FileResource::setExternalAndroidFilesDirectory (const Path& directory) { + externalAndroidFilesDirectory = directory; + } + + Path FileResource::getExternalAndroidFilesDirectory () { + return externalAndroidFilesDirectory; + } + + void FileResource::setExternalAndroidCacheDirectory (const Path& directory) { + externalAndroidCacheDirectory = directory; + } + + Path FileResource::getExternalAndroidCacheDirectory () { + return externalAndroidCacheDirectory; + } +#endif + + bool FileResource::isFile (const String& resourcePath) { + return FileResource::isFile(Path(resourcePath)); + } + + bool FileResource::isFile (const Path& resourcePath) { + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (sharedAndroidAssetManager) { + const auto assetPath = getRelativeAndroidAssetManagerPath(resourcePath); + const auto asset = AAssetManager_open( + sharedAndroidAssetManager, + assetPath.c_str(), + AASSET_MODE_BUFFER + ); + + if (asset) { + AAsset_close(asset); + return true; + } + } + #elif SOCKET_RUNTIME_PLATFORM_APPLE + static auto fileManager = [[NSFileManager alloc] init]; + bool isDirectory = false; + const auto fileExistsAtPath = [fileManager + fileExistsAtPath: @(resourcePath.c_str()) + isDirectory: &isDirectory + ]; + + return fileExistsAtPath && !isDirectory; + #endif + + return fs::is_regular_file(resourcePath); + } + + bool FileResource::isDirectory (const String& resourcePath) { + return FileResource::isDirectory(Path(resourcePath)); + } + + bool FileResource::isDirectory (const Path& resourcePath) { + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (sharedAndroidAssetManager) { + const auto assetPath = getRelativeAndroidAssetManagerPath(resourcePath); + const auto assetDir = AAssetManager_openDir( + sharedAndroidAssetManager, + assetPath.c_str() + ); + + if (assetDir) { + AAssetDir_close(assetDir); + return true; + } + } + #elif SOCKET_RUNTIME_PLATFORM_APPLE + static auto fileManager = [[NSFileManager alloc] init]; + bool isDirectory = false; + const auto fileExistsAtPath = [fileManager + fileExistsAtPath: @(resourcePath.string().c_str()) + isDirectory: &isDirectory + ]; + + return fileExistsAtPath && isDirectory; + #endif + + return fs::is_directory(resourcePath); + } + + bool FileResource::isMountedPath (const Path& path) { + static auto userConfig = getUserConfig(); + static auto mounts = FileResource::getMountedPaths(); + for (const auto& entry : mounts) { + if (path.string().starts_with(entry.first)) { + return true; + } + } + return false; + } + + const Map FileResource::getMountedPaths () { + static auto userConfig = getUserConfig(); + static Map mounts = {}; + + if (mounts.size() > 0) { + return mounts; + } + + // determine HOME + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + static const auto HOME = Env::get("HOMEPATH", Env::get("USERPROFILE", Env::get("HOME"))); + #elif SOCKET_RUNTIME_PLATFORM_IOS + static const auto HOME = String(NSHomeDirectory().UTF8String); + #else + static const auto uid = getuid(); + static const auto pwuid = getpwuid(uid); + static const auto HOME = pwuid != nullptr + ? String(pwuid->pw_dir) + : Env::get("HOME", getcwd()); + #endif + + static const Map mappings = { + {"\\$HOST_HOME", HOME}, + {"~", HOME}, + + {"\\$HOST_CONTAINER", + #if SOCKET_RUNTIME_PLATFORM_IOS + [NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSUserDomainMask, YES) objectAtIndex: 0].UTF8String + #elif SOCKET_RUNTIME_PLATFORM_MACOS + // `homeDirectoryForCurrentUser` resolves to sandboxed container + // directory when in "sandbox" mode, otherwise the user's HOME directory + NSFileManager.defaultManager.homeDirectoryForCurrentUser.absoluteString.UTF8String + #elif SOCKET_RUNTIME_PLATFORM_LINUX || SOCKET_RUNTIME_PLATFORM_ANDROID + // TODO(@jwerle): figure out `$HOST_CONTAINER` for Linux/Android + getcwd() + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + // TODO(@jwerle): figure out `$HOST_CONTAINER` for Windows + getcwd() + #else + getcwd() + #endif + }, + + {"\\$HOST_PROCESS_WORKING_DIRECTORY", + #if SOCKET_RUNTIME_PLATFORM_APPLE + NSBundle.mainBundle.resourcePath.UTF8String + #else + getcwd() + #endif + } + }; + + static const auto wellKnownPaths = FileResource::getWellKnownPaths(); + + for (const auto& entry : userConfig) { + if (entry.first.starts_with("webview_navigator_mounts_")) { + auto key = replace(entry.first, "webview_navigator_mounts_", ""); + + if (key.starts_with("android") && !platform.android) continue; + if (key.starts_with("ios") && !platform.ios) continue; + if (key.starts_with("linux") && !platform.linux) continue; + if (key.starts_with("mac") && !platform.mac) continue; + if (key.starts_with("win") && !platform.win) continue; + + key = replace(key, "android_", ""); + key = replace(key, "ios_", ""); + key = replace(key, "linux_", ""); + key = replace(key, "mac_", ""); + key = replace(key, "win_", ""); + + String path = key; + + for (const auto& map : mappings) { + path = replace(path, map.first, map.second); + } + + const auto& value = entry.second; + mounts.insert_or_assign(path, value); + } + } + + return mounts; + } + + Path FileResource::getResourcesPath () { + static String value; + + if (value.size() > 0) { + return Path(value); + } + + #if SOCKET_RUNTIME_PLATFORM_MACOS + static const auto resourcePath = NSBundle.mainBundle.resourcePath; + value = resourcePath.UTF8String; + #elif SOCKET_RUNTIME_PLATFORM_IOS || SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR + static const auto resourcePath = NSBundle.mainBundle.resourcePath; + value = [resourcePath stringByAppendingPathComponent: @"ui"].UTF8String; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + #if SOCKET_RUNTIME_DESKTOP_EXTENSION + value = getcwd(); + #else + static const auto self = fs::canonical("/proc/self/exe"); + value = self.parent_path().string(); + #endif + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + static wchar_t filename[MAX_PATH]; + GetModuleFileNameW(NULL, filename, MAX_PATH); + const auto self = Path(filename).remove_filename(); + value = self.string(); + size_t offset = 0; + // escape + while ((offset = value.find('\\', offset)) != String::npos) { + value.replace(offset, 1, "\\\\"); + offset += 2; + } + #else + value = getcwd_state_value(); + #endif + + if (value.size() > 0) { + #if !SOCKET_RUNTIME_PLATFORM_WINDOWS + std::replace( + value.begin(), + value.end(), + '\\', + '/' + ); + #endif + } + + if (value.ends_with("/")) { + value = value.substr(0, value.size() - 1); + } + + return Path(value); + } + + Path FileResource::getResourcePath (const Path& resourcePath) { + return FileResource::getResourcePath(resourcePath.string()); + } + + Path FileResource::getResourcePath (const String& resourcePath) { + const auto resourcesPath = FileResource::getResourcesPath(); + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + if (resourcePath.starts_with("\\")) { + return Path(resourcesPath.string() + resourcePath); + } + + return Path(resourcesPath.string() + "\\" + resourcePath); + #else + if (resourcePath.starts_with("/")) { + return Path(resourcesPath.string() + resourcePath); + } + + return Path(resourcesPath.string() + "/" + resourcePath); + #endif + } + + void FileResource::WellKnownPaths::setDefaults (const WellKnownPaths& paths) { + defaultWellKnownPaths = paths; + } + + const FileResource::WellKnownPaths& FileResource::getWellKnownPaths () { + static const auto paths = WellKnownPaths {}; + return paths; + } + + FileResource::WellKnownPaths::WellKnownPaths () { + static auto userConfig = getUserConfig(); + static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + + // initialize default values + this->resources = defaultWellKnownPaths.resources; + this->downloads = defaultWellKnownPaths.downloads; + this->documents = defaultWellKnownPaths.documents; + this->pictures = defaultWellKnownPaths.pictures; + this->desktop = defaultWellKnownPaths.desktop; + this->videos = defaultWellKnownPaths.videos; + this->music = defaultWellKnownPaths.music; + this->config = defaultWellKnownPaths.config; + this->home = defaultWellKnownPaths.home; + this->data = defaultWellKnownPaths.data; + this->log = defaultWellKnownPaths.log; + this->tmp = defaultWellKnownPaths.tmp; + + this->resources = FileResource::getResourcesPath(); + this->tmp = fs::temp_directory_path(); + #if SOCKET_RUNTIME_PLATFORM_APPLE + static const auto uid = getuid(); + static const auto pwuid = getpwuid(uid); + static const auto HOME = pwuid != nullptr + ? String(pwuid->pw_dir) + : Env::get("HOME", getcwd()); + + static const auto fileManager = NSFileManager.defaultManager; + + #define DIRECTORY_PATH_FROM_FILE_MANAGER(type) ( \ + String([fileManager \ + URLForDirectory: type \ + inDomain: NSUserDomainMask \ + appropriateForURL: nil \ + create: NO \ + error: nil \ + ].path.UTF8String) \ + ) + + // overload with main bundle resources path for macos/ios + this->downloads = Path(DIRECTORY_PATH_FROM_FILE_MANAGER(NSDownloadsDirectory)); + this->documents = Path(DIRECTORY_PATH_FROM_FILE_MANAGER(NSDocumentDirectory)); + this->pictures = Path(DIRECTORY_PATH_FROM_FILE_MANAGER(NSPicturesDirectory)); + this->desktop = Path(DIRECTORY_PATH_FROM_FILE_MANAGER(NSDesktopDirectory)); + this->videos = Path(DIRECTORY_PATH_FROM_FILE_MANAGER(NSMoviesDirectory)); + this->config = Path(HOME + "/Library/Application Support/" + bundleIdentifier); + this->media = Path(DIRECTORY_PATH_FROM_FILE_MANAGER(NSSharedPublicDirectory)); + this->music = Path(DIRECTORY_PATH_FROM_FILE_MANAGER(NSMusicDirectory)); + this->home = Path(String(NSHomeDirectory().UTF8String)); + this->data = Path(HOME + "/Library/Application Support/" + bundleIdentifier); + this->log = Path(HOME + "/Library/Logs/" + bundleIdentifier); + this->tmp = Path(String(NSTemporaryDirectory().UTF8String)); + + #undef DIRECTORY_PATH_FROM_FILE_MANAGER + + #elif SOCKET_RUNTIME_PLATFORM_LINUX + static const auto uid = getuid(); + static const auto pwuid = getpwuid(uid); + static const auto HOME = pwuid != nullptr + ? String(pwuid->pw_dir) + : Env::get("HOME", getcwd()); + + static const auto XDG_DOCUMENTS_DIR = Env::get("XDG_DOCUMENTS_DIR"); + static const auto XDG_DOWNLOAD_DIR = Env::get("XDG_DOWNLOAD_DIR"); + static const auto XDG_PICTURES_DIR = Env::get("XDG_PICTURES_DIR"); + static const auto XDG_DESKTOP_DIR = Env::get("XDG_DESKTOP_DIR"); + static const auto XDG_VIDEOS_DIR = Env::get("XDG_VIDEOS_DIR"); + static const auto XDG_MUSIC_DIR = Env::get("XDG_MUSIC_DIR"); + static const auto XDG_PUBLICSHARE_DIR = Env::get("XDG_PUBLICSHARE_DIR"); + + static const auto XDG_CONFIG_HOME = Env::get("XDG_CONFIG_HOME", HOME + "/.config"); + static const auto XDG_DATA_HOME = Env::get("XDG_DATA_HOME", HOME + "/.local/share"); + + if (XDG_DOCUMENTS_DIR.size() > 0) { + this->documents = Path(XDG_DOCUMENTS_DIR); + } else { + this->documents = Path(HOME) / "Documents"; + } + + if (XDG_DOWNLOAD_DIR.size() > 0) { + this->downloads = Path(XDG_DOWNLOAD_DIR); + } else { + this->downloads = Path(HOME) / "Downloads"; + } + + if (XDG_DESKTOP_DIR.size() > 0) { + this->desktop = Path(XDG_DESKTOP_DIR); + } else { + this->desktop = Path(HOME) / "Desktop"; + } + + if (XDG_PICTURES_DIR.size() > 0) { + this->pictures = Path(XDG_PICTURES_DIR); + } else if (fs::exists(Path(HOME) / "Images")) { + this->pictures = Path(HOME) / "Images"; + } else if (fs::exists(Path(HOME) / "Photos")) { + this->pictures = Path(HOME) / "Photos"; + } else { + this->pictures = Path(HOME) / "Pictures"; + } + + if (XDG_VIDEOS_DIR.size() > 0) { + this->videos = Path(XDG_VIDEOS_DIR); + } else { + this->videos = Path(HOME) / "Videos"; + } + + if (XDG_MUSIC_DIR.size() > 0) { + this->music = Path(XDG_MUSIC_DIR); + } else { + this->music = Path(HOME) / "Music"; + } + + if (XDG_PUBLICSHARE_DIR.size() > 0) { + this->media = Path(XDG_PUBLICSHARE_DIR); + } + + this->config = Path(XDG_CONFIG_HOME) / bundleIdentifier; + this->home = Path(HOME); + this->data = Path(XDG_DATA_HOME) / bundleIdentifier; + this->log = this->config; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + static const auto HOME = Env::get("HOMEPATH", Env::get("HOME")); + static const auto USERPROFILE = Env::get("USERPROFILE", HOME); + this->downloads = Path(USERPROFILE) / "Downloads"; + this->documents = Path(USERPROFILE) / "Documents"; + this->pictures = Path(USERPROFILE) / "Pictures"; + this->desktop = Path(USERPROFILE) / "Desktop"; + this->videos = Path(USERPROFILE) / "Videos"; + this->music = Path(USERPROFILE) / "Music"; + this->config = Path(Env::get("APPDATA")) / bundleIdentifier; + this->home = Path(USERPROFILE); + this->data = Path(Env::get("APPDATA")) / bundleIdentifier; + this->log = this->config; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto storage = FileResource::getExternalAndroidStorageDirectory(); + const auto cache = FileResource::getExternalAndroidCacheDirectory(); + this->resources = "socket://" + bundleIdentifier; + this->tmp = !cache.empty() ? cache : storage / "tmp"; + #endif + } + + JSON::Object FileResource::WellKnownPaths::json () const { + return JSON::Object::Entries { + {"resources", this->resources}, + {"downloads", this->downloads}, + {"documents", this->documents}, + {"pictures", this->pictures}, + {"desktop", this->desktop}, + {"videos", this->videos}, + {"config", this->config}, + {"media", this->media}, + {"music", this->music}, + {"home", this->home}, + {"data", this->data}, + {"log", this->log}, + {"tmp", this->tmp} + }; + } + + const Vector<Path> FileResource::WellKnownPaths::entries () const { + auto entries = Vector<Path>(); + entries.push_back(this->resources); + entries.push_back(this->downloads); + entries.push_back(this->documents); + entries.push_back(this->pictures); + entries.push_back(this->desktop); + entries.push_back(this->videos); + entries.push_back(this->music); + entries.push_back(this->media); + entries.push_back(this->config); + entries.push_back(this->home); + entries.push_back(this->data); + entries.push_back(this->log); + entries.push_back(this->tmp); + return entries; + } + +#if SOCKET_RUNTIME_PLATFORM_WINDOWS + const Path FileResource::getMicrosoftEdgeRuntimePath () { + // this is something like "C:\\Users\\jwerle\\AppData\\Local\\Microsoft\\Edge SxS\\Application\\123.0.2386.0" + static const auto EDGE_RUNTIME_DIRECTORY = trim(Env::get("SOCKET_EDGE_RUNTIME_DIRECTORY")); + + return Path( + EDGE_RUNTIME_DIRECTORY.size() > 0 && fs::exists(EDGE_RUNTIME_DIRECTORY) + ? EDGE_RUNTIME_DIRECTORY + : "" + ); + } +#endif + + FileResource::FileResource ( + const Path& resourcePath, + const Options& options + ) : + Resource("FileResource", resourcePath.string()) + { + this->url = URL(resourcePath.string()); + + if (url.scheme == "socket") { + const auto resourcesPath = FileResource::getResourcesPath(); + this->path = fs::absolute(resourcesPath / url.pathname); + #if SOCKET_RUNTIME_PLATFORM_ANDROID + this->path = Path(url.pathname); + this->name = getRelativeAndroidAssetManagerPath(this->path).string(); + #endif + } else if (url.scheme == "content" || url.scheme == "android.resource") { + this->path = resourcePath; + } else if (url.scheme == "file") { + this->path = Path(url.pathname); + } else { + this->path = fs::absolute(resourcePath); + this->url = URL("file://" + this->path.string()); + } + + this->options = options; + this->startAccessing(); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (this->isAndroidLocalAsset()) { + this->name = getRelativeAndroidAssetManagerPath(this->path).string(); + } + #endif + } + + FileResource::FileResource (const String& resourcePath, const Options& options) + : FileResource(Path(resourcePath), options) + {} + + FileResource::~FileResource () { + this->stopAccessing(); + } + + FileResource::FileResource (const FileResource& resource) + : Resource("FileResource", resource.name) + { + this->url = resource.url; + this->path = resource.path; + this->bytes = resource.bytes; + this->cache = resource.cache; + this->options = resource.options; + this->startAccessing(); + } + + FileResource::FileResource (FileResource&& resource) + : Resource("FileResource", resource.name) + { + this->url = resource.url; + this->path = resource.path; + this->bytes = resource.bytes; + this->cache = resource.cache; + this->options = resource.options; + this->accessing = resource.accessing.load(); + + resource.url = URL {}; + resource.bytes = nullptr; + resource.cache.size = 0; + resource.cache.bytes = nullptr; + resource.accessing = false; + + this->startAccessing(); + } + + FileResource& FileResource::operator= (const FileResource& resource) { + this->url = resource.url; + this->path = resource.path; + this->bytes = resource.bytes; + this->cache = resource.cache; + this->options = resource.options; + this->accessing = resource.accessing.load(); + + this->startAccessing(); + + return *this; + } + + FileResource& FileResource::operator= (FileResource&& resource) { + this->url = resource.url; + this->path = resource.path; + this->bytes = resource.bytes; + this->cache = resource.cache; + this->options = resource.options; + this->accessing = resource.accessing.load(); + + resource.url = URL {}; + resource.bytes = nullptr; + resource.cache.size = 0; + resource.cache.bytes = nullptr; + resource.accessing = false; + + this->startAccessing(); + + return *this; + } + + bool FileResource::startAccessing () { + static const auto resourcesPath = FileResource::getResourcesPath(); + + if (this->accessing) { + return false; + } + + if (caches.contains(this->path.string())) { + this->cache = caches.at(this->path.string()); + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->nsURL == nullptr) { + this->nsURL = [NSURL fileURLWithPath: @(this->path.string().c_str())]; + } + #endif + + if (FileResource::isMountedPath(this->path)) { + this->accessing = true; + return true; + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (!this->path.string().starts_with(resourcesPath.string())) { + if (![this->nsURL startAccessingSecurityScopedResource]) { + this->nsURL = nullptr; + return false; + } + } + #endif + + this->accessing = true; + return true; + } + + bool FileResource::stopAccessing () { + if (!this->accessing) { + return false; + } + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->nsURL != nullptr) { + [this->nsURL stopAccessingSecurityScopedResource]; + } + #endif + this->accessing = false; + return true; + } + + bool FileResource::exists () const noexcept { + if (!this->accessing) { + return false; + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + static auto fileManager = [[NSFileManager alloc] init]; + return [fileManager + fileExistsAtPath: @(this->path.string().c_str()) + isDirectory: NULL + ]; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + if (sharedAndroidAssetManager) { + const auto assetPath = getRelativeAndroidAssetManagerPath(this->path); + const auto asset = AAssetManager_open( + sharedAndroidAssetManager, + assetPath.c_str(), + AASSET_MODE_BUFFER + ); + + if (asset) { + AAsset_close(asset); + return true; + } + } + + return fs::exists(this->path); + #else + return fs::exists(this->path); + #endif + } + + int FileResource::access (int mode) const noexcept { + if (this->accessing) { + if (mode == ::access(this->path.string().c_str(), mode)) { + return mode; + } + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (this->isAndroidLocalAsset() || this->isAndroidContent()) { + if (mode == F_OK || mode == R_OK) { + return mode; + } + } + #endif + } + + return -1; // `EPERM` + } + + const String FileResource::mimeType () const noexcept { + const auto extension = this->path.extension().string(); + if (extension.size() > 0) { + // try in memory simle mime db + for (const auto& entry : FileResource::mimeTypes) { + const auto& mimeType = entry.first; + const auto& extensions = entry.second; + if (extensions.contains(extension)) { + return mimeType; + } + } + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (extension.size() > 0) { + @try { + const auto types = [UTType + typesWithTag: @(extension.c_str()) + tagClass: UTTagClassFilenameExtension + conformingToType: nullptr + ]; + + if (types.count > 0 && types.firstObject.preferredMIMEType != nullptr) { + return types.firstObject.preferredMIMEType.UTF8String; + } + } @catch (::id) {} + } + #elif SOCKET_RUNTIME_PLATFORM_LINUX + gboolean typeGuessResultUncertain = false; + gchar* type = nullptr; + + type = g_content_type_guess(this->path.string().c_str(), nullptr, 0, &typeGuessResultUncertain); + + if (!type || typeGuessResultUncertain) { + const auto bytes = this->read(); + const auto size = this->size(); + const auto nextType = g_content_type_guess( + reinterpret_cast<const gchar*>(this->path.string().c_str()), + reinterpret_cast<const guchar*>(bytes), + (gsize) size, + &typeGuessResultUncertain + ); + + if (nextType) { + if (type) { + g_free(type); + } + + type = nextType; + } + } + + if (type == nullptr) { + return ""; + } + + const auto mimeType = String(type); + + if (type) { + g_free(type); + } + + return mimeType; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + LPWSTR mimeData; + const auto bytes = this->read(); + const auto size = this->size(); + const auto result = FindMimeFromData( + nullptr, // (LPBC) ignored (unsused) + convertStringToWString(path).c_str(), // filename + const_cast<void*>(reinterpret_cast<const void*>(bytes)), // detected buffer data + (DWORD) size, // detected buffer size + nullptr, // mime suggestion + 0, // flags (unsused) + &mimeData, // output + 0 // reserved (unsused) + ); + + if (result == S_OK) { + return convertWStringToString(WString(mimeData)); + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + if (extension.size() > 1) { + const auto value = Android::MimeTypeMap::sharedMimeTypeMap()->getMimeTypeFromExtension( + extension.starts_with(".") ? extension.substr(1) : extension + ); + + if (value.size() > 0) { + return value; + } + } + + if (this->options.core && this->url.scheme == "content") { + auto core = this->options.core; + return core->platform.contentResolver.getContentMimeType(this->url.str()); + } + #endif + + return ""; + } + + size_t FileResource::size () const noexcept { + return this->cache.size; + } + + size_t FileResource::size (bool cached) noexcept { + if (cached && this->cache.size > 0) { + return this->cache.size; + } + + if (!this->accessing) { + return -1; + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->nsURL != nullptr) { + NSNumber* size = nullptr; + NSError* error = nullptr; + [this->nsURL getResourceValue: &size + forKey: NSURLFileSizeKey + error: &error + ]; + + if (error) { + return -error.code; + } + + this->cache.size = size.longLongValue; + } + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + LARGE_INTEGER fileSize; + auto handle = CreateFile( + convertWStringToString(this->path.string()).c_str(), + GENERIC_READ, // access + FILE_SHARE_READ, // share mode + nullptr, // security attribues (unused) + OPEN_EXISTING, // creation disposition + 0, // flags and attribues (unused) + nullptr // templte file (unused) + ); + + if (handle) { + auto result = GetFileSizeEx(handle, &fileSize); + CloseHandle(handle); + this->cache.size = fileSize.QuadPart; + } else { + return -2; + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + bool success = false; + if (sharedAndroidAssetManager) { + if (this->isAndroidLocalAsset()) { + const auto assetPath = getRelativeAndroidAssetManagerPath(this->path); + const auto asset = AAssetManager_open( + sharedAndroidAssetManager, + assetPath.c_str(), + AASSET_MODE_BUFFER + ); + + if (asset) { + this->cache.size = AAsset_getLength(asset); + AAsset_close(asset); + } + } + + if (!success) { + if (fs::exists(this->path)) { + this->cache.size = fs::file_size(this->path); + } + } + } else if (this->url.scheme == "content" || this->url.scheme == "android.resource") { + auto core = this->options.core; + if (core != nullptr) { + off_t offset = 0; + off_t length = 0; + auto fileDescriptor = core->platform.contentResolver.openFileDescriptor ( + this->url.str(), + &offset, + &length + ); + + core->platform.contentResolver.closeFileDescriptor(fileDescriptor); + return length; + } + } + #else + if (fs::exists(this->path)) { + this->cache.size = fs::file_size(this->path); + } + #endif + + return this->cache.size; + } + + const char* FileResource::read () const { + return this->cache.bytes.get(); + } + + // caller takes ownership of returned pointer + const char* FileResource::read (bool cached) { + if (!this->accessing || !this->exists()) { + return nullptr; + } + + if (cached && this->cache.bytes != nullptr) { + return this->cache.bytes.get(); + } + + if (this->bytes != nullptr) { + this->bytes = nullptr; + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->nsURL == nullptr) { + return nullptr; + } + + const auto data = [NSData dataWithContentsOfURL: this->nsURL]; + if (data.length > 0) { + this->bytes.reset(new char[data.length]{0}); + memcpy(this->bytes.get(), data.bytes, data.length); + } + #elif SOCKET_RUNTIME_PLATFORM_LINUX + auto span = this->tracer.span("read"); + GError* error = nullptr; + char* contents = nullptr; + gsize size = 0; + if (g_file_get_contents(this->path.string().c_str(), &contents, &size, &error)) { + if (size > 0) { + this->bytes.reset(new char[size]{0}); + memcpy(this->bytes.get(), contents, size); + } + } + + if (contents) { + g_free(contents); + } + span->end(); + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + auto handle = CreateFile( + convertWStringToString(this->path.string()).c_str(), + GENERIC_READ, // access + FILE_SHARE_READ, // share mode + nullptr, // security attribues (unused) + OPEN_EXISTING, // creation disposition + 0, // flags and attribues (unused) + nullptr // templte file (unused) + ); + + if (handle) { + const auto size = this->size(); + auto bytes = new char[size]{0}; + auto result = ReadFile( + handle, // File handle + reinterpret_cast<void*>(bytes), + (DWORD) size, // output buffer size + nullptr, // bytes read (unused) + nullptr // ignored (unused) + ); + + if (result) { + this->bytes.reset(bytes); + } else { + delete [] bytes; + } + + CloseHandle(handle); + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + bool success = false; + if (sharedAndroidAssetManager) { + const auto assetPath = getRelativeAndroidAssetManagerPath(this->path); + const auto asset = AAssetManager_open( + sharedAndroidAssetManager, + assetPath.c_str(), + AASSET_MODE_BUFFER + ); + + if (asset) { + auto size = AAsset_getLength(asset); + if (size) { + const auto buffer = AAsset_getBuffer(asset); + if (buffer) { + auto bytes = new char[size]{0}; + memcpy(bytes, buffer, size); + this->bytes.reset(bytes); + this->cache.size = size; + success = true; + } + } + + AAsset_close(asset); + } + } + + if (!success) { + auto stream = std::ifstream(this->path); + auto buffer = std::istreambuf_iterator<char>(stream); + auto size = fs::file_size(this->path); + auto end = std::istreambuf_iterator<char>(); + + auto bytes = new char[size]{0}; + String content; + + content.assign(buffer, end); + memcpy(bytes, content.data(), size); + + this->bytes.reset(bytes); + this->cache.size = size; + } + #endif + + this->cache.bytes = this->bytes; + if (this->options.cache) { + caches.insert_or_assign(this->path.string(), this->cache); + } + return this->cache.bytes.get(); + } + + const String FileResource::str (bool cached) { + if (!this->accessing || !this->exists()) { + return ""; + } + + const auto size = this->size(cached); + const auto bytes = this->read(cached); + + if (bytes != nullptr && size > 0) { + return String(bytes, size); + } + + return ""; + } + + FileResource::ReadStream FileResource::stream (const ReadStream::Options& options) { + return ReadStream(ReadStream::Options(this->path, options.highWaterMark, this->size())); + } + + FileResource::ReadStream::ReadStream (const Options& options) + : options(options) + {} + + FileResource::ReadStream::~ReadStream () { + #if SOCKET_RUNTIME_PLATFORM_APPLE + #elif SOCKET_RUNTIME_PLATFORM_LINUX + if (this->file != nullptr) { + g_object_unref(this->file); + this->file = nullptr; + } + + if (this->stream != nullptr) { + g_input_stream_close( + reinterpret_cast<GInputStream*>(this->stream), + nullptr, + nullptr + ); + g_object_unref(this->stream); + this->stream = nullptr; + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + #endif + } + + FileResource::ReadStream::ReadStream ( + const ReadStream& stream + ) : options(stream.options), + offset(stream.offset.load()), + ended(stream.ended.load()) + { + #if SOCKET_RUNTIME_PLATFORM_APPLE + #elif SOCKET_RUNTIME_PLATFORM_LINUX + this->file = stream.file; + this->stream = stream.stream; + + if (this->file) { + g_object_ref(this->file); + } + + if (this->stream) { + g_object_ref(this->stream); + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + #endif + } + + FileResource::ReadStream::ReadStream (ReadStream&& stream) + : options(stream.options), + offset(stream.offset.load()), + ended(stream.ended.load()) + { + #if SOCKET_RUNTIME_PLATFORM_APPLE + #elif SOCKET_RUNTIME_PLATFORM_LINUX + this->file = stream.file; + this->stream = stream.stream; + if (stream.file) { + stream.file = nullptr; + } + + if (stream.stream) { + stream.stream = nullptr; + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + #endif + } + + FileResource::ReadStream& FileResource::ReadStream::operator = ( + const ReadStream& stream + ) { + this->options.highWaterMark = stream.options.highWaterMark; + this->options.resourcePath = stream.options.resourcePath; + this->options.size = stream.options.size; + this->offset = stream.offset.load(); + this->ended = stream.ended.load(); + + #if SOCKET_RUNTIME_PLATFORM_APPLE + #elif SOCKET_RUNTIME_PLATFORM_LINUX + this->file = stream.file; + this->stream = stream.stream; + + if (this->file) { + g_object_ref(this->file); + } + + if (this->stream) { + g_object_ref(this->stream); + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + #endif + return *this; + } + + FileResource::ReadStream& FileResource::ReadStream::operator = ( + ReadStream&& stream + ) { + this->options.highWaterMark = stream.options.highWaterMark; + this->options.resourcePath = stream.options.resourcePath; + this->options.size = stream.options.size; + this->offset = stream.offset.load(); + this->ended = stream.ended.load(); + + #if SOCKET_RUNTIME_PLATFORM_APPLE + #elif SOCKET_RUNTIME_PLATFORM_LINUX + this->file = stream.file; + this->stream = stream.stream; + if (stream.file) { + stream.file = nullptr; + } + + if (stream.stream) { + stream.stream = nullptr; + } + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + #endif + return *this; + } + + const FileResource::ReadStream::Buffer FileResource::ReadStream::read (off_t offset, size_t highWaterMark) { + if (offset == -1) { + offset = this->offset; + } + + if (highWaterMark == -1) { + highWaterMark = this->options.highWaterMark; + } + + const auto remaining = this->remaining(offset); + const auto size = highWaterMark > remaining ? remaining : highWaterMark; + auto buffer = Buffer(size); + + if (buffer.size > 0 && buffer.bytes != nullptr) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->data == nullptr) { + auto url = [NSURL fileURLWithPath: @(this->options.resourcePath.string().c_str())]; + this->data = [NSData dataWithContentsOfURL: url]; + @try { + [this->data + getBytes: buffer.bytes.get() + range: NSMakeRange(offset, size) + ]; + } @catch (NSException* error) { + this->error = [NSError + errorWithDomain: error.name + code: 0 + userInfo: @{ + NSUnderlyingErrorKey: error, + NSDebugDescriptionErrorKey: error.userInfo ?: @{}, + NSLocalizedFailureReasonErrorKey: (error.reason ?: @"???") + }]; + } + } + #elif SOCKET_RUNTIME_PLATFORM_LINUX + if (this->file == nullptr) { + this->file = g_file_new_for_path(this->options.resourcePath.c_str()); + } + + if (this->stream == nullptr) { + this->stream = g_file_read(this->file, nullptr, nullptr); + } + + if (size == 0 || highWaterMark == 0) { + return buffer; + } + + if (offset > this->offset) { + g_input_stream_skip( + reinterpret_cast<GInputStream*>(this->stream), + offset - this->offset, + nullptr, + nullptr + ); + + this->offset = offset; + } + + buffer.size = g_input_stream_read( + reinterpret_cast<GInputStream*>(this->stream), + buffer.bytes.get(), + size, + nullptr, + &this->error + ); + + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + #endif + } + + if (this->error) { + buffer.size = 0; + debug( + "FileResource::ReadStream: read error: %s", + #if SOCKET_RUNTIME_PLATFORM_APPLE + error.localizedDescription.UTF8String + #elif SOCKET_RUNTIME_PLATFORM_LINUX + error->message + #else + "An unknown error occurred" + #endif + ); + } + + if (buffer.size <= 0) { + buffer.bytes = nullptr; + buffer.size = 0; + this->ended = true; + } else { + this->offset += buffer.size; + this->ended = this->offset >= this->options.size; + } + + return buffer; + } + + size_t FileResource::ReadStream::remaining (off_t offset) const { + const auto size = this->options.size; + if (offset > -1) { + return size - offset; + } + + return size - this->offset; + } + + FileResource::ReadStream::Buffer::Buffer (size_t size) + : bytes(std::make_shared<char[]>(size)), + size(size) + { + memset(this->bytes.get(), 0, size); + } + + FileResource::ReadStream::Buffer::Buffer (const Options& options) + : bytes(std::make_shared<char[]>(options.highWaterMark)), + size(0) + { + memset(this->bytes.get(), 0, options.highWaterMark); + } + + FileResource::ReadStream::Buffer::Buffer (const Buffer& buffer) { + this->size = buffer.size.load(); + this->bytes = buffer.bytes; + } + + FileResource::ReadStream::Buffer::Buffer (Buffer&& buffer) { + this->size = buffer.size.load(); + this->bytes = buffer.bytes; + buffer.size = 0; + buffer.bytes = nullptr; + } + + FileResource::ReadStream::Buffer& FileResource::ReadStream::Buffer::operator = ( + const Buffer& buffer + ) { + this->size = buffer.size.load(); + this->bytes = buffer.bytes; + return *this; + } + + FileResource::ReadStream::Buffer& FileResource::ReadStream::Buffer::operator = ( + Buffer&& buffer + ) { + this->size = buffer.size.load(); + this->bytes = buffer.bytes; + buffer.size = 0; + buffer.bytes = nullptr; + return *this; + } + + bool FileResource::ReadStream::Buffer::isEmpty () const { + return this->size == 0 || this->bytes == nullptr; + } + +#if SOCKET_RUNTIME_PLATFORM_ANDROID + bool FileResource::isAndroidLocalAsset () const noexcept { + if (sharedAndroidAssetManager) { + const auto assetPath = getRelativeAndroidAssetManagerPath(this->path); + const auto asset = AAssetManager_open( + sharedAndroidAssetManager, + assetPath.c_str(), + AASSET_MODE_BUFFER + ); + + if (asset) { + AAsset_close(asset); + return true; + } + } + + return false; + } + + bool FileResource::isAndroidContent () const noexcept { + const auto core = this->options.core; + + if (core != nullptr) { + const auto uri = this->path.string(); + if (core->platform.contentResolver.isContentURI(uri)) { + const auto pathname = core->platform.contentResolver.getPathnameFromURI(uri); + return pathname.size() > 0; + } + } + return false; + } +#endif +} diff --git a/src/core/resource.hh b/src/core/resource.hh new file mode 100644 index 0000000000..525855a287 --- /dev/null +++ b/src/core/resource.hh @@ -0,0 +1,195 @@ +#ifndef SOCKET_RUNTIME_CORE_RESOURCE_H +#define SOCKET_RUNTIME_CORE_RESOURCE_H + +#include "../platform/platform.hh" + +#include "trace.hh" +#include "url.hh" + +namespace SSC { + // forward + class Core; + + class Resource { + public: + Atomic<bool> accessing = false; + String name; + String type; + Tracer tracer; + Resource (const String& type, const String& name); + bool hasAccess () const noexcept; + virtual bool startAccessing () = 0; + virtual bool stopAccessing () = 0; + }; + + class FileResource : public Resource { + public: + struct Cache { + SharedPointer<char[]> bytes = nullptr; + size_t size = 0; + }; + + struct WellKnownPaths { + Path resources; + Path downloads; + Path documents; + Path pictures; + Path desktop; + Path videos; + Path config; + Path music; + Path media; + Path home; + Path data; + Path log; + Path tmp; + + static void setDefaults (const WellKnownPaths& paths); + + WellKnownPaths (); + const Vector<Path> entries () const; + JSON::Object json () const; + }; + + class ReadStream { + public: + #if SOCKET_RUNTIME_PLATFORM_APPLE + using Error = NSError; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + using Error = GError; + #else + using Error = char*; + #endif + + struct Options { + Path resourcePath; + size_t highWaterMark = 64 * 1024; + size_t size = 0; + + Options ( + const Path& resourcePath = Path(""), + size_t highWaterMark = 64 * 1024, + size_t size = 0 + ) + : highWaterMark(highWaterMark), + resourcePath(resourcePath), + size(size) + {} + }; + + struct Buffer { + Atomic<size_t> size = 0; + SharedPointer<char[]> bytes = nullptr; + Buffer (size_t size); + Buffer (const Options& options); + Buffer (const Buffer& buffer); + Buffer (Buffer&& buffer); + Buffer& operator= (const Buffer&); + Buffer& operator= (Buffer&&); + bool isEmpty () const; + }; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + NSData* data = nullptr; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + GFileInputStream* stream = nullptr; + GFile* file = nullptr; + #endif + + Options options; + Error* error = nullptr; + Atomic<off_t> offset = 0; + Atomic<bool> ended = false; + + ReadStream (const Options& options); + ~ReadStream (); + ReadStream (const ReadStream&); + ReadStream (ReadStream&&); + ReadStream& operator= (const ReadStream&); + ReadStream& operator= (ReadStream&&); + + const Buffer read (off_t offset = -1, size_t highWaterMark = -1); + size_t remaining (off_t offset = -1) const; + }; + + struct Options { + bool cache; + Core* core; + }; + + Cache cache; + Options options; + SharedPointer<char[]> bytes = nullptr; + + static std::map<String, Set<String>> mimeTypes; + static Path getResourcesPath (); + static Path getResourcePath (const Path& resourcePath); + static Path getResourcePath (const String& resourcePath); + static bool isFile (const String& resourcePath); + static bool isFile (const Path& resourcePath); + static bool isDirectory (const String& resourcePath); + static bool isDirectory (const Path& resourcePath); + static bool isMountedPath (const Path& path); + static const WellKnownPaths& getWellKnownPaths (); + static const Map getMountedPaths (); + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + static const Path getMicrosoftEdgeRuntimePath (); + #endif + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + static void setSharedAndroidAssetManager (Android::AssetManager*); + static Android::AssetManager* getSharedAndroidAssetManager (); + + static void setExternalAndroidStorageDirectory (const Path&); + static Path getExternalAndroidStorageDirectory (); + + static void setExternalAndroidFilesDirectory (const Path&); + static Path getExternalAndroidFilesDirectory (); + + static void setExternalAndroidCacheDirectory (const Path&); + static Path getExternalAndroidCacheDirectory (); + #endif + + Path path; + URL url; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + NSURL* nsURL = nullptr; + #endif + + FileResource ( + const Path& resourcePath, + const Options& options = {0} + ); + + FileResource ( + const String& resourcePath, + const Options& options = {0} + ); + + ~FileResource (); + FileResource (const FileResource&); + FileResource (FileResource&&); + FileResource& operator= (const FileResource&); + FileResource& operator= (FileResource&&); + + bool startAccessing (); + bool stopAccessing (); + bool exists () const noexcept; + int access (int mode = F_OK) const noexcept; + const String mimeType () const noexcept; + size_t size (bool cached = false) noexcept; + size_t size () const noexcept; + const char* read () const; + const char* read (bool cached = false); + const String str (bool cached = false); + ReadStream stream (const ReadStream::Options& options = {}); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + bool isAndroidLocalAsset () const noexcept; + bool isAndroidContent () const noexcept; + #endif + }; +} +#endif diff --git a/src/core/peer.cc b/src/core/socket.cc similarity index 58% rename from src/core/peer.cc rename to src/core/socket.cc index ace0ca9f05..f9357fb0fd 100644 --- a/src/core/peer.cc +++ b/src/core/socket.cc @@ -1,84 +1,9 @@ +#include "modules/udp.hh" +#include "socket.hh" #include "core.hh" +#include "ip.hh" namespace SSC { - void Core::resumeAllPeers () { - dispatchEventLoop([=, this]() { - for (auto const &tuple : this->peers) { - auto peer = tuple.second; - if (peer != nullptr && (peer->isBound() || peer->isConnected())) { - peer->resume(); - } - } - }); - } - - void Core::pauseAllPeers () { - dispatchEventLoop([=, this]() { - for (auto const &tuple : this->peers) { - auto peer = tuple.second; - if (peer != nullptr && (peer->isBound() || peer->isConnected())) { - peer->pause(); - } - } - }); - } - - bool Core::hasPeer (uint64_t peerId) { - Lock lock(this->peersMutex); - return this->peers.find(peerId) != this->peers.end(); - } - - void Core::removePeer (uint64_t peerId) { - return this->removePeer(peerId, false); - } - - void Core::removePeer (uint64_t peerId, bool autoClose) { - if (this->hasPeer(peerId)) { - if (autoClose) { - auto peer = this->getPeer(peerId); - if (peer != nullptr) { - peer->close(); - } - } - - Lock lock(this->peersMutex); - this->peers.erase(peerId); - } - } - - Peer* Core::getPeer (uint64_t peerId) { - if (!this->hasPeer(peerId)) return nullptr; - Lock lock(this->peersMutex); - return this->peers.at(peerId); - } - - Peer* Core::createPeer (peer_type_t peerType, uint64_t peerId) { - return this->createPeer(peerType, peerId, false); - } - - Peer* Core::createPeer ( - peer_type_t peerType, - uint64_t peerId, - bool isEphemeral - ) { - if (this->hasPeer(peerId)) { - auto peer = this->getPeer(peerId); - if (peer != nullptr) { - if (isEphemeral) { - Lock lock(peer->mutex); - peer->flags = (peer_flag_t) (peer->flags | PEER_FLAG_EPHEMERAL); - } - } - - return peer; - } - - auto peer = new Peer(this, peerType, peerId, isEphemeral); - Lock lock(this->peersMutex); - this->peers[peer->id] = peer; - return peer; - } - int LocalPeerInfo::getsockname (uv_udp_t *socket, struct sockaddr *addr) { int namelen = sizeof(struct sockaddr_storage); return uv_udp_getsockname(socket, addr, &namelen); @@ -116,11 +41,11 @@ namespace SSC { void LocalPeerInfo::init (const struct sockaddr_storage *addr) { if (addr->ss_family == AF_INET) { this->family = "IPv4"; - this->address = addrToIPv4((struct sockaddr_in*) addr); + this->address = IP::addrToIPv4((struct sockaddr_in*) addr); this->port = (int) htons(((struct sockaddr_in*) addr)->sin_port); } else if (addr->ss_family == AF_INET6) { this->family = "IPv6"; - this->address = addrToIPv6((struct sockaddr_in6*) addr); + this->address = IP::addrToIPv6((struct sockaddr_in6*) addr); this->port = (int) htons(((struct sockaddr_in6*) addr)->sin6_port); } } @@ -162,18 +87,18 @@ namespace SSC { void RemotePeerInfo::init (const struct sockaddr_storage *addr) { if (addr->ss_family == AF_INET) { this->family = "IPv4"; - this->address = addrToIPv4((struct sockaddr_in*) addr); + this->address = IP::addrToIPv4((struct sockaddr_in*) addr); this->port = (int) htons(((struct sockaddr_in*) addr)->sin_port); } else if (addr->ss_family == AF_INET6) { this->family = "IPv6"; - this->address = addrToIPv6((struct sockaddr_in6*) addr); + this->address = IP::addrToIPv6((struct sockaddr_in6*) addr); this->port = (int) htons(((struct sockaddr_in6*) addr)->sin6_port); } } - Peer::Peer ( + Socket::Socket ( Core *core, - peer_type_t peerType, + socket_type_t peerType, uint64_t peerId, bool isEphemeral ) { @@ -182,29 +107,27 @@ namespace SSC { this->core = core; if (isEphemeral) { - this->flags = (peer_flag_t) (this->flags | PEER_FLAG_EPHEMERAL); + this->flags = (socket_flag_t) (this->flags | SOCKET_FLAG_EPHEMERAL); } this->init(); } - Peer::~Peer () { - this->core->removePeer(this->id, true); // auto close - } + Socket::~Socket () {} - int Peer::init () { + int Socket::init () { Lock lock(this->mutex); auto loop = this->core->getEventLoop(); int err = 0; memset(&this->handle, 0, sizeof(this->handle)); - if (this->type == PEER_TYPE_UDP) { + if (this->type == SOCKET_TYPE_UDP) { if ((err = uv_udp_init(loop, (uv_udp_t *) &this->handle))) { return err; } this->handle.udp.data = (void *) this; - } else if (this->type == PEER_TYPE_TCP) { + } else if (this->type == SOCKET_TYPE_TCP) { if ((err = uv_tcp_init(loop, (uv_tcp_t *) &this->handle))) { return err; } @@ -214,102 +137,102 @@ namespace SSC { return err; } - int Peer::initRemotePeerInfo () { + int Socket::initRemotePeerInfo () { Lock lock(this->mutex); - if (this->type == PEER_TYPE_UDP) { + if (this->type == SOCKET_TYPE_UDP) { this->remote.init((uv_udp_t *) &this->handle); - } else if (this->type == PEER_TYPE_TCP) { + } else if (this->type == SOCKET_TYPE_TCP) { this->remote.init((uv_tcp_t *) &this->handle); } return this->remote.err; } - int Peer::initLocalPeerInfo () { + int Socket::initLocalPeerInfo () { Lock lock(this->mutex); - if (this->type == PEER_TYPE_UDP) { + if (this->type == SOCKET_TYPE_UDP) { this->local.init((uv_udp_t *) &this->handle); - } else if (this->type == PEER_TYPE_TCP) { + } else if (this->type == SOCKET_TYPE_TCP) { this->local.init((uv_tcp_t *) &this->handle); } return this->local.err; } - void Peer::addState (peer_state_t value) { + void Socket::addState (socket_state_t value) { Lock lock(this->mutex); - this->state = (peer_state_t) (this->state | value); + this->state = (socket_state_t) (this->state | value); } - void Peer::removeState (peer_state_t value) { + void Socket::removeState (socket_state_t value) { Lock lock(this->mutex); - this->state = (peer_state_t) (this->state & ~value); + this->state = (socket_state_t) (this->state & ~value); } - bool Peer::hasState (peer_state_t value) { + bool Socket::hasState (socket_state_t value) { Lock lock(this->mutex); return (value & this->state) == value; } - const RemotePeerInfo* Peer::getRemotePeerInfo () { + const RemotePeerInfo* Socket::getRemotePeerInfo () { Lock lock(this->mutex); return &this->remote; } - const LocalPeerInfo* Peer::getLocalPeerInfo () { + const LocalPeerInfo* Socket::getLocalPeerInfo () { Lock lock(this->mutex); return &this->local; } - bool Peer::isUDP () { + bool Socket::isUDP () { Lock lock(this->mutex); - return this->type == PEER_TYPE_UDP; + return this->type == SOCKET_TYPE_UDP; } - bool Peer::isTCP () { + bool Socket::isTCP () { Lock lock(this->mutex); - return this->type == PEER_TYPE_TCP; + return this->type == SOCKET_TYPE_TCP; } - bool Peer::isEphemeral () { + bool Socket::isEphemeral () { Lock lock(this->mutex); - return (PEER_FLAG_EPHEMERAL & this->flags) == PEER_FLAG_EPHEMERAL; + return (SOCKET_FLAG_EPHEMERAL & this->flags) == SOCKET_FLAG_EPHEMERAL; } - bool Peer::isBound () { + bool Socket::isBound () { return ( - (this->isUDP() && this->hasState(PEER_STATE_UDP_BOUND)) || - (this->isTCP() && this->hasState(PEER_STATE_TCP_BOUND)) + (this->isUDP() && this->hasState(SOCKET_STATE_UDP_BOUND)) || + (this->isTCP() && this->hasState(SOCKET_STATE_TCP_BOUND)) ); } - bool Peer::isActive () { + bool Socket::isActive () { Lock lock(this->mutex); return uv_is_active((const uv_handle_t *) &this->handle); } - bool Peer::isClosing () { + bool Socket::isClosing () { Lock lock(this->mutex); return uv_is_closing((const uv_handle_t *) &this->handle); } - bool Peer::isClosed () { - return this->hasState(PEER_STATE_CLOSED); + bool Socket::isClosed () { + return this->hasState(SOCKET_STATE_CLOSED); } - bool Peer::isConnected () { + bool Socket::isConnected () { return ( - (this->isUDP() && this->hasState(PEER_STATE_UDP_CONNECTED)) || - (this->isTCP() && this->hasState(PEER_STATE_TCP_CONNECTED)) + (this->isUDP() && this->hasState(SOCKET_STATE_UDP_CONNECTED)) || + (this->isTCP() && this->hasState(SOCKET_STATE_TCP_CONNECTED)) ); } - bool Peer::isPaused () { + bool Socket::isPaused () { return ( - (this->isUDP() && this->hasState(PEER_STATE_UDP_PAUSED)) || - (this->isTCP() && this->hasState(PEER_STATE_TCP_PAUSED)) + (this->isUDP() && this->hasState(SOCKET_STATE_UDP_PAUSED)) || + (this->isTCP() && this->hasState(SOCKET_STATE_TCP_PAUSED)) ); } - int Peer::bind () { + int Socket::bind () { auto info = this->getLocalPeerInfo(); if (info->err) { @@ -319,11 +242,11 @@ namespace SSC { return this->bind(info->address, info->port, this->options.udp.reuseAddr); } - int Peer::bind (const String address, int port) { + int Socket::bind (const String& address, int port) { return this->bind(address, port, false); } - int Peer::bind (const String address, int port, bool reuseAddr) { + int Socket::bind (const String& address, int port, bool reuseAddr) { Lock lock(this->mutex); auto sockaddr = (struct sockaddr*) &this->addr; int flags = 0; @@ -345,7 +268,7 @@ namespace SSC { return err; } - this->addState(PEER_STATE_UDP_BOUND); + this->addState(SOCKET_STATE_UDP_BOUND); } if (this->isTCP()) { @@ -355,7 +278,7 @@ namespace SSC { return this->initLocalPeerInfo(); } - int Peer::rebind () { + int Socket::rebind () { int err = 0; if (this->isUDP()) { @@ -380,7 +303,7 @@ namespace SSC { return err; } - int Peer::connect (const String address, int port) { + int Socket::connect (const String& address, int port) { Lock lock(this->mutex); auto sockaddr = (struct sockaddr*) &this->addr; int err = 0; @@ -394,13 +317,13 @@ namespace SSC { return err; } - this->addState(PEER_STATE_UDP_CONNECTED); + this->addState(SOCKET_STATE_UDP_CONNECTED); } return this->initRemotePeerInfo(); } - int Peer::disconnect () { + int Socket::disconnect () { int err = 0; if (this->isUDP()) { @@ -412,19 +335,19 @@ namespace SSC { return err; } - this->removeState(PEER_STATE_UDP_CONNECTED); + this->removeState(SOCKET_STATE_UDP_CONNECTED); } } return err; } - void Peer::send ( - char *buf, + void Socket::send ( + SharedPointer<char[]> bytes, size_t size, int port, - const String address, - Peer::RequestContext::Callback cb + const String& address, + const Socket::RequestContext::Callback& callback ) { Lock lock(this->mutex); int err = 0; @@ -436,25 +359,24 @@ namespace SSC { err = uv_ip4_addr((char *) address.c_str(), port, &this->addr); if (err) { - return cb(err, Post{}); + return callback(err, Post{}); } } - auto buffer = uv_buf_init(buf, (int) size); - auto ctx = new Peer::RequestContext(cb); + auto ctx = new Socket::RequestContext(size, bytes, callback); auto req = new uv_udp_send_t; req->data = (void *) ctx; - ctx->peer = this; + ctx->socket = this; - err = uv_udp_send(req, (uv_udp_t *) &this->handle, &buffer, 1, sockaddr, [](uv_udp_send_t *req, int status) { - auto ctx = reinterpret_cast<Peer::RequestContext*>(req->data); - auto peer = ctx->peer; + err = uv_udp_send(req, (uv_udp_t *) &this->handle, &ctx->buffer, 1, sockaddr, [](uv_udp_send_t *req, int status) { + auto ctx = reinterpret_cast<Socket::RequestContext*>(req->data); + auto socket = ctx->socket; - ctx->cb(status, Post{}); + ctx->callback(status, Post{}); - if (peer->isEphemeral()) { - peer->close(); + if (socket->isEphemeral()) { + socket->close(); } delete ctx; @@ -462,7 +384,7 @@ namespace SSC { }); if (err < 0) { - ctx->cb(err, Post{}); + ctx->callback(err, Post{}); if (this->isEphemeral()) { this->close(); @@ -473,7 +395,7 @@ namespace SSC { } } - int Peer::recvstart () { + int Socket::recvstart () { if (this->receiveCallback != nullptr) { return this->recvstart(this->receiveCallback); } @@ -481,14 +403,14 @@ namespace SSC { return UV_EINVAL; } - int Peer::recvstart (Peer::UDPReceiveCallback receiveCallback) { + int Socket::recvstart (Socket::UDPReceiveCallback receiveCallback) { Lock lock(this->mutex); - if (this->hasState(PEER_STATE_UDP_RECV_STARTED)) { + if (this->hasState(SOCKET_STATE_UDP_RECV_STARTED)) { return UV_EALREADY; } - this->addState(PEER_STATE_UDP_RECV_STARTED); + this->addState(SOCKET_STATE_UDP_RECV_STARTED); this->receiveCallback = receiveCallback; auto allocate = [](uv_handle_t *handle, size_t size, uv_buf_t *buf) { @@ -505,32 +427,29 @@ namespace SSC { const struct sockaddr *addr, unsigned flags ) { - auto peer = (Peer *) handle->data; + auto socket = (Socket *) handle->data; if (nread == UV_ENOTCONN) { - peer->recvstop(); + socket->recvstop(); return; } - peer->receiveCallback(nread, buf, addr); + socket->receiveCallback(nread, buf, addr); }; return uv_udp_recv_start((uv_udp_t *) &this->handle, allocate, receive); } - int Peer::recvstop () { - int err = 0; - - if (this->hasState(PEER_STATE_UDP_RECV_STARTED)) { - this->removeState(PEER_STATE_UDP_RECV_STARTED); - Lock lock(this->core->loopMutex); - err = uv_udp_recv_stop((uv_udp_t *) &this->handle); + int Socket::recvstop () { + if (this->hasState(SOCKET_STATE_UDP_RECV_STARTED)) { + this->removeState(SOCKET_STATE_UDP_RECV_STARTED); + return uv_udp_recv_stop((uv_udp_t *) &this->handle); } - return err; + return 0; } - int Peer::resume () { + int Socket::resume () { int err = 0; if (this->isPaused()) { @@ -546,13 +465,13 @@ namespace SSC { // @TODO } - this->removeState(PEER_STATE_UDP_PAUSED); + this->removeState(SOCKET_STATE_UDP_PAUSED); } return err; } - int Peer::pause () { + int Socket::pause () { int err = 0; if ((err = this->recvstop())) { @@ -560,10 +479,14 @@ namespace SSC { } if (!this->isPaused() && !this->isClosing()) { - this->addState(PEER_STATE_UDP_PAUSED); + this->addState(SOCKET_STATE_UDP_PAUSED); if (this->isBound()) { Lock lock(this->mutex); - uv_close((uv_handle_t *) &this->handle, nullptr); + if ( + !uv_is_closing(reinterpret_cast<uv_handle_t*>(&this->handle)) + ) { + uv_close((uv_handle_t *) &this->handle, nullptr); + } } else if (this->isConnected()) { // TODO } @@ -572,13 +495,15 @@ namespace SSC { return err; } - void Peer::close () { + void Socket::close () { return this->close(nullptr); } - void Peer::close (std::function<void()> onclose) { + void Socket::close (Function<void()> onclose) { + Lock lock(this->mutex); + if (this->isClosed()) { - this->core->removePeer(this->id); + this->core->udp.removeSocket(this->id); if (onclose != nullptr) { onclose(); } @@ -593,27 +518,26 @@ namespace SSC { } if (onclose != nullptr) { - Lock lock(this->mutex); this->onclose.push_back(onclose); } - if (this->type == PEER_TYPE_UDP) { - Lock lock(this->mutex); + if (this->type == SOCKET_TYPE_UDP) { // reset state and set to CLOSED uv_close((uv_handle_t*) &this->handle, [](uv_handle_t *handle) { - auto peer = (Peer *) handle->data; - if (peer != nullptr) { - peer->removeState((peer_state_t) ( - PEER_STATE_UDP_BOUND | - PEER_STATE_UDP_CONNECTED | - PEER_STATE_UDP_RECV_STARTED + auto socket = (Socket *) handle->data; + if (socket != nullptr) { + socket->removeState((socket_state_t) ( + SOCKET_STATE_UDP_BOUND | + SOCKET_STATE_UDP_CONNECTED | + SOCKET_STATE_UDP_RECV_STARTED )); - for (const auto &onclose : peer->onclose) { + for (const auto &onclose : socket->onclose) { onclose(); } - delete peer; + socket->core->udp.removeSocket(socket->id); + socket = nullptr; } }); } diff --git a/src/core/socket.hh b/src/core/socket.hh new file mode 100644 index 0000000000..ac9d403417 --- /dev/null +++ b/src/core/socket.hh @@ -0,0 +1,172 @@ +#ifndef SOCKET_RUNTIME_CORE_SOCKET_H +#define SOCKET_RUNTIME_CORE_SOCKET_H + +#include "../platform/platform.hh" +#include "post.hh" + +namespace SSC { + class Core; + typedef enum { + SOCKET_TYPE_NONE = 0, + SOCKET_TYPE_TCP = 1 << 1, + SOCKET_TYPE_UDP = 1 << 2, + SOCKET_TYPE_MAX = 0xF + } socket_type_t; + + typedef enum { + SOCKET_FLAG_NONE = 0, + SOCKET_FLAG_EPHEMERAL = 1 << 1 + } socket_flag_t; + + typedef enum { + SOCKET_STATE_NONE = 0, + // general states + SOCKET_STATE_CLOSED = 1 << 1, + // udp states (10) + SOCKET_STATE_UDP_BOUND = 1 << 10, + SOCKET_STATE_UDP_CONNECTED = 1 << 11, + SOCKET_STATE_UDP_RECV_STARTED = 1 << 12, + SOCKET_STATE_UDP_PAUSED = 1 << 13, + // tcp states (20) + SOCKET_STATE_TCP_BOUND = 1 << 20, + SOCKET_STATE_TCP_CONNECTED = 1 << 21, + SOCKET_STATE_TCP_PAUSED = 1 << 13, + SOCKET_STATE_MAX = 1 << 0xF + } socket_state_t; + + struct LocalPeerInfo { + struct sockaddr_storage addr; + String address = ""; + String family = ""; + int port = 0; + int err = 0; + + int getsockname (uv_udp_t *socket, struct sockaddr *addr); + int getsockname (uv_tcp_t *socket, struct sockaddr *addr); + void init (uv_udp_t *socket); + void init (uv_tcp_t *socket); + void init (const struct sockaddr_storage *addr); + }; + + struct RemotePeerInfo { + struct sockaddr_storage addr; + String address = ""; + String family = ""; + int port = 0; + int err = 0; + + int getpeername (uv_udp_t *socket, struct sockaddr *addr); + int getpeername (uv_tcp_t *socket, struct sockaddr *addr); + void init (uv_udp_t *socket); + void init (uv_tcp_t *socket); + void init (const struct sockaddr_storage *addr); + }; + + /** + * A generic structure for a bound or connected socket. + */ + class Socket { + public: + struct RequestContext { + using Callback = Function<void(int, Post)>; + SharedPointer<char[]> bytes = nullptr; + size_t size = 0; + uv_buf_t buffer; + Callback callback; + Socket* socket = nullptr; + RequestContext (Callback callback) { this->callback = callback; } + RequestContext (size_t size, SharedPointer<char[]> bytes, Callback callback) + : size(size), + bytes(bytes), + callback(callback) + { + if (bytes != nullptr) { + this->buffer = uv_buf_init(bytes.get(), size); + } + } + }; + + using UDPReceiveCallback = Function<void( + ssize_t, + const uv_buf_t*, + const struct sockaddr* + )>; + + // uv handles + union { + uv_udp_t udp; + uv_tcp_t tcp; // XXX: FIXME + } handle; + + // sockaddr + struct sockaddr_in addr; + + // callbacks + UDPReceiveCallback receiveCallback; + Vector<Function<void()>> onclose; + + // instance state + uint64_t id = 0; + Mutex mutex; + Core *core = nullptr; + + struct { + struct { + bool reuseAddr = false; + bool ipv6Only = false; // @TODO + } udp; + } options; + + // peer state + LocalPeerInfo local; + RemotePeerInfo remote; + socket_type_t type = SOCKET_TYPE_NONE; + socket_flag_t flags = SOCKET_FLAG_NONE; + socket_state_t state = SOCKET_STATE_NONE; + + /** + * Private `Socket` class constructor + */ + Socket (Core *core, socket_type_t peerType, uint64_t peerId, bool isEphemeral); + ~Socket (); + + int init (); + int initRemotePeerInfo (); + int initLocalPeerInfo (); + void addState (socket_state_t value); + void removeState (socket_state_t value); + bool hasState (socket_state_t value); + const RemotePeerInfo* getRemotePeerInfo (); + const LocalPeerInfo* getLocalPeerInfo (); + bool isUDP (); + bool isTCP (); + bool isEphemeral (); + bool isBound (); + bool isActive (); + bool isClosing (); + bool isClosed (); + bool isConnected (); + bool isPaused (); + int bind (); + int bind (const String& address, int port); + int bind (const String& address, int port, bool reuseAddr); + int rebind (); + int connect (const String& address, int port); + int disconnect (); + void send ( + SharedPointer<char[]> bytes, + size_t size, + int port, + const String& address, + const Socket::RequestContext::Callback& callback + ); + int recvstart (); + int recvstart (UDPReceiveCallback onrecv); + int recvstop (); + int resume (); + int pause (); + void close (); + void close (Function<void()> onclose); + }; +} +#endif diff --git a/src/core/trace.cc b/src/core/trace.cc new file mode 100644 index 0000000000..390d65eff1 --- /dev/null +++ b/src/core/trace.cc @@ -0,0 +1,216 @@ +#include "trace.hh" + +namespace SSC { + Tracer::Tracer (const String& name) + : name(name), + spans(std::make_shared<SharedSpanCollection>()) + {} + + Tracer::Tracer (const Tracer& tracer) + : name(tracer.name), + spans(tracer.spans) + {} + + Tracer::Tracer (Tracer&& tracer) noexcept + : name(tracer.name), + spans(std::move(tracer.spans)) + {} + + Tracer& Tracer::operator= (const Tracer& tracer) { + if (this != &tracer) { + this->name = tracer.name; + this->spans = tracer.spans; + } + return *this; + } + + Tracer& Tracer::operator= (Tracer&& tracer) noexcept { + if (this != &tracer) { + this->name = tracer.name; + this->spans = std::move(tracer.spans); + } + return *this; + } + + Tracer::SharedSpan Tracer::span (const String& name, const Span::ID id) { + auto span = std::make_shared<Span>(*this, name); + + if (id > 0) { + span->id = id; + } + + do { + Lock lock(this->mutex); + const auto i = this->spans->size(); + this->spans->emplace_back(span); + this->index[span->id] = i; + } while (0); + + return span; + } + + Tracer::SharedSpan Tracer::span (const Span::ID id) { + Lock lock(this->mutex); + if (this->index.contains(id)) { + const auto i = this->index[id]; + if (i < this->spans->size()) { + return this->spans->at(i); + } + } + + return nullptr; + } + + size_t Tracer::size (bool onlyActive) const noexcept { + size_t count = 0; + for (const auto& span : *this->spans) { + if (onlyActive && !span->ended) { + continue; + } + + count++; + } + + return count; + } + + JSON::Object Tracer::json () const { + JSON::Array spans; + + for (const auto& span : *this->spans) { + // only top level spans + if (span->parent == nullptr) { + spans.push(span->json()); + } + } + + return JSON::Object::Entries { + {"id", this->id}, + {"name", this->name}, + {"spans", spans}, + {"type", "Tracer"} + }; + } + + const Tracer::SharedSpan Tracer::begin (const String& name) { + Lock lock(this->mutex); + + for (const auto& span : *this->spans) { + if (span->name == name) { + return span; + } + } + + return this->span(name); + } + + bool Tracer::end (const String& name) { + Lock lock(this->mutex); + for (const auto& span : *this->spans) { + if (span->name == name) { + span->end(); + return true; + } + } + + return false; + } + + const Tracer::Iterator Tracer::begin () const noexcept { + return this->spans->begin(); + } + + const Tracer::Iterator Tracer::end () const noexcept { + return this->spans->end(); + } + + const bool Tracer::clear () noexcept { + Lock lock(this->mutex); + if (this->spans->size() > 0) { + this->spans->clear(); + return true; + } + return false; + } + + Tracer::Duration Tracer::Timing::now () { + using namespace std::chrono; + return duration_cast<Duration>(system_clock::now().time_since_epoch()); + } + + void Tracer::Timing::stop () { + using namespace std::chrono; + this->end = TimePoint(Timing::now()); + this->duration = duration_cast<milliseconds>(this->end.load() - this->start.load()); + } + + JSON::Object Tracer::Timing::json () const { + using namespace std::chrono; + const auto duration = duration_cast<milliseconds>(this->duration.load()).count(); + const auto start = time_point_cast<milliseconds>(this->start.load()).time_since_epoch().count(); + const auto end = time_point_cast<milliseconds>(this->end.load()).time_since_epoch().count(); + return JSON::Object::Entries { + {"start", start}, + {"end", end}, + {"duration", duration}, + {"type", "Timing"} + }; + } + + Tracer::Timing::Timing () + : start(TimePoint(Timing::now())) + {} + + Tracer::Span::Span (Tracer& tracer, const String& name) + : tracer(tracer), + name(name) + {} + + Tracer::Span::~Span () { + this->end(); + } + + bool Tracer::Span::end () { + if (this->ended) { + return false; + } + + this->timing.stop(); + this->ended = true; + return true; + } + + JSON::Object Tracer::Span::json () const { + JSON::Array spans; + + for (const auto id : this->spans) { + const auto span = this->tracer.span(id); + if (span != nullptr) { + spans.push(span->json()); + } + } + + return JSON::Object::Entries { + {"id", this->id}, + {"name", this->name}, + {"timing", this->timing.json()}, + {"spans", spans}, + {"type", "Span"}, + {"tracer", JSON::Object::Entries { + {"id", this->tracer.id}, + {"name", this->tracer.name} + }} + }; + } + + Tracer::SharedSpan Tracer::Span::span (const String& name, const Span::ID id) { + auto span = this->tracer.span(name, id); + span->parent = this; + return span; + } + + long Tracer::Span::duration () const { + using namespace std::chrono; + return duration_cast<milliseconds>(this->timing.duration.load()).count(); + } +} diff --git a/src/core/trace.hh b/src/core/trace.hh new file mode 100644 index 0000000000..21c1739951 --- /dev/null +++ b/src/core/trace.hh @@ -0,0 +1,253 @@ +#ifndef SOCKET_RUNTIME_CORE_TRACE_H +#define SOCKET_RUNTIME_CORE_TRACE_H + +#include "../platform/platform.hh" +#include "json.hh" + +namespace SSC { + /** + * The `Tracer` class manages multiple `Tracer::Span` instances, allowing + * spans to be created and tracked across multiple threads. + * It ensures thread-safe access to the spans and provides support for + * copying and moving, making it suitable for use in async and + * multi-threaded contexts. + */ + class Tracer { + public: + // forward + class Span; + + /** + * A high resolution time time point used by the `Tracer::Span` class. + */ + using TimePoint = std::chrono::time_point<std::chrono::high_resolution_clock>; + /** + * A tracer duration quantized to milliseconds + */ + using Duration = std::chrono::milliseconds; + + /** + * A shared pointer `Tracer::Span` type + */ + using SharedSpan = SharedPointer<Span>; + + /** + * A collection of `SharedSpan` instances + */ + using SharedSpanCollection = Vector<SharedSpan>; + + /** + * A mapping type of `Span::ID` to `Vector` index for fast span access + */ + using SharedSpanColletionIndex = std::map<uint64_t, size_t>; + + /** + * A `Tracer` ID type + */ + using ID = uint64_t; + + /** + * Iterator for a `Tracer` + */ + using Iterator = SharedSpanCollection::const_iterator; + + /** + * A container for `Tracer` timing. + */ + struct Timing { + Atomic<TimePoint> start; + Atomic<TimePoint> end; + Atomic<Duration> duration; + static Duration now (); + Timing (); + void stop (); + JSON::Object json () const; + }; + + /** + * The `Tracer::Span` class represents a "time span", tracking its start + * time and providing the ability to end it, either automatically upon + * destruction or manually via the end() method. It is thread-safe and + * ensures that the duration is printed only once. + */ + class Span { + public: + using ID = uint64_t; + + /** + * Iterator for a `Span` + */ + using Iterator = Vector<SharedSpan>::const_iterator; + + /** + * A unique ID for this `Span`. + */ + ID id = rand64(); + + /** + * The name of this span + */ + String name; + + /** + * The span timing. + */ + Timing timing; + + /** + * This value is `true` if the span has ended + */ + Atomic<bool> ended = false; + + /** + * A strong reference to the `Tracer` that created this `Span`. + */ + Tracer& tracer; + + /** + * A weak pointer to a parent `Span` that created this `Span`. + * This value may be `nullptr`. + */ + Span* parent = nullptr; + + /** + * A vector of `Span::ID` that point to a `Span` instance in a + * `Tracer`. + */ + Vector<ID> spans; + + /** + * Initializes a new span with the given name and starts the timer. + */ + Span (Tracer& tracer, const String& name); + + /** + * Ends the span, storing the duration if it has not been ended + * already. + */ + ~Span (); + + /** + * Ends the span, storing the duration, and ensures thread-safe + * access. If the span has already been ended, this method does + * nothing. + * + * This function returns `true` if the span ended for the first time, + * otherwise `false` + */ + bool end (); + + /** + * Computed JSON representation of this `Tracer::Span` instance. + */ + JSON::Object json () const; + + /** + * Creates a new `Span` with the given name, adds it to the + * collection of spans, and returns a "shared span" (a shared pointer + * to the span). This functinon ensures thread-safe access to the + * collection. The returned `Span` is a "child" of this `Span`. + */ + SharedSpan span (const String& name, const ID id = 0); + + /** + * Returns the computed duration for an "ended" `Span` + */ + long duration () const; + }; + + /** + * A collection of shared spans owned by this `Tracer` instance. + */ + SharedPointer<SharedSpanCollection> spans = nullptr; + + /** + * The shared span collection index + */ + SharedSpanColletionIndex index; + + /** + * Used for thread synchronization + */ + Mutex mutex; + + /** + * The name of the `Tracer` instance. + */ + String name; + + /** + * A unique ID for this `Tracer`. + */ + ID id = rand64(); + + /** + * Initializes a new Tracer instance with an empty collection of spans. + */ + Tracer (const String& name); + // copy + Tracer (const Tracer&); + // move + Tracer (Tracer&&) noexcept; + ~Tracer () = default; + + // copy + Tracer& operator= (const Tracer&); + // move + Tracer& operator= (Tracer&&) noexcept; + + /** + * Creates a new `Tracer::Span` with the given name, adds it to the + * collection of spans, and returns a "shared span" (a shared pointer + * to the span). This functinon ensures thread-safe access to the + * collection. + */ + SharedSpan span (const String& name, const Span::ID id = 0); + + /** + * Gets a span by `Span::ID`. This function _will not_ create a new + * `Span`, but instead return a "null" `SharedSpan`. + */ + SharedSpan span (const Span::ID id); + + /** + * The number of spans in this tracer. This function accepts an + * `onlyActive` boolean to get the computed "active" (not ended) + * spans in a trace. + */ + size_t size (bool onlyActive = false) const noexcept; + + /** + * Computed JSON representation of this `Tracer` instance and its + * `Tracer::Span` children. + */ + JSON::Object json () const; + + /** + * Create or return a span by name to "begin" a span + */ + const SharedSpan begin (const String& name); + + /** + * Ends a named span if one exists. + */ + bool end (const String& name); + + /** + * Get the beginning of iterator to the vector `Span` instances. + */ + const Iterator begin () const noexcept; + + /** + * Get the end of iterator to the vector of `Span` instances. + */ + const Iterator end () const noexcept; + + /** + * Clears all `Span` entries in the `Tracer`. + */ + const bool clear () noexcept; + }; +} + +#endif diff --git a/src/core/udp.cc b/src/core/udp.cc deleted file mode 100644 index e6d4768332..0000000000 --- a/src/core/udp.cc +++ /dev/null @@ -1,592 +0,0 @@ -#include "core.hh" - -namespace SSC { - static JSON::Object::Entries ERR_SOCKET_ALREADY_BOUND ( - const String& source, - uint64_t id - ) { - return JSON::Object::Entries { - {"source", source}, - {"err", JSON::Object::Entries { - {"id", std::to_string(id)}, - {"type", "InternalError"}, - {"code", "ERR_SOCKET_ALREADY_BOUND"}, - {"message", "Socket is already bound"} - }} - }; - } - - static JSON::Object::Entries ERR_SOCKET_DGRAM_IS_CONNECTED ( - const String &source, - uint64_t id - ) { - return JSON::Object::Entries { - {"source", source}, - {"err", JSON::Object::Entries { - {"id", std::to_string(id)}, - {"type", "InternalError"}, - {"code", "ERR_SOCKET_DGRAM_IS_CONNECTED"}, - {"message", "Already connected"} - }} - }; - } - - static JSON::Object::Entries ERR_SOCKET_DGRAM_NOT_CONNECTED ( - const String &source, - uint64_t id - ) { - return JSON::Object::Entries { - {"source", source}, - {"err", JSON::Object::Entries { - {"id", std::to_string(id)}, - {"type", "InternalError"}, - {"code", "ERR_SOCKET_DGRAM_NOT_CONNECTED"}, - {"message", "Not connected"} - }} - }; - } - - static JSON::Object::Entries ERR_SOCKET_DGRAM_CLOSED ( - const String& source, - uint64_t id - ) { - return JSON::Object::Entries { - {"source", source}, - {"err", JSON::Object::Entries { - {"id", std::to_string(id)}, - {"type", "InternalError"}, - {"code", "ERR_SOCKET_DGRAM_CLOSED"}, - {"message", "Socket is closed"} - }} - }; - } - - static JSON::Object::Entries ERR_SOCKET_DGRAM_CLOSING ( - const String& source, - uint64_t id - ) { - return JSON::Object::Entries { - {"source", source}, - {"err", JSON::Object::Entries { - {"id", std::to_string(id)}, - {"type", "NotFoundError"}, - {"code", "ERR_SOCKET_DGRAM_CLOSING"}, - {"message", "Socket is closing"} - }} - }; - } - - static JSON::Object::Entries ERR_SOCKET_DGRAM_NOT_RUNNING ( - const String& source, - uint64_t id - ) { - return JSON::Object::Entries { - {"source", source}, - {"err", JSON::Object::Entries { - {"id", std::to_string(id)}, - {"type", "NotFoundError"}, - {"code", "ERR_SOCKET_DGRAM_NOT_RUNNING"}, - {"message", "Not running"} - }} - }; - } - - void Core::UDP::bind ( - const String seq, - uint64_t peerId, - UDP::BindOptions options, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { - if (this->core->hasPeer(peerId)) { - if (this->core->getPeer(peerId)->isBound()) { - auto json = ERR_SOCKET_ALREADY_BOUND("udp.bind", peerId); - return cb(seq, json, Post{}); - } - } - - auto peer = this->core->createPeer(PEER_TYPE_UDP, peerId); - auto err = peer->bind(options.address, options.port, options.reuseAddr); - - if (err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.bind"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto info = peer->getLocalPeerInfo(); - - if (info->err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.bind"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(info->err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.bind"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"port", (int) info->port}, - {"event" , "listening"}, - {"family", info->family}, - {"address", info->address} - }} - }; - - cb(seq, json, Post{}); - }); - } - - void Core::UDP::connect ( - const String seq, - uint64_t peerId, - UDP::ConnectOptions options, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { - auto peer = this->core->createPeer(PEER_TYPE_UDP, peerId); - - if (peer->isConnected()) { - auto json = ERR_SOCKET_DGRAM_IS_CONNECTED("udp.connect", peerId); - return cb(seq, json, Post{}); - } - - auto err = peer->connect(options.address, options.port); - - if (err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.connect"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto info = peer->getRemotePeerInfo(); - - if (info->err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.connect"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(info->err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.connect"}, - {"data", JSON::Object::Entries { - {"address", info->address}, - {"family", info->family}, - {"port", (int) info->port}, - {"id", std::to_string(peerId)} - }} - }; - - cb(seq, json, Post{}); - }); - } - - void Core::UDP::disconnect ( - const String seq, - uint64_t peerId, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { - if (!this->core->hasPeer(peerId)) { - auto json = ERR_SOCKET_DGRAM_NOT_CONNECTED("udp.disconnect", peerId); - return cb(seq, json, Post{}); - } - - auto peer = this->core->getPeer(peerId); - auto err = peer->disconnect(); - - if (err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.disconnect"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.disconnect"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)} - }} - }; - - cb(seq, json, Post{}); - }); - } - - void Core::UDP::getPeerName (String seq, uint64_t peerId, Module::Callback cb) { - if (!this->core->hasPeer(peerId)) { - auto json = ERR_SOCKET_DGRAM_NOT_CONNECTED("udp.getPeerName", peerId); - return cb(seq, json, Post{}); - } - - auto peer = this->core->getPeer(peerId); - auto info = peer->getRemotePeerInfo(); - - if (info->err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.getPeerName"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(info->err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.getPeerName"}, - {"data", JSON::Object::Entries { - {"address", info->address}, - {"family", info->family}, - {"port", (int) info->port}, - {"id", std::to_string(peerId)} - }} - }; - - cb(seq, json, Post{}); - } - - void Core::UDP::getSockName (String seq, uint64_t peerId, Callback cb) { - if (!this->core->hasPeer(peerId)) { - auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.getSockName", peerId); - return cb(seq, json, Post{}); - } - - auto peer = this->core->getPeer(peerId); - auto info = peer->getLocalPeerInfo(); - - if (info->err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.getSockName"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(info->err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.getSockName"}, - {"data", JSON::Object::Entries { - {"address", info->address}, - {"family", info->family}, - {"port", (int) info->port}, - {"id", std::to_string(peerId)} - }} - }; - - cb(seq, json, Post{}); - } - - void Core::UDP::getState ( - const String seq, - uint64_t peerId, - Module::Callback cb - ) { - if (!this->core->hasPeer(peerId)) { - auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.getState", peerId); - return cb(seq, json, Post{}); - } - - auto peer = this->core->getPeer(peerId); - - if (!peer->isUDP()) { - auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.getState", peerId); - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.getState"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"type", "udp"}, - {"bound", peer->isBound()}, - {"active", peer->isActive()}, - {"closed", peer->isClosed()}, - {"closing", peer->isClosing()}, - {"connected", peer->isConnected()}, - {"ephemeral", peer->isEphemeral()} - }} - }; - - cb(seq, json, Post{}); - } - - void Core::UDP::send ( - String seq, - uint64_t peerId, - UDP::SendOptions options, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this] { - auto peer = this->core->createPeer(PEER_TYPE_UDP, peerId, options.ephemeral); - auto size = options.size; // @TODO(jwerle): validate MTU - auto port = options.port; - auto bytes = options.bytes; - auto address = options.address; - peer->send(bytes, size, port, address, [=](auto status, auto post) { - if (status < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.send"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(status))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.send"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"status", status} - }} - }; - - cb(seq, json, Post{}); - }); - }); - } - - void Core::UDP::readStart (String seq, uint64_t peerId, Module::Callback cb) { - if (!this->core->hasPeer(peerId)) { - auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.readStart", peerId); - return cb(seq, json, Post{}); - } - - auto peer = this->core->getPeer(peerId); - - if (peer->isClosed()) { - auto json = ERR_SOCKET_DGRAM_CLOSED("udp.readStart", peerId); - return cb(seq, json, Post{}); - } - - if (peer->isClosing()) { - auto json = ERR_SOCKET_DGRAM_CLOSING("udp.readStart", peerId); - return cb(seq, json, Post{}); - } - - if (peer->hasState(PEER_STATE_UDP_RECV_STARTED)) { - auto json = JSON::Object::Entries { - {"source", "udp.readStart"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", "Socket is already receiving"} - }} - }; - - return cb(seq, json, Post{}); - } - - if (peer->isActive()) { - auto json = JSON::Object::Entries { - {"source", "udp.readStart"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)} - }} - }; - - return cb(seq, json, Post{}); - } - - auto err = peer->recvstart([=](auto nread, auto buf, auto addr) { - if (nread == UV_EOF) { - auto json = JSON::Object::Entries { - {"source", "udp.readStart"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"EOF", true} - }} - }; - - cb("-1", json, Post{}); - } else if (nread > 0) { - char address[17] = {0}; - Post post; - int port; - - parseAddress((struct sockaddr *) addr, &port, address); - - auto headers = Headers {{ - {"content-type" ,"application/octet-stream"}, - {"content-length", nread} - }}; - - post.id = rand64(); - post.body = buf->base; - post.length = (int) nread; - post.headers = headers.str(); - - auto json = JSON::Object::Entries { - {"source", "udp.readStart"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"port", port}, - {"bytes", std::to_string(post.length)}, - {"address", address} - }} - }; - - cb("-1", json, post); - } - }); - - // `UV_EALREADY || UV_EBUSY` could mean there might be - // active IO on the underlying handle - if (err < 0 && err != UV_EALREADY && err != UV_EBUSY) { - auto json = JSON::Object::Entries { - {"source", "udp.readStart"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.readStart"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)} - }} - }; - - cb(seq, json, Post {}); - } - - void Core::UDP::readStop ( - const String seq, - uint64_t peerId, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this] { - if (!this->core->hasPeer(peerId)) { - auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.readStop", peerId); - return cb(seq, json, Post{}); - } - - auto peer = this->core->getPeer(peerId); - - if (peer->isClosed()) { - auto json = ERR_SOCKET_DGRAM_CLOSED("udp.readStop", peerId); - return cb(seq, json, Post{}); - } - - if (peer->isClosing()) { - auto json = ERR_SOCKET_DGRAM_CLOSING("udp.readStop", peerId); - return cb(seq, json, Post{}); - } - - if (!peer->hasState(PEER_STATE_UDP_RECV_STARTED)) { - auto json = JSON::Object::Entries { - {"source", "udp.readStop"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", "Socket is not receiving"} - }} - }; - - return cb(seq, json, Post{}); - } - - auto err = peer->recvstop(); - - if (err < 0) { - auto json = JSON::Object::Entries { - {"source", "udp.readStop"}, - {"err", JSON::Object::Entries { - {"id", std::to_string(peerId)}, - {"message", String(uv_strerror(err))} - }} - }; - - return cb(seq, json, Post{}); - } - - auto json = JSON::Object::Entries { - {"source", "udp.readStop"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)} - }} - }; - - cb(seq, json, Post {}); - }); - } - - void Core::UDP::close ( - const String seq, - uint64_t peerId, - Module::Callback cb - ) { - this->core->dispatchEventLoop([=, this]() { - if (!this->core->hasPeer(peerId)) { - auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.close", peerId); - return cb(seq, json, Post{}); - } - - auto peer = this->core->getPeer(peerId); - - if (!peer->isUDP()) { - auto json = ERR_SOCKET_DGRAM_NOT_RUNNING("udp.close", peerId); - return cb(seq, json, Post{}); - } - - if (peer->isClosed()) { - auto json = ERR_SOCKET_DGRAM_CLOSED("udp.close", peerId); - return cb(seq, json, Post{}); - } - - if (peer->isClosing()) { - auto json = ERR_SOCKET_DGRAM_CLOSING("udp.close", peerId); - return cb(seq, json, Post{}); - } - - peer->close([=, this]() { - auto json = JSON::Object::Entries { - {"source", "udp.close"}, - {"data", JSON::Object::Entries { - {"id", std::to_string(peerId)} - }} - }; - - cb(seq, json, Post{}); - }); - }); - } -} diff --git a/src/core/unique_client.hh b/src/core/unique_client.hh new file mode 100644 index 0000000000..8c6697278b --- /dev/null +++ b/src/core/unique_client.hh @@ -0,0 +1,13 @@ +#ifndef SOCKET_RUNTIME_UNIQUE_CLIENT_H +#define SOCKET_RUNTIME_UNIQUE_CLIENT_H + +#include "../platform/platform.hh" + +namespace SSC { + struct UniqueClient { + using ID = uint64_t; + ID id = 0; + int index = 0; + }; +} +#endif diff --git a/src/core/url.cc b/src/core/url.cc new file mode 100644 index 0000000000..141a0bb1eb --- /dev/null +++ b/src/core/url.cc @@ -0,0 +1,307 @@ +#include "codec.hh" +#include "debug.hh" +#include "url.hh" + +namespace SSC { + const URL::Components URL::Components::parse (const String& url) { + URL::Components components; + components.originalURL = url; + auto input = url; + + if (input.starts_with("./")) { + input = input.substr(1); + } + + if (!input.starts_with("/")) { + const auto colon = input.find(':'); + + if (colon != String::npos) { + components.scheme = input.substr(0, colon); + input = input.substr(colon + 1, input.size()); + + if (input.starts_with("//")) { + input = input.substr(2, input.size()); + + const auto slash = input.find("/"); + if (slash != String::npos) { + components.authority = input.substr(0, slash); + input = input.substr(slash, input.size()); + } else { + const auto questionMark = input.find("?"); + const auto fragment = input.find("#"); + if (questionMark != String::npos & fragment != String::npos) { + if (questionMark < fragment) { + components.authority = input.substr(0, questionMark); + input = input.substr(questionMark, input.size()); + } else { + components.authority = input.substr(0, fragment); + input = input.substr(fragment, input.size()); + } + } else if (questionMark != String::npos) { + components.authority = input.substr(0, questionMark); + input = input.substr(questionMark, input.size()); + } else if (fragment != String::npos) { + components.authority = input.substr(0, fragment); + input = input.substr(fragment, input.size()); + } else { + components.authority = input; + components.pathname = "/"; + } + } + } + } + } + + if (components.pathname.size() == 0) { + const auto questionMark = input.find("?"); + const auto fragment = input.find("#"); + + if (questionMark != String::npos && fragment != String::npos) { + if (questionMark < fragment) { + components.pathname = input.substr(0, questionMark); + components.query = input.substr(questionMark + 1, fragment - questionMark - 1); + components.fragment = input.substr(fragment + 1, input.size()); + } else { + components.pathname = input.substr(0, fragment); + components.fragment = input.substr(fragment + 1, input.size()); + } + } else if (questionMark != String::npos) { + components.pathname = input.substr(0, questionMark); + components.query = input.substr(questionMark + 1, input.size()); + } else if (fragment != String::npos) { + components.pathname = input.substr(0, fragment); + components.fragment = input.substr(fragment + 1, input.size()); + } else { + components.pathname = input; + } + + if (!components.pathname.starts_with("/")) { + components.pathname = "/" + components.pathname; + } + } + + return components; + } + + URL::Builder& URL::Builder::setProtocol (const String& protocol) { + this->protocol = protocol; + return *this; + } + + URL::Builder& URL::Builder::setUsername (const String& username) { + this->username = username; + return *this; + } + + URL::Builder& URL::Builder::setPassword (const String& password) { + this->password = password; + return *this; + } + + URL::Builder& URL::Builder::setHostname (const String& hostname) { + this->hostname = hostname; + return *this; + } + + URL::Builder& URL::Builder::setPort (const String& port) { + this->port = port; + return *this; + } + + URL::Builder& URL::Builder::setPort (const int port) { + this->port = std::to_string(port); + return *this; + } + + URL::Builder& URL::Builder::setPathname (const String& pathname) { + this->pathname = pathname; + return *this; + } + + URL::Builder& URL::Builder::setQuery (const String& query) { + this->search = "?" + query; + return *this; + } + + URL::Builder& URL::Builder::setSearch (const String& search) { + this->search = search; + return *this; + } + + URL::Builder& URL::Builder::setHash (const String& hash) { + this->hash = hash; + return *this; + } + + URL::Builder& URL::Builder::setFragment (const String& fragment) { + this->hash = "#" + fragment; + return *this; + } + + URL::Builder& URL::Builder::setSearchParam (const String& key, const String& value) { + return this->setSearchParams(Map {{ key, value }}); + } + + URL::Builder& URL::Builder::setSearchParam (const String& key, const JSON::Any& value) { + if (JSON::typeof(value) == "string" || JSON::typeof(value) == "number" || JSON::typeof(value) == "boolean") { + return this->setSearchParam(key, value.str()); + } + + return *this; + } + + URL::Builder& URL::Builder::setSearchParams (const Map& params) { + if (params.size() > 0) { + if (!this->search.starts_with("?")) { + this->search = "?"; + } else if (this->search.size() > 0) { + this->search += "&"; + } + + for (const auto& entry : params) { + this->search = entry.first + "=" + entry.second + "&"; + } + } + + if (this->search.ends_with("&")) { + this->search = this->search.substr(0, this->search.size() - 1); + } + + return *this; + } + + URL URL::Builder::build () const { + StringStream stream; + + if (this->protocol.size() == 0) { + return String(""); + } + + stream << this->protocol << ":"; + + if ( + (this->username.size() > 0 || this->password.size() > 0) && + this->hostname.size() > 0 + ) { + stream << "//"; + if (this->username.size() > 0) { + stream << this->username; + if (this->password.size() > 0) { + stream << ":" << this->password; + } + + stream << "@" << this->hostname; + if (this->port.size() > 0) { + stream << ":" << this->port; + } + } + } + + if (this->hostname.size() > 0 && this->pathname.size() > 0) { + if (!this->pathname.starts_with("/")) { + stream << "/"; + } + } + + stream << this->pathname << this->search << this->hash; + + return stream.str(); + } + + URL::URL (const JSON::Object& json) + : URL(json["href"].str()) + {} + + URL::URL (const String& href) { + if (href.size() > 0) { + this->set(href); + } + } + + void URL::set (const String& href) { + const auto components = URL::Components::parse(href); + + this->scheme = components.scheme; + this->pathname = components.pathname; + this->query = components.query; + this->fragment = components.fragment; + this->search = this->query.size() > 0 ? "?" + this->query : ""; + this->hash = this->fragment.size() > 0 ? "#" + this->fragment : ""; + + if (components.scheme.size() > 0) { + this->protocol = components.scheme + ":"; + } + + const auto authorityParts = components.authority.size() > 0 + ? split(components.authority, '@') + : Vector<String> {}; + + if (authorityParts.size() == 2) { + const auto userParts = split(authorityParts[0], ':'); + + if (userParts.size() == 2) { + this->username = userParts[0]; + this->password = userParts[1]; + } else if (userParts.size() == 1) { + this->username = userParts[0]; + } + + const auto hostParts = split(authorityParts[1], ':'); + if (hostParts.size() > 1) { + this->port = hostParts[1]; + } + + if (hostParts.size() > 0) { + this->hostname = hostParts[0]; + } + } else if (authorityParts.size() == 1) { + const auto hostParts = split(authorityParts[0], ':'); + if (hostParts.size() > 1) { + this->port = hostParts[1]; + } + + if (hostParts.size() > 0) { + this->hostname = hostParts[0]; + } + } + + if (this->protocol.size() > 0) { + if (this->hostname.size() > 0) { + this->origin = this->protocol + "//" + this->hostname; + } else { + this->origin = this->protocol + this->pathname; + } + + this->href = this->origin + this->pathname + this->search + this->hash; + } + + if (this->query.size() > 0) { + for (const auto& entry : split(this->query, '&')) { + const auto parts = split(entry, '='); + if (parts.size() == 2) { + const auto key = decodeURIComponent(trim(parts[0])); + const auto value = decodeURIComponent(trim(parts[1])); + this->searchParams.insert_or_assign(key, value); + } + } + } + } + + const String URL::str () const { + return this->href; + } + + const JSON::Object URL::json () const { + return JSON::Object::Entries { + {"href", this->href}, + {"origin", this->origin}, + {"protocol", this->protocol}, + {"username", this->username}, + {"password", this->password}, + {"hostname", this->hostname}, + {"pathname", this->pathname}, + {"search", this->search}, + {"hash", this->hash} + }; + } +} diff --git a/src/core/url.hh b/src/core/url.hh new file mode 100644 index 0000000000..bca1d1196d --- /dev/null +++ b/src/core/url.hh @@ -0,0 +1,75 @@ +#ifndef SOCKET_RUNTIME_CORE_URL_H +#define SOCKET_RUNTIME_CORE_URL_H + +#include "json.hh" + +namespace SSC { + struct URL { + struct Components { + String originalURL = ""; + String scheme = ""; + String authority = ""; + String pathname = ""; + String query = ""; + String fragment = ""; + + static const Components parse (const String& url); + }; + + struct Builder { + String protocol = ""; + String username = ""; + String password = ""; + String hostname = ""; + String port = ""; + String pathname = ""; + String search = ""; // includes '?' and 'query' if 'query' is not empty + String hash = ""; // include '#' and 'fragment' if 'fragment' is not empty + + Builder& setProtocol (const String& protocol); + Builder& setUsername (const String& username); + Builder& setPassword (const String& password); + Builder& setHostname (const String& hostname); + Builder& setPort (const String& port); + Builder& setPort (const int port); + Builder& setPathname (const String& pathname); + Builder& setQuery (const String& query); + Builder& setSearch (const String& search); + Builder& setHash (const String& hash); + Builder& setFragment (const String& fragment); + Builder& setSearchParam (const String& key, const String& value); + Builder& setSearchParam (const String& key, const JSON::Any& value); + Builder& setSearchParams (const Map& params); + + URL build () const; + }; + + // core properties + String href = ""; + String origin = ""; + String protocol = ""; + String username = ""; + String password = ""; + String hostname = ""; + String port = ""; + String pathname = ""; + String search = ""; // includes '?' and 'query' if 'query' is not empty + String hash = ""; // include '#' and 'fragment' if 'fragment' is not empty + + // extra properties + String scheme; + String fragment; + String query; + Map searchParams; + + URL () = default; + URL (const String& href); + URL (const JSON::Object& json); + + void set (const String& href); + void set (const JSON::Object& json); + const String str () const; + const JSON::Object json () const; + }; +} +#endif diff --git a/src/core/version.hh b/src/core/version.hh index 287636700d..46a64e5f9c 100644 --- a/src/core/version.hh +++ b/src/core/version.hh @@ -1,14 +1,13 @@ -#ifndef SSC_CORE_VERSION -#define SSC_CORE_VERSION +#ifndef SOCKET_RUNTIME_CORE_VERSION_H +#define SOCKET_RUNTIME_CORE_VERSION_H +#include "../platform/string.hh" #include "config.hh" -#include "string.hh" -#include "types.hh" namespace SSC { - inline const auto VERSION_FULL_STRING = String(CONVERT_TO_STRING(SSC_VERSION) " (" CONVERT_TO_STRING(SSC_VERSION_HASH) ")"); - inline const auto VERSION_HASH_STRING = String(CONVERT_TO_STRING(SSC_VERSION_HASH)); - inline const auto VERSION_STRING = String(CONVERT_TO_STRING(SSC_VERSION)); + inline const auto VERSION_FULL_STRING = String(CONVERT_TO_STRING(SOCKET_RUNTIME_VERSION) " (" CONVERT_TO_STRING(SOCKET_RUNTIME_VERSION_HASH) ")"); + inline const auto VERSION_HASH_STRING = String(CONVERT_TO_STRING(SOCKET_RUNTIME_VERSION_HASH)); + inline const auto VERSION_STRING = String(CONVERT_TO_STRING(SOCKET_RUNTIME_VERSION)); } #endif diff --git a/src/core/webview.cc b/src/core/webview.cc new file mode 100644 index 0000000000..b33fcf88b1 --- /dev/null +++ b/src/core/webview.cc @@ -0,0 +1,729 @@ +#include "webview.hh" +#include "../window/window.hh" + +using namespace SSC; + +#if SOCKET_RUNTIME_PLATFORM_APPLE +#if SOCKET_RUNTIME_PLATFORM_MACOS +@interface WKOpenPanelParameters (WKPrivate) +- (NSArray<NSString*>*) _acceptedMIMETypes; +- (NSArray<NSString*>*) _acceptedFileExtensions; +- (NSArray<NSString*>*) _allowedFileExtensions; +@end +#endif + +@implementation SSCWebView +#if SOCKET_RUNTIME_PLATFORM_MACOS +Vector<String> draggablePayload; +int lastX = 0; +int lastY = 0; + +- (void) viewDidAppear: (BOOL) animated { +} + +- (void) viewDidDisappear: (BOOL) animated { +} + +- (void) viewDidChangeEffectiveAppearance { + [super viewDidChangeEffectiveAppearance]; + const auto window = (Window*) objc_getAssociatedObject(self, "window"); + + if ([window->window.effectiveAppearance.name containsString: @"Dark"]) { + [window->window setBackgroundColor: [NSColor colorWithCalibratedWhite: 0.1 alpha: 1.0]]; // Dark mode color + } else { + [window->window setBackgroundColor: [NSColor colorWithCalibratedWhite: 1.0 alpha: 1.0]]; // Light mode color + } +} + +- (void) resizeSubviewsWithOldSize: (NSSize) oldSize { + [super resizeSubviewsWithOldSize: oldSize]; + + auto window = (Window*) objc_getAssociatedObject(self, "window"); + const auto w = reinterpret_cast<SSCWindow*>(window->window); + const auto viewWidth = w.titleBarView.frame.size.width; + const auto viewHeight = w.titleBarView.frame.size.height; + const auto newX = w.windowControlOffsets.x; + const auto newY = 0.f; + + const auto closeButton = [w standardWindowButton: NSWindowCloseButton]; + const auto minimizeButton = [w standardWindowButton: NSWindowMiniaturizeButton]; + const auto zoomButton = [w standardWindowButton: NSWindowZoomButton]; + + if (closeButton && minimizeButton && zoomButton) { + [w.titleBarView addSubview: closeButton]; + [w.titleBarView addSubview: minimizeButton]; + [w.titleBarView addSubview: zoomButton]; + } + + w.titleBarView.frame = NSMakeRect(newX, newY, viewWidth, viewHeight); +} + +- (instancetype) initWithFrame: (NSRect) frameRect + configuration: (WKWebViewConfiguration*) configuration + radius: (CGFloat) radius + margin: (CGFloat) margin +{ + self = [super initWithFrame: frameRect configuration: configuration]; + + if (self && radius > 0.0) { + self.radius = radius; + self.margin = margin; + self.layer.cornerRadius = radius; + self.layer.masksToBounds = YES; + } + + return self; +} + +- (void) layout { + [super layout]; + auto window = (Window*) objc_getAssociatedObject(self, "window"); + + #if SOCKET_RUNTIME_PLATFORM_MACOS + self.autoresizesSubviews = YES; + self.autoresizingMask = ( + NSViewHeightSizable | + NSViewWidthSizable | + NSViewMaxXMargin | + NSViewMinYMargin + ); + self.translatesAutoresizingMaskIntoConstraints = YES; + #endif + + NSRect bounds = self.superview.bounds; + + if (self.radius > 0.0) { + if (self.contentHeight == 0.0) { + self.contentHeight = self.superview.bounds.size.height - self.bounds.size.height; + } + + bounds.size.height = bounds.size.height - self.contentHeight; + } + + if (self.margin > 0.0) { + CGFloat borderWidth = self.margin; + #if SOCKET_RUNTIME_PLATFORM_MACOS + [window->window + setFrame: NSInsetRect(bounds, borderWidth, borderWidth) + display: YES + animate: NO + ]; + #elif SOCKET_RUNTIME_PLATFORM_IOS + window->window.frame = NSInsetRect(bounds, borderWidth, borderWidth); + #endif + } +} + +- (BOOL) wantsPeriodicDraggingUpdates { + return YES; +} + +- (BOOL) prepareForDragOperation: (id<NSDraggingInfo>) info { + [info setDraggingFormation: NSDraggingFormationNone]; + return YES; +} + +- (BOOL) performDragOperation: (id<NSDraggingInfo>) info { + return YES; +} + +- (void) concludeDragOperation: (id<NSDraggingInfo>) info { +} + +- (void) updateDraggingItemsForDrag: (id<NSDraggingInfo>) info { +} + +- (NSDragOperation) draggingEntered: (id<NSDraggingInfo>) info { + const auto json = JSON::Object {}; + const auto payload = getEmitToRenderProcessJavaScript("dragenter", json.str()); + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; + [self draggingUpdated: info]; + return NSDragOperationGeneric; +} + +- (NSDragOperation) draggingUpdated: (id<NSDraggingInfo>) info { + const auto position = info.draggingLocation; + const auto x = std::to_string(position.x); + const auto y = std::to_string(self.frame.size.height - position.y); + + auto count = draggablePayload.size(); + auto inbound = false; + + if (count == 0) { + inbound = true; + count = [info numberOfValidItemsForDrop]; + } + + const auto data = JSON::Object::Entries { + {"count", (unsigned int) count}, + {"inbound", inbound}, + {"x", x}, + {"y", y} + }; + + const auto json = JSON::Object {data}; + const auto payload = getEmitToRenderProcessJavaScript("drag", json.str()); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; + return [super draggingUpdated: info]; +} + +- (void) draggingExited: (id<NSDraggingInfo>) info { + const auto position = info.draggingLocation; + const auto x = std::to_string(position.x); + const auto y = std::to_string(self.frame.size.height - position.y); + + const auto data = JSON::Object::Entries { + {"x", x}, + {"y", y} + }; + + const auto json = JSON::Object {data}; + const auto payload = getEmitToRenderProcessJavaScript("dragend", json.str()); + + draggablePayload.clear(); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; +} + +- (void) draggingEnded: (id<NSDraggingInfo>) info { + const auto pasteboard = info.draggingPasteboard; + const auto position = info.draggingLocation; + const auto x = position.x; + const auto y = self.frame.size.height - position.y; + + const auto pasteboardFiles = [pasteboard + readObjectsForClasses: @[NSURL.class] + options: @{} + ]; + + auto files = JSON::Array::Entries {}; + + for (NSURL* file in pasteboardFiles) { + files.push_back(file.path.UTF8String); + } + + const auto data = JSON::Object::Entries { + {"files", files}, + {"x", x}, + {"y", y} + }; + + const auto json = JSON::Object { data }; + const auto payload = getEmitToRenderProcessJavaScript("dropin", json.str()); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; +} + +- (void) updateEvent: (NSEvent*) event { + const auto location = [self convertPoint: event.locationInWindow fromView :nil]; + const auto x = std::to_string(location.x); + const auto y = std::to_string(location.y); + const auto count = draggablePayload.size(); + + if (((int) location.x) == lastX || ((int) location.y) == lastY) { + return [super mouseDown: event]; + } + + const auto data = JSON::Object::Entries { + {"count", (unsigned int) count}, + {"x", x}, + {"y", y} + }; + + const auto json = JSON::Object { data }; + const auto payload = getEmitToRenderProcessJavaScript("drag", json.str()); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; +} + +- (void) mouseUp: (NSEvent*) event { + [super mouseUp: event]; + + const auto location = [self convertPoint: event.locationInWindow fromView: nil]; + const auto x = location.x; + const auto y = location.y; + + const auto significantMoveX = (lastX - x) > 6 || (x - lastX) > 6; + const auto significantMoveY = (lastY - y) > 6 || (y - lastY) > 6; + + if (significantMoveX || significantMoveY) { + for (const auto& path : draggablePayload) { + const auto data = JSON::Object::Entries { + {"src", path}, + {"x", x}, + {"y", y} + }; + + const auto json = JSON::Object { data }; + const auto payload = getEmitToRenderProcessJavaScript("drop", json.str()); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; + } + } + + const auto data = JSON::Object::Entries { + {"x", x}, + {"y", y} + }; + + const auto json = JSON::Object { data }; + auto payload = getEmitToRenderProcessJavaScript("dragend", json.str()); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; +} + +- (void) mouseDown: (NSEvent*) event { + self.shouldDrag = false; + draggablePayload.clear(); + + const auto point = [self convertPoint: event.locationInWindow fromView: nil]; + const auto x = std::to_string(point.x); + const auto y = std::to_string(point.y); + + self.initialWindowPos = point; + + lastX = (int) point.x; + lastY = (int) point.y; + + String js( + "(() => { " + " const v = '--app-region'; " + " let el = document.elementFromPoint(" + x + "," + y + "); " + " " + " while (el) { " + " if (getComputedStyle(el).getPropertyValue(v) === 'drag') { " + " return 'movable'; " + " } " + " el = el.parentElement; " + " } " + " return '' " + "})(); " + ); + + [self + evaluateJavaScript: @(js.c_str()) + completionHandler: ^(id result, NSError *error) + { + if (error) { + NSLog(@"%@", error); + [super mouseDown: event]; + return; + } + + if (![result isKindOfClass: NSString.class]) { + [super mouseDown: event]; + return; + } + + const auto match = String([result UTF8String]); + + if (match.compare("movable") != 0) { + [super mouseDown: event]; + return; + } + + self.shouldDrag = true; + [self updateEvent: event]; + }]; +} + +- (void) mouseDragged: (NSEvent*) event { + NSPoint currentLocation = [self convertPoint:event.locationInWindow fromView:nil]; + const auto window = (Window*) objc_getAssociatedObject(self, "window"); + + if (self.shouldDrag) { + CGFloat deltaX = currentLocation.x - self.initialWindowPos.x; + CGFloat deltaY = currentLocation.y - self.initialWindowPos.y; + + NSRect frame = window->window.frame; + frame.origin.x += deltaX; + frame.origin.y -= deltaY; + + [window->window setFrame:frame display:YES]; + } + + [super mouseDragged:event]; + + if (!NSPointInRect(currentLocation, self.frame)) { + auto payload = getEmitToRenderProcessJavaScript("dragexit", "{}"); + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; + } + + /* + + // TODO(@heapwolf): refactor the legacy native multi-file drag-drop stuff + + if (draggablePayload.size() == 0) { + return; + } + + const auto x = location.x; + const auto y = location.y; + const auto significantMoveX = (lastX - x) > 6 || (x - lastX) > 6; + const auto significantMoveY = (lastY - y) > 6 || (y - lastY) > 6; + + if (significantMoveX || significantMoveY) { + const auto data = JSON::Object::Entries { + {"count", (unsigned int) draggablePayload.size()}, + {"x", x}, + {"y", y} + }; + + const auto json = JSON::Object { data }; + const auto payload = getEmitToRenderProcessJavaScript("drag", json.str()); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; + } + + if (NSPointInRect(location, self.frame)) { + return; + } + + const auto pasteboard = [NSPasteboard pasteboardWithName: NSPasteboardNameDrag]; + const auto dragItems = [NSMutableArray new]; + const auto iconSize = NSMakeSize(32, 32); // according to documentation + + [pasteboard declareTypes: @[(NSString*) kPasteboardTypeFileURLPromise] owner:self]; + + auto dragPosition = [self convertPoint: event.locationInWindow fromView: nil]; + dragPosition.x -= 16; + dragPosition.y -= 16; + + NSRect imageLocation; + imageLocation.origin = dragPosition; + imageLocation.size = iconSize; + + for (const auto& file : draggablePayload) { + const auto url = [NSURL fileURLWithPath: @(file.c_str())]; + const auto icon = [NSWorkspace.sharedWorkspace iconForContentType: UTTypeURL]; + + NSArray* (^providerBlock)() = ^NSArray* () { + const auto component = [ + [NSDraggingImageComponent.alloc initWithKey: NSDraggingImageComponentIconKey + ] retain]; + + component.frame = NSMakeRect(0, 0, iconSize.width, iconSize.height); + component.contents = icon; + return @[component]; + }; + + auto provider = [NSFilePromiseProvider.alloc initWithFileType: @"public.url" delegate: self]; + + [provider setUserInfo: @(file.c_str())]; + + auto dragItem = [NSDraggingItem.alloc initWithPasteboardWriter: provider]; + + dragItem.draggingFrame = NSMakeRect( + dragPosition.x, + dragPosition.y, + iconSize.width, + iconSize.height + ); + + dragItem.imageComponentsProvider = providerBlock; + [dragItems addObject: dragItem]; + } + + auto session = [self + beginDraggingSessionWithItems: dragItems + event: event + source: self + ]; + + session.draggingFormation = NSDraggingFormationPile; + draggablePayload.clear(); + */ +} + +- (NSDragOperation) draggingSession: (NSDraggingSession*) session + sourceOperationMaskForDraggingContext: (NSDraggingContext) context +{ + return NSDragOperationGeneric; +} + +- (void) filePromiseProvider: (NSFilePromiseProvider*) filePromiseProvider + writePromiseToURL: (NSURL*) url + completionHandler: (void (^)(NSError *errorOrNil)) completionHandler +{ + const auto dest = String(url.path.UTF8String); + const auto src = String([filePromiseProvider.userInfo UTF8String]); + const auto data = [@"" dataUsingEncoding: NSUTF8StringEncoding]; + + [data writeToURL: url atomically: YES]; + + const auto json = JSON::Object { + JSON::Object::Entries { + {"src", src}, + {"dest", dest} + } + }; + + const auto payload = getEmitToRenderProcessJavaScript("dropout", json.str()); + + [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; + + completionHandler(nil); +} + +- (NSString*) filePromiseProvider: (NSFilePromiseProvider*) filePromiseProvider + fileNameForType: (NSString*) fileType +{ + const auto id = rand64(); + const auto filename = std::to_string(id) + ".download"; + return @(filename.c_str()); +} + +- (void) webView: (WKWebView*) webView + runOpenPanelWithParameters: (WKOpenPanelParameters*) parameters + initiatedByFrame: (WKFrameInfo*) frame + completionHandler: (void (^)(NSArray<NSURL*>*URLs)) completionHandler +{ + const auto acceptedFileExtensions = parameters._acceptedFileExtensions; + const auto acceptedMIMETypes = parameters._acceptedMIMETypes; + const auto window = (Window*) objc_getAssociatedObject(self, "window"); + StringStream contentTypesSpec; + + for (NSString* acceptedMIMEType in acceptedMIMETypes) { + contentTypesSpec << acceptedMIMEType.UTF8String << "|"; + } + + if (acceptedFileExtensions.count > 0) { + contentTypesSpec << "*/*:"; + const auto count = acceptedFileExtensions.count; + int seen = 0; + for (NSString* acceptedFileExtension in acceptedFileExtensions) { + const auto string = String(acceptedFileExtension.UTF8String); + + if (!string.starts_with(".")) { + contentTypesSpec << "."; + } + + contentTypesSpec << string; + if (++seen < count) { + contentTypesSpec << ","; + } + } + } + + auto contentTypes = trim(contentTypesSpec.str()); + + if (contentTypes.size() == 0) { + contentTypes = "*/*"; + } + + if (contentTypes.ends_with("|")) { + contentTypes = contentTypes.substr(0, contentTypes.size() - 1); + } + + const auto options = Dialog::FileSystemPickerOptions { + .directories = false, + .multiple = parameters.allowsMultipleSelection ? true : false, + .contentTypes = contentTypes, + .defaultName = "", + .defaultPath = "", + .title = "Choose a File" + }; + + Dialog dialog(window); + const auto success = dialog.showOpenFilePicker(options, [=](const auto results) { + if (results.size() == 0) { + completionHandler(nullptr); + return; + } + + auto urls = [NSMutableArray array]; + + for (const auto& result : results) { + [urls addObject: [NSURL URLWithString: @(result.c_str())]]; + } + + completionHandler(urls); + }); + + if (!success) { + completionHandler(nullptr); + return; + } +} +#endif + +#if (!SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR) || (SOCKET_RUNTIME_PLATFORM_IOS && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_15) +- (void) webView: (WKWebView*) webView + requestDeviceOrientationAndMotionPermissionForOrigin: (WKSecurityOrigin*) origin + initiatedByFrame: (WKFrameInfo*) frame + decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler { + static auto userConfig = getUserConfig(); + + if (userConfig["permissions_allow_device_orientation"] == "false") { + decisionHandler(WKPermissionDecisionDeny); + return; + } + + decisionHandler(WKPermissionDecisionGrant); +} + +- (void) webView: (WKWebView*) webView + requestMediaCapturePermissionForOrigin: (WKSecurityOrigin*) origin + initiatedByFrame: (WKFrameInfo*) frame + type: (WKMediaCaptureType) type + decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler { + static auto userConfig = getUserConfig(); + + if (userConfig["permissions_allow_user_media"] == "false") { + decisionHandler(WKPermissionDecisionDeny); + return; + } + + if (type == WKMediaCaptureTypeCameraAndMicrophone) { + if ( + userConfig["permissions_allow_camera"] == "false" || + userConfig["permissions_allow_microphone"] == "false" + ) { + decisionHandler(WKPermissionDecisionDeny); + return; + } + } + + if ( + type == WKMediaCaptureTypeCamera && + userConfig["permissions_allow_camera"] == "false" + ) { + decisionHandler(WKPermissionDecisionDeny); + return; + } + + if ( + type == WKMediaCaptureTypeMicrophone && + userConfig["permissions_allow_microphone"] == "false" + ) { + decisionHandler(WKPermissionDecisionDeny); + return; + } + + decisionHandler(WKPermissionDecisionGrant); +} +#endif + +- (void) webView: (SSCWebView*) webview + runJavaScriptAlertPanelWithMessage: (NSString*) message + initiatedByFrame: (WKFrameInfo*) frame + completionHandler: (void (^)(void)) completionHandler +{ + static auto userConfig = getUserConfig(); + const auto title = userConfig.contains("window_alert_title") + ? userConfig["window_alert_title"] + : userConfig["meta_title"] + ":"; + +#if SOCKET_RUNTIME_PLATFORM_IOS + const auto alert = [UIAlertController + alertControllerWithTitle: @(title.c_str()) + message: message + preferredStyle: UIAlertControllerStyleAlert + ]; + + const auto ok = [UIAlertAction + actionWithTitle: @"OK" + style: UIAlertActionStyleDefault + handler: ^(UIAlertAction * action) { + completionHandler(); + }]; + + [alert addAction: ok]; + + [webview.window.rootViewController + presentViewController: alert + animated: YES + completion: nil + ]; +#else + const auto alert = [NSAlert new]; + [alert setMessageText: @(title.c_str())]; + [alert setInformativeText: message]; + [alert addButtonWithTitle: @"OK"]; + [alert runModal]; + completionHandler(); +#if !__has_feature(objc_arc) + [alert release]; +#endif +#endif +} + +#if SOCKET_RUNTIME_PLATFORM_IOS +- (void) traitCollectionDidChange: (UITraitCollection*) previousTraitCollection { + [super traitCollectionDidChange:previousTraitCollection]; + + static auto userConfig = getUserConfig(); + const auto window = (Window*) objc_getAssociatedObject(self, "window"); + + UIUserInterfaceStyle interfaceStyle = window->window.traitCollection.userInterfaceStyle; + + auto hasBackgroundDark = userConfig.count("window_background_color_dark") > 0; + auto hasBackgroundLight = userConfig.count("window_background_color_light") > 0; + + if (interfaceStyle == UIUserInterfaceStyleDark && hasBackgroundDark) { + window->setBackgroundColor(userConfig["window_background_color_dark"]); + } else if (hasBackgroundLight) { + window->setBackgroundColor(userConfig["window_background_color_light"]); + } +} +#endif + +- (void) webView: (WKWebView*) webview + runJavaScriptConfirmPanelWithMessage: (NSString*) message + initiatedByFrame: (WKFrameInfo*) frame + completionHandler: (void (^)(BOOL result)) completionHandler +{ + static auto userConfig = getUserConfig(); + const auto title = userConfig.contains("window_alert_title") + ? userConfig["window_alert_title"] + : userConfig["meta_title"] + ":"; + +#if SOCKET_RUNTIME_PLATFORM_IOS + const auto alert = [UIAlertController + alertControllerWithTitle: @(title.c_str()) + message: message + preferredStyle: UIAlertControllerStyleAlert + ]; + + const auto ok = [UIAlertAction + actionWithTitle: @"OK" + style: UIAlertActionStyleDefault + handler: ^(UIAlertAction * action) { + completionHandler(YES); + }]; + + const auto cancel = [UIAlertAction + actionWithTitle: @"Cancel" + style: UIAlertActionStyleDefault + handler: ^(UIAlertAction * action) { + completionHandler(NO); + }]; + + [alert addAction: ok]; + [alert addAction: cancel]; + + [webview.window.rootViewController + presentViewController: alert + animated: YES + completion: nil + ]; +#else + const auto alert = [NSAlert new]; + [alert setMessageText: @(title.c_str())]; + [alert setInformativeText: message]; + [alert addButtonWithTitle: @"OK"]; + [alert addButtonWithTitle: @"Cancel"]; + completionHandler([alert runModal] == NSAlertFirstButtonReturn); +#if !__has_feature(objc_arc) + [alert release]; +#endif +#endif +} +@end + +#if SOCKET_RUNTIME_PLATFORM_IOS +@implementation SSCWebViewController +@end +#endif +#endif diff --git a/src/core/webview.hh b/src/core/webview.hh new file mode 100644 index 0000000000..f20f95e98f --- /dev/null +++ b/src/core/webview.hh @@ -0,0 +1,96 @@ +#ifndef SOCKET_RUNTIME_WINDOW_WEBVIEW_H +#define SOCKET_RUNTIME_WINDOW_WEBVIEW_H + +#include "../platform/platform.hh" + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@interface SSCWebView : +#if SOCKET_RUNTIME_PLATFORM_IOS + WKWebView<WKUIDelegate> + @property (strong, nonatomic) NSLayoutConstraint *keyboardHeightConstraint; +#else + WKWebView< + WKUIDelegate, + NSDraggingDestination, + NSFilePromiseProviderDelegate, + NSDraggingSource + > + + @property (nonatomic) NSPoint initialWindowPos; + @property (nonatomic) CGFloat contentHeight; + @property (nonatomic) CGFloat radius; + @property (nonatomic) CGFloat margin; + @property (nonatomic) BOOL shouldDrag; +#endif + +#if SOCKET_RUNTIME_PLATFORM_MACOS +- (instancetype) initWithFrame: (NSRect) frameRect + configuration: (WKWebViewConfiguration*) configuration + radius: (CGFloat) radius + margin: (CGFloat) margin; + + - (NSDragOperation) draggingSession: (NSDraggingSession *) session + sourceOperationMaskForDraggingContext: (NSDraggingContext) context; + + - (void) webView: (WKWebView*) webView + runOpenPanelWithParameters: (WKOpenPanelParameters*) parameters + initiatedByFrame: (WKFrameInfo*) frame + completionHandler: (void (^)(NSArray<NSURL*>*)) completionHandler; +#endif + +#if SOCKET_RUNTIME_PLATFORM_MACOS || (SOCKET_RUNTIME_PLATFORM_IOS && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_15) + + - (void) webView: (WKWebView*) webView + requestDeviceOrientationAndMotionPermissionForOrigin: (WKSecurityOrigin*) origin + initiatedByFrame: (WKFrameInfo*) frame + decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler; + + - (void) webView: (WKWebView*) webView + requestMediaCapturePermissionForOrigin: (WKSecurityOrigin*) origin + initiatedByFrame: (WKFrameInfo*) frame + type: (WKMediaCaptureType) type + decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler; +#endif + + - (void) webView: (WKWebView*) webView + runJavaScriptAlertPanelWithMessage: (NSString*) message + initiatedByFrame: (WKFrameInfo*) frame + completionHandler: (void (^)(void)) completionHandler; + + - (void) webView: (WKWebView*) webView + runJavaScriptConfirmPanelWithMessage: (NSString*) message + initiatedByFrame: (WKFrameInfo*) frame + completionHandler: (void (^)(BOOL result)) completionHandler; +@end + +#if SOCKET_RUNTIME_PLATFORM_IOS +@interface SSCWebViewController : UIViewController + @property (nonatomic, strong) SSCWebView* webview; +@end +#endif +#endif + +namespace SSC { +#if SOCKET_RUNTIME_PLATFORM_ANDROID + class CoreAndroidWebView; + class CoreAndroidWebViewSettings; +#endif + +#if SOCKET_RUNTIME_PLATFORM_APPLE + using WebView = SSCWebView; + using WebViewSettings = WKWebViewConfiguration; +#elif SOCKET_RUNTIME_PLATFORM_LINUX && !SOCKET_RUNTIME_DESKTOP_EXTENSION + using WebView = WebKitWebView; + using WebViewSettings = WebKitSettings; +#elif SOCKET_RUNTIME_PLATFORM_WINDOWS + using WebView = ICoreWebView2; + using WebViewSettings = Microsoft::WRL::ComPtr<CoreWebView2EnvironmentOptions>; +#elif SOCKET_RUNTIME_PLATFORM_ANDROID + using WebView = CoreAndroidWebView; + using WebViewSettings = CoreAndroidWebViewSettings; +#else + struct WebView; + struct WebViewSettings; +#endif +} +#endif diff --git a/src/core/webview.kt b/src/core/webview.kt new file mode 100644 index 0000000000..52efc25fb8 --- /dev/null +++ b/src/core/webview.kt @@ -0,0 +1,21 @@ +// vim: set sw=2: +package socket.runtime.core + +import android.content.Context +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse + +/** + * @see https://developer.android.com/reference/kotlin/android/webkit/WebView + */ +open class WebView (context: android.content.Context) : android.webkit.WebView(context) + +/** + * @see https://developer.android.com/reference/kotlin/android/webkit/WebChromeClient + */ +open class WebChromeClient : android.webkit.WebChromeClient() {} + +/** + * @see https://developer.android.com/reference/kotlin/android/webkit/WebViewClient + */ +open class WebViewClient : android.webkit.WebViewClient() {} diff --git a/src/desktop/extension.hh b/src/desktop/extension.hh new file mode 100644 index 0000000000..16b8b301ca --- /dev/null +++ b/src/desktop/extension.hh @@ -0,0 +1,17 @@ +#ifndef SOCKET_RUNTIME_DESKTOP_EXTENSION_H +#define SOCKET_RUNTIME_DESKTOP_EXTENSION_H + +#include "../platform/platform.hh" + +namespace SSC { + struct WebExtensionContext { + struct ConfigData { + char* bytes = nullptr; + size_t size = 0; + }; + + ConfigData config; + }; +} + +#endif diff --git a/src/desktop/extension/linux.cc b/src/desktop/extension/linux.cc new file mode 100644 index 0000000000..b30e329776 --- /dev/null +++ b/src/desktop/extension/linux.cc @@ -0,0 +1,315 @@ +#define SOCKET_RUNTIME_DESKTOP_EXTENSION 1 +#include "../../platform/platform.hh" +#include "../../core/resource.hh" +#include "../../core/debug.hh" +#include "../../core/trace.hh" +#include "../../core/url.hh" +#include "../../app/app.hh" + +#include "../extension.hh" + +using namespace SSC; + +#if SOCKET_RUNTIME_PLATFORM_LINUX +#if defined(__cplusplus) +extern "C" { +#endif + static SharedPointer<IPC::Bridge> sharedBridge = nullptr; + static bool isMainApplicationDebugEnabled = false; + static WebExtensionContext context; + static Mutex sharedMutex; + + static SharedPointer<IPC::Bridge> getSharedBridge (JSCContext* context) { + static auto app = App::sharedApplication(); + Lock lock(sharedMutex); + + if (sharedBridge == nullptr) { + g_object_ref(context); + auto options = IPC::Bridge::Options(app->userConfig); + sharedBridge = std::make_shared<IPC::Bridge>(app->core, options); + sharedBridge->dispatchFunction = [](auto callback) { + callback(); + }; + sharedBridge->evaluateJavaScriptFunction = [context] (const auto source) { + app->dispatch([=] () { + auto _ = jsc_context_evaluate(context, source.c_str(), source.size()); + }); + }; + sharedBridge->init(); + } + + return sharedBridge; + } + + static void onMessageResolver ( + JSCValue* resolve, + JSCValue* reject, + IPC::Message* message + ) { + auto context = jsc_value_get_context(resolve); + auto bridge = getSharedBridge(context); + auto app = App::sharedApplication(); + + auto routed = bridge->route(message->str(), message->buffer.bytes, message->buffer.size, [=](auto result) { + app->dispatch([=] () { + if (result.post.body != nullptr) { + auto array = jsc_value_new_typed_array( + context, + JSC_TYPED_ARRAY_UINT8, + result.post.length + ); + + gsize size = 0; + auto bytes = jsc_value_typed_array_get_data(array, &size); + memcpy(bytes, result.post.body.get(), size); + + const auto _ = jsc_value_function_call( + resolve, + JSC_TYPE_VALUE, + array, + G_TYPE_NONE + ); + } else { + const auto json = result.json().str(); + if (json.size() > 0) { + auto _ = jsc_value_function_call( + resolve, + JSC_TYPE_VALUE, + jsc_value_new_string(context, json.c_str()), + G_TYPE_NONE + ); + } + } + + g_object_unref(context); + g_object_unref(resolve); + g_object_unref(reject); + }); + }); + + if (routed) { + g_object_ref(context); + g_object_ref(resolve); + g_object_ref(reject); + } else { + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"message", "Not found"}, + {"type", "NotFoundError"}, + {"source", message->name} + }} + }; + + const auto _ = jsc_value_function_call( + resolve, + JSC_TYPE_VALUE, + jsc_value_new_string(context, JSON::Object(json).str().c_str()), + G_TYPE_NONE + ); + } + + delete message; + } + + static JSCValue* onMessage (const char* source, JSCValue* value, gpointer userData) { + auto context = reinterpret_cast<JSCContext*>(userData); + auto Promise = jsc_context_get_value(context, "Promise"); + auto message = new IPC::Message(source); + + if (jsc_value_is_typed_array(value)) { + auto bytes = jsc_value_typed_array_get_data(value, &message->buffer.size); + message->buffer.bytes = std::make_shared<char[]>(message->buffer.size); + memcpy( + message->buffer.bytes.get(), + bytes, + message->buffer.size + ); + } + + if (message->get("__sync__") == "true") { + auto bridge = getSharedBridge(context); + auto app = App::sharedApplication(); + auto semaphore = new std::binary_semaphore{0}; + + IPC::Result returnResult; + + auto routed = bridge->route( + message->str(), + message->buffer.bytes, + message->buffer.size, + [&returnResult, &semaphore] (auto result) mutable { + returnResult = std::move(result); + semaphore->release(); + } + ); + + semaphore->acquire(); + + delete semaphore; + delete message; + + if (routed) { + if (returnResult.post.body != nullptr) { + auto array = jsc_value_new_typed_array( + context, + JSC_TYPED_ARRAY_UINT8, + returnResult.post.length + ); + + gsize size = 0; + auto bytes = jsc_value_typed_array_get_data(array, &size); + memcpy(bytes, returnResult.post.body.get(), size); + return array; + } else { + auto json = returnResult.json().str(); + if (json.size() > 0) { + return jsc_value_new_string(context, json.c_str()); + } + } + } + + const auto json = JSON::Object::Entries { + {"err", JSON::Object::Entries { + {"message", "Not found"}, + {"type", "NotFoundError"}, + {"source", source} + }} + }; + + return jsc_value_new_string(context, JSON::Object(json).str().c_str()); + } + + auto resolver = jsc_value_new_function( + context, + nullptr, + G_CALLBACK(onMessageResolver), + message, + nullptr, + G_TYPE_NONE, + 2, + JSC_TYPE_VALUE, + JSC_TYPE_VALUE + ); + + auto promise = jsc_value_constructor_call( + Promise, + JSC_TYPE_VALUE, + resolver, + G_TYPE_NONE + ); + + g_object_unref(Promise); + g_object_unref(resolver); + return promise; + } + + static void onDocumentLoaded ( + WebKitWebPage* page, + gpointer userData + ) { + auto frame = webkit_web_page_get_main_frame(page); + auto context = webkit_frame_get_js_context(frame); + auto __global_ipc_extension_handler = jsc_value_new_function( + context, + "__global_ipc_extension_handler", + G_CALLBACK(onMessage), + context, + nullptr, + JSC_TYPE_VALUE, + 2, + G_TYPE_STRING, + JSC_TYPE_VALUE + ); + + jsc_context_set_value( + context, + "__global_ipc_extension_handler", + __global_ipc_extension_handler + ); + } + + static void onPageCreated ( + WebKitWebExtension* extension, + WebKitWebPage* page, + gpointer userData + ) { + auto userConfig = getUserConfig(); + g_signal_connect( + page, + "document-loaded", + G_CALLBACK(onDocumentLoaded), + nullptr + ); + } + + G_MODULE_EXPORT void webkit_web_extension_initialize_with_user_data ( + WebKitWebExtension* extension, + const GVariant* userData + ) { + g_signal_connect( + extension, + "page-created", + G_CALLBACK(onPageCreated), + nullptr + ); + + if (!context.config.bytes) { + context.config.size = g_variant_get_size(const_cast<GVariant*>(userData)); + if (context.config.size) { + context.config.bytes = reinterpret_cast<char*>(new unsigned char[context.config.size]{0}); + + memcpy( + context.config.bytes, + g_variant_get_data(const_cast<GVariant*>(userData)), + context.config.size + ); + } + } + + Core::Options options; + options.dedicatedLoopThread = true; + auto userConfig = getUserConfig(); + auto cwd = userConfig["web-process-extension_cwd"]; + + if (cwd.size() > 0) { + setcwd(cwd); + uv_chdir(cwd.c_str()); + } + + static App app(App::DEFAULT_INSTANCE_ID, std::move(std::make_shared<Core>(options))); + } + + const unsigned char* socket_runtime_init_get_user_config_bytes () { + return reinterpret_cast<const unsigned char*>(context.config.bytes); + } + + unsigned int socket_runtime_init_get_user_config_bytes_size () { + return context.config.size; + } + + bool socket_runtime_init_is_debug_enabled () { + return isMainApplicationDebugEnabled; + } + + const char* socket_runtime_init_get_dev_host () { + auto userConfig = getUserConfig(); + if (userConfig.contains("web-process-extension_host")) { + return userConfig["web-process-extension_host"].c_str(); + } + return ""; + } + + int socket_runtime_init_get_dev_port () { + auto userConfig = getUserConfig(); + if (userConfig.contains("web-process-extension_port")) { + try { + return std::stoi(userConfig["web-process-extension_port"]);; + } catch (...) {} + } + + return 0; + } +#if defined(__cplusplus) +} +#endif +#endif diff --git a/src/desktop/main.cc b/src/desktop/main.cc index 00ae007012..26026c99ef 100644 --- a/src/desktop/main.cc +++ b/src/desktop/main.cc @@ -2,16 +2,17 @@ #include "../cli/cli.hh" #include "../ipc/ipc.hh" #include "../core/core.hh" -#include "../process/process.hh" #include "../window/window.hh" -#if defined(__linux__) +#if SOCKET_RUNTIME_PLATFORM_LINUX #include <dbus/dbus.h> #include <fcntl.h> + +#include "extension.hh" #endif #include <iostream> -#include <ostream> +#include <chrono> #include <regex> #include <span> @@ -19,82 +20,121 @@ // A cross platform MAIN macro that // magically gives us argc and argv. // -#if defined(_WIN32) -#define MAIN \ - static const int argc = __argc; \ - static char** argv = __argv; \ - int CALLBACK WinMain ( \ - _In_ HINSTANCE instanceId, \ - _In_ HINSTANCE hPrevInstance, \ - _In_ LPSTR lpCmdLine, \ - _In_ int nCmdShow) +#if SOCKET_RUNTIME_PLATFORM_WINDOWS +#define MAIN \ + static const int argc = __argc; \ + static char** argv = __argv; \ + int CALLBACK WinMain ( \ + _In_ HINSTANCE instanceId, \ + _In_ HINSTANCE hPrevInstance, \ + _In_ LPSTR lpCmdLine, \ + _In_ int nCmdShow \ + ) #else -#define MAIN \ - const int instanceId = 0; \ +#define MAIN \ + static const int instanceId = 0; \ int main (int argc, char** argv) #endif -#if defined(__APPLE__) +#if SOCKET_RUNTIME_PLATFORM_APPLE #include <os/log.h> #endif #define InvalidWindowIndexError(index) \ - SSC::String("Invalid index given for window: ") + std::to_string(index) + String("Invalid index given for window: ") + std::to_string(index) + +static void installSignalHandler (int signum, void (*handler)(int)) { +#if SOCKET_RUNTIME_PLATFORM_LINUX + struct sigaction action; + sigemptyset(&action.sa_mask); + action.sa_handler = handler; + action.sa_flags = SA_NODEFER; + sigaction(signum, &action, NULL); +#else + signal(signum, handler); +#endif +} -using namespace SSC; -static App *app_ptr = nullptr; +using namespace SSC; -std::function<void(int)> shutdownHandler; -void signalHandler (int signal) { - if (shutdownHandler != nullptr) { - shutdownHandler(signal); - } +static inline String readFile (const Path& path) { + static String buffer; + auto stream = InputFileStream(path.string()); + auto begin = InputStreamBufferIterator<char>(stream); + auto end = InputStreamBufferIterator<char>(); + buffer.assign(begin, end); + stream.close(); + return buffer; } -SSC::String getNavigationError (const String &cwd, const String &value) { - auto url = value.substr(7); +static inline void writeFile (const Path& path, const String& source) { + auto stream = OutputFileStream(path.string()); + stream << source; + stream.close(); +} - if (!value.starts_with("socket://") && !value.starts_with("socket://")) { - return SSC::String("only socket:// protocol is allowed for the file navigation. Got url ") + value; - } +static Function<void(int)> shutdownHandler; + +// propagate signals to the default window which will use the +// 'socket.runtime.signal' broadcast channel to propagate to all +// other windows who may be subscribers +static void defaultWindowSignalHandler (int signal) { + auto app = App::sharedApplication(); + if (app != nullptr && app->core->platform.wasFirstDOMContentLoadedEventDispatched) { + app->dispatch([=] () { + auto defaultWindow = app->windowManager.getWindow(0); + if (defaultWindow != nullptr) { + if (defaultWindow->status < WindowManager::WindowStatus::WINDOW_CLOSING) { + const auto json = JSON::Object { + JSON::Object::Entries { + {"signal", signal} + } + }; - if (url.empty()) { - return SSC::String("empty url"); + defaultWindow->eval(getEmitToRenderProcessJavaScript("signal", json.str())); + } + } + }); } - - return SSC::String(""); } -inline const Vector<int> splitToInts (const String& s, const char& c) { - Vector<int> result; - String token; - std::istringstream ss(s); +void signalHandler (int signum) { + static auto app = App::sharedApplication(); + static auto userConfig = getUserConfig(); + static const auto signalsDisabled = userConfig["application_signals"] == "false"; + static const auto signals = parseStringList(userConfig["application_signals"]); + String name; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + name = String(sys_signame[signum]); + #elif SOCKET_RUNTIME_PLATFORM_LINUX + name = strsignal(signum); + #endif - while (std::getline(ss, token, c)) { - result.push_back(std::stoi(token)); + if (!signalsDisabled || std::find(signals.begin(), signals.end(), name) != signals.end()) { + app->dispatch([signum]() { defaultWindowSignalHandler(signum); }); } - return result; -} -#if defined(__linux__) -static void handleApplicationURLEvent (const String url) { - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ - "url", url - }}; + if (signum == SIGTERM || signum == SIGINT) { + signal(signum, SIG_DFL); + if (shutdownHandler != nullptr) { + app->dispatch([signum] () { + shutdownHandler(signum); + }); + } else { + raise(signum); + } + } +} - if (app_ptr != nullptr && app_ptr->windowManager != nullptr) { - for (auto window : app_ptr->windowManager->windows) { +#if SOCKET_RUNTIME_PLATFORM_LINUX +static void handleApplicationURLEvent (const String& url) { + auto app = App::sharedApplication(); + if (app != nullptr && url.size() > 0) { + for (auto window : app->windowManager.windows) { if (window != nullptr) { - if (window->index == 0) { - gtk_widget_show_all(GTK_WIDGET(window->window)); - gtk_widget_grab_focus(GTK_WIDGET(window->webview)); - gtk_widget_grab_focus(GTK_WIDGET(window->window)); - gtk_window_activate_focus(GTK_WINDOW(window->window)); - gtk_window_present(GTK_WINDOW(window->window)); - } - - window->bridge->router.emit("applicationurl", json.str()); + window->handleApplicationURL(url); } } } @@ -107,7 +147,9 @@ static void onGTKApplicationActivation ( const gchar* hint, gpointer userData ) { - handleApplicationURLEvent(String(hint)); + if (hint != nullptr) { + handleApplicationURLEvent(String(hint)); + } } static DBusHandlerResult onDBusMessage ( @@ -115,7 +157,7 @@ static DBusHandlerResult onDBusMessage ( DBusMessage* message, void* userData ) { - static auto userConfig = SSC::getUserConfig(); + static auto userConfig = getUserConfig(); static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; static auto dbusBundleIdentifier = replace(bundleIdentifier, "-", "_"); @@ -132,9 +174,9 @@ static DBusHandlerResult onDBusMessage ( return DBUS_HANDLER_RESULT_HANDLED; } -#elif defined(_WIN32) +#elif SOCKET_RUNTIME_PLATFORM_WINDOWS BOOL registerWindowsURISchemeInRegistry () { - static auto userConfig = SSC::getUserConfig(); + static auto userConfig = getUserConfig(); HKEY shellKey; HKEY key; LONG result; @@ -205,39 +247,31 @@ BOOL registerWindowsURISchemeInRegistry () { // which on windows is hInstance, on mac and linux this is just an int. // MAIN { - -#if defined(__linux__) +#if SOCKET_RUNTIME_PLATFORM_LINUX + // use 'SIGPWR' instead of the default 'SIGUSR1' handler + // see https://github.com/WebKit/WebKit/blob/2fd8f81aac4e867ffe107c0e1b3e34b1628c0953/Source/WTF/wtf/posix/ThreadingPOSIX.cpp#L185 + Env::set("JSC_SIGNAL_FOR_GC", "30"); gtk_init(&argc, &argv); #endif // Singletons should be static to remove some possible race conditions in // their instantiation and destruction. static App app(instanceId); - static WindowManager windowManager(app); - static auto userConfig = SSC::getUserConfig(); - - // TODO(trevnorris): Since App is a singleton, follow the CppCoreGuidelines - // better in how it's handled in the future. - // For now make a pointer reference since there is some member variable name - // collision in the call to shutdownHandler when it's being called from the - // windowManager instance. - app_ptr = &app; + static auto userConfig = getUserConfig(); - app.setWindowManager(&windowManager); - const String _host = getDevHost(); - const auto _port = getDevPort(); + const String devHost = getDevHost(); + const auto devPort = getDevPort(); - const SSC::String OK_STATE = "0"; - const SSC::String ERROR_STATE = "1"; - const SSC::String EMPTY_SEQ = SSC::String(""); + const String OK_STATE = "0"; + const String ERROR_STATE = "1"; auto cwd = app.getcwd(); - app.appData = userConfig; + app.userConfig = userConfig; - SSC::String suffix = ""; + String suffix = ""; - SSC::StringStream argvArray; - SSC::StringStream argvForward; + Vector<String> argvArray; + StringStream argvForward; bool isCommandMode = false; bool isReadingStdin = false; @@ -252,24 +286,44 @@ MAIN { auto bundleIdentifier = userConfig["meta_bundle_identifier"]; -#if defined(__linux__) +#if SOCKET_RUNTIME_PLATFORM_LINUX static const auto TMPDIR = Env::get("TMPDIR", "/tmp"); - static const auto appInstanceLock = fs::path(TMPDIR) / (bundleIdentifier + ".lock"); + static const auto appInstanceLock = Path(TMPDIR) / (bundleIdentifier + ".lock"); + static const auto appInstancePID = Path(TMPDIR) / (bundleIdentifier + ".pid"); + static const auto appProtocol = userConfig["meta_application_protocol"]; + + const auto existingPIDValue = readFile(appInstancePID); + if (existingPIDValue.size() > 0) { + try { + const pid_t pid = std::stoi(existingPIDValue); + if (kill(pid, 0) != 0) { + throw std::runtime_error(""); + } + } catch (...) { + unlink(appInstanceLock.c_str()); + unlink(appInstancePID.c_str()); + } + } + + // lock + pid files auto appInstanceLockFd = open(appInstanceLock.c_str(), O_CREAT | O_EXCL, 0600); - auto appProtocol = userConfig["meta_application_protocol"]; + + // dbus auto dbusError = DBusError {}; dbus_error_init(&dbusError); - auto connection = dbus_bus_get(DBUS_BUS_SESSION, &dbusError); + static auto connection = dbus_bus_get(DBUS_BUS_SESSION, &dbusError); auto dbusBundleIdentifier = replace(bundleIdentifier, "-", "_"); // instance is running if fd was acquired - if (appInstanceLockFd != -1) { - auto filter = ( + if (appInstanceLockFd > 0) { + const auto pid = getpid(); + const auto filter = ( String("type='method_call',") + String("interface='") + dbusBundleIdentifier + String("',") + String("member='handleApplicationURLEvent'") ); dbus_bus_add_match(connection, filter.c_str(), &dbusError); + writeFile(appInstancePID, std::to_string(pid)); if (dbus_error_is_set(&dbusError)) { fprintf(stderr, "error: dbus: Failed to add match rule: %s\n", dbusError.message); @@ -297,20 +351,21 @@ MAIN { dbus_connection_add_filter(connection, onDBusMessage, nullptr, nullptr); - static std::function<void()> pollForMessage = [connection]() { - Thread thread([connection] () { - while (dbus_connection_read_write_dispatch(connection, 100)); - app_ptr->dispatch(pollForMessage); + static Function<void()> pollForMessage = []() { + Thread thread([] () { + auto app = App::sharedApplication(); + while (dbus_connection_read_write_dispatch(connection, 256)); + app->dispatch(pollForMessage); }); thread.detach(); }; - app_ptr->dispatch(pollForMessage); + app.dispatch(pollForMessage); if (appProtocol.size() > 0 && argc > 1 && String(argv[1]).starts_with(appProtocol + ":")) { const auto uri = String(argv[1]); - app_ptr->dispatch([uri]() { - app_ptr->core->dispatchEventLoop([uri]() { + app.dispatch([uri]() { + app.core->dispatchEventLoop([uri]() { handleApplicationURLEvent(uri); }); }); @@ -388,7 +443,7 @@ MAIN { g_signal_connect(gtkApp, "activate", G_CALLBACK(onGTKApplicationActivation), NULL); } -#elif defined(_WIN32) +#elif SOCKET_RUNTIME_PLATFORM_WINDOWS HANDLE hMutex = CreateMutex(NULL, TRUE, bundleIdentifier.c_str()); auto lastWindowsError = GetLastError(); auto appProtocol = userConfig["meta_application_protocol"]; @@ -411,8 +466,8 @@ MAIN { reinterpret_cast<LPARAM>(&data) ); } else { - app_ptr->dispatch([hWnd, lpCmdLine]() { - Window::WndProc( + app.dispatch([hWnd, lpCmdLine]() { + app.forwardWindowProcMessage( hWnd, WM_HANDLE_DEEP_LINK, (WPARAM) strlen(lpCmdLine), @@ -436,12 +491,9 @@ MAIN { // isn't the most robust way of doing this. possible a URI-encoded query // string would be more in-line with how everything else works. for (auto const arg : std::span(argv, argc)) { - auto s = SSC::String(arg); + auto s = String(arg); - argvArray - << "'" - << replace(s, "'", "\'") - << (c++ < argc ? "', " : "'"); + argvArray.push_back("'" + replace(s, "'", "\'") + "'"); bool helpRequested = ( (s.find("--help") == 0) || @@ -470,17 +522,11 @@ MAIN { if (s.find("--headless") == 0) { isHeadless = true; + userConfig["build_headless"] = "true"; } // launched from the `ssc` cli - app.fromSSC = s.find("--from-ssc") == 0 ? true : false; - - #ifdef _WIN32 - if (!app.w32ShowConsole && s.find("--w32-console") == 0) { - app.w32ShowConsole = true; - app.ShowConsole(); - } - #endif + app.wasLaunchedFromCli = s.find("--from-ssc") == 0 ? true : false; if (s.find("--test") == 0) { suffix = "-test"; @@ -498,26 +544,26 @@ MAIN { } else if (versionRequested) { argvForward << " " << "version --warn-arg-usage=" << s; } else if (c > 1 || isCommandMode) { - argvForward << " " << SSC::String(arg); + argvForward << " " << String(arg); } } if (isDebugEnabled()) { - app.appData["build_name"] += "-dev"; + app.userConfig["build_name"] += "-dev"; } - app.appData["build_name"] += suffix; + app.userConfig["build_name"] += suffix; - argvForward << " --ssc-version=v" << SSC::VERSION_STRING; - argvForward << " --version=v" << app.appData["meta_version"]; - argvForward << " --name=" << app.appData["build_name"]; + argvForward << " --ssc-version=v" << VERSION_STRING; + argvForward << " --version=v" << app.userConfig["meta_version"]; + argvForward << " --name=" << app.userConfig["build_name"]; if (isDebugEnabled()) { argvForward << " --debug=1"; } - SSC::StringStream env; - for (auto const &envKey : parseStringList(app.appData["build_env"])) { + StringStream env; + for (auto const &envKey : parseStringList(app.userConfig["build_env"])) { auto cleanKey = trim(envKey); if (!Env::has(cleanKey)) { @@ -526,29 +572,29 @@ MAIN { auto envValue = Env::get(cleanKey.c_str()); - env << SSC::String( + env << String( cleanKey + "=" + encodeURIComponent(envValue) + "&" ); } - SSC::String cmd; + String cmd; if (platform.os == "win32") { - cmd = app.appData["win_cmd"]; + cmd = app.userConfig["win_cmd"]; } else { - cmd = app.appData[platform.os + "_cmd"]; + cmd = app.userConfig[platform.os + "_cmd"]; } if (cmd[0] == '.') { auto index = cmd.find_first_of('.'); auto executable = cmd.substr(0, index); - auto absPath = fs::path(cwd) / fs::path(executable); + auto absPath = Path(cwd) / Path(executable); cmd = absPath.string() + cmd.substr(index); } static Process* process = nullptr; - static std::function<void(bool)> createProcess; + static Function<void(bool)> createProcess; - auto killProcess = [&](Process* processToKill) { + auto killProcess = [](Process* processToKill) { if (processToKill != nullptr) { processToKill->kill(); processToKill->wait(); @@ -561,7 +607,7 @@ MAIN { } }; - auto createProcessTemplate = [&]<class... Args>(Args... args) { + auto createProcessTemplate = [killProcess]<class... Args>(Args... args) { return [=](bool force) { if (process != nullptr && force) { killProcess(process); @@ -577,7 +623,7 @@ MAIN { cmd, argvForward.str(), cwd, - [&](SSC::String const &out) { + [&exitCode](String const &out) mutable { IPC::Message message(out); if (message.name == "exit") { @@ -587,8 +633,8 @@ MAIN { IO::write(message.get("value"), false); } }, - [](SSC::String const &out) { IO::write(out, true); }, - [](SSC::String const &code){ exit(std::stoi(code)); } + [](String const &out) { IO::write(out, true); }, + [](String const &code){ exit(std::stoi(code)); } ); if (cmd.size() == 0) { @@ -598,87 +644,84 @@ MAIN { createProcess(true); - shutdownHandler = [&](int signum) { - #if defined(__linux__) + shutdownHandler = [=](int signum) mutable { + msleep(32); + + #if SOCKET_RUNTIME_PLATFORM_LINUX unlink(appInstanceLock.c_str()); #endif if (process != nullptr) { - process->kill(); + process->kill(signum); } exit(signum); }; - #ifndef _WIN32 - signal(SIGHUP, signalHandler); + #if !SOCKET_RUNTIME_PLATFORM_WINDOWS + installSignalHandler(SIGHUP, signalHandler); #endif - signal(SIGINT, signalHandler); - signal(SIGTERM, signalHandler); + installSignalHandler(SIGINT, signalHandler); + installSignalHandler(SIGTERM, signalHandler); return exitCode; } - #if defined(__APPLE__) - static auto SSC_OS_LOG_BUNDLE = os_log_create( + #if SOCKET_RUNTIME_PLATFORM_APPLE + static auto SOCKET_RUNTIME_OS_LOG_BUNDLE = os_log_create( bundleIdentifier.c_str(), - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - "socket.runtime.mobile" - #else - "socket.runtime.desktop" - #endif + "socket.runtime" ); #endif - auto onStdErr = [&](auto err) { - #if defined(__APPLE__) - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", err.c_str()); + const auto onStdErr = [](const auto& output) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + os_log_with_type(SOCKET_RUNTIME_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", output.c_str()); #endif - std::cerr << "\033[31m" + err + "\033[0m"; + std::cerr << "\033[31m" + output + "\033[0m"; - for (auto w : windowManager.windows) { - if (w != nullptr) { - auto window = windowManager.getWindow(w->opts.index); - window->eval(getEmitToRenderProcessJavaScript("process-error", err)); + for (const auto& window : app.windowManager.windows) { + if (window != nullptr) { + window->bridge.emit("process-error", output); } } }; // - // # Backend -> Main + // # "Backend" -> Main // Launch the backend process and connect callbacks to the stdio and stderr pipes. // - auto onStdOut = [&](SSC::String const &out) { - IPC::Message message(out); + const auto onStdOut = [](const auto& output) { + const auto message = IPC::Message(output); if (message.index > 0 && message.name.size() == 0) { // @TODO: print warning return; } - if (message.index > SSC_MAX_WINDOWS) { + if (message.index > SOCKET_RUNTIME_MAX_WINDOWS) { // @TODO: print warning return; } - auto value = message.get("value"); + const auto value = message.value; if (message.name == "stdout") { - #if defined(__APPLE__) + #if SOCKET_RUNTIME_PLATFORM_APPLE dispatch_async(dispatch_get_main_queue(), ^{ - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_DEFAULT, "%{public}s", value.c_str()); + os_log_with_type(SOCKET_RUNTIME_OS_LOG_BUNDLE, OS_LOG_TYPE_DEFAULT, "%{public}s", value.c_str()); }); #endif - std::cout << value; + IO::write(value); return; } if (message.name == "stderr") { - #if defined(__APPLE__) + #if SOCKET_RUNTIME_PLATFORM_APPLE dispatch_async(dispatch_get_main_queue(), ^{ - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", value.c_str()); + os_log_with_type(SOCKET_RUNTIME_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", value.c_str()); }); #endif - std::cerr << "\033[31m" + value + "\033[0m"; + IO::write(value, true); return; } @@ -688,34 +731,28 @@ MAIN { // are parsable commands, try to do something with them, otherwise they are // just stdout and we can write the data to the pipe. // - app.dispatch([&, message, value] { - auto seq = message.get("seq"); - + app.dispatch([&, message, value]() { if (message.name == "send") { - SSC::String script = getEmitToRenderProcessJavaScript( - message.get("event"), - value - ); + const auto event = message.get("event"); if (message.index >= 0) { - auto window = windowManager.getWindow(message.index); + const auto window = app.windowManager.getWindow(message.index); if (window) { - window->eval(script); + window->bridge.emit(event, value); } } else { - for (auto w : windowManager.windows) { - if (w != nullptr) { - auto window = windowManager.getWindow(w->opts.index); - window->eval(script); + for (const auto& window : app.windowManager.windows) { + if (window) { + window->bridge.emit(event, value); } } } return; } - auto window = windowManager.getOrCreateWindow(message.index); + auto window = app.windowManager.getOrCreateWindow(message.index); if (!window) { - auto defaultWindow = windowManager.getWindow(0); + auto defaultWindow = app.windowManager.getWindow(0); if (defaultWindow) { window = defaultWindow; @@ -725,30 +762,22 @@ MAIN { } if (message.name == "heartbeat") { - if (seq.size() > 0) { - auto result = SSC::IPC::Result(message.seq, message, "heartbeat"); - window->resolvePromise(seq, OK_STATE, result.str()); + if (message.seq.size() > 0) { + const auto result = IPC::Result(message.seq, message, "heartbeat"); + window->bridge.send(message.seq, result.json()); } - return; } if (message.name == "resolve") { - window->resolvePromise(seq, message.get("state"), encodeURIComponent(value)); - return; - } - - if (message.name == "config") { - auto key = message.get("key"); - window->resolvePromise(seq, OK_STATE, app.appData[key]); + window->resolvePromise(message.seq, message.get("state"), encodeURIComponent(value)); return; } if (message.name == "process.exit") { - for (auto w : windowManager.windows) { - if (w != nullptr) { - auto window = windowManager.getWindow(w->opts.index); - window->resolvePromise(message.seq, OK_STATE, value); + for (const auto& window : app.windowManager.windows) { + if (window) { + window->bridge.emit("process-exit", message.value); } } return; @@ -762,10 +791,10 @@ MAIN { cwd, onStdOut, onStdErr, - [&](SSC::String const &code) { - for (auto w : windowManager.windows) { + [&](String const &code) { + for (auto w : app.windowManager.windows) { if (w != nullptr) { - auto window = windowManager.getWindow(w->opts.index); + auto window = app.windowManager.getWindow(w->options.index); window->eval(getEmitToRenderProcessJavaScript("backend-exit", code)); } } @@ -780,16 +809,15 @@ MAIN { // callback doesnt need to dispatch because it's already in the // main thread. // - auto onMessage = [&](auto out) { - // debug("onMessage %s", out.c_str()); - IPC::Message message(out, true); + const auto onMessage = [&](const auto& output) { + const auto message = IPC::Message(output, true); - auto window = windowManager.getWindow(message.index); - auto value = message.get("value"); + auto window = app.windowManager.getWindow(message.index); + auto value = message.value; // the window must exist if (!window && message.index >= 0) { - auto defaultWindow = windowManager.getWindow(0); + auto defaultWindow = app.windowManager.getWindow(0); if (defaultWindow) { window = defaultWindow; @@ -797,16 +825,15 @@ MAIN { } if (message.name == "process.open") { - auto seq = message.get("seq"); auto force = message.get("force") == "true" ? true : false; if (cmd.size() > 0) { if (process == nullptr || force) { createProcess(force); process->open(); } - #ifdef _WIN32 + #ifdef SOCKET_RUNTIME_PLATFORM_WINDOWS size_t last_pos = 0; - while ((last_pos = process->path.find('\\', last_pos)) != std::string::npos) { + while ((last_pos = process->path.find('\\', last_pos)) != String::npos) { process->path.replace(last_pos, 1, "\\\\\\\\"); last_pos += 4; } @@ -816,508 +843,27 @@ MAIN { { "argv", process->argv }, { "path", process->path } }; - window->resolvePromise(seq, OK_STATE, json); + window->resolvePromise(message.seq, OK_STATE, json); return; } - window->resolvePromise(seq, ERROR_STATE, SSC::JSON::null); + window->resolvePromise(message.seq, ERROR_STATE, JSON::null); return; } if (message.name == "process.kill") { - auto seq = message.get("seq"); - if (cmd.size() > 0 && process != nullptr) { killProcess(process); } - window->resolvePromise(seq, OK_STATE, SSC::JSON::null); + window->resolvePromise(message.seq, OK_STATE, JSON::null); return; } if (message.name == "process.write") { - auto seq = message.get("seq"); if (cmd.size() > 0 && process != nullptr) { - process->write(out); - } - window->resolvePromise(seq, OK_STATE, SSC::JSON::null); - return; - } - - if (message.name == "window.send") { - const auto event = message.get("event"); - const auto value = message.get("value"); - const auto targetWindowIndex = message.get("targetWindowIndex").size() >= 0 ? std::stoi(message.get("targetWindowIndex")) : -1; - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - const auto currentWindow = windowManager.getWindow(message.index); - if (targetWindow) { - targetWindow->eval(getEmitToRenderProcessJavaScript(event, value)); - } - const auto seq = message.get("seq"); - currentWindow->resolvePromise(seq, OK_STATE, SSC::JSON::null); - return; - } - - if (message.name == "application.exit") { - try { - exitCode = std::stoi(value); - } catch (...) { - } - - #if defined(__APPLE__) - if (app.fromSSC) { - debug("__EXIT_SIGNAL__=%d", exitCode); - CLI::notify(); - } - #endif - window->exit(exitCode); - return; - } - - if (message.name == "application.getScreenSize") { - const auto seq = message.get("seq"); - const auto index = message.index; - const auto window = windowManager.getWindow(index); - if (window) { - const auto screenSize = window->getScreenSize(); - const JSON::Object json = JSON::Object::Entries { - { "width", screenSize.width }, - { "height", screenSize.height } - }; - window->resolvePromise(seq, OK_STATE, json); - } - return; - } - - if (message.name == "application.getWindows") { - const auto index = message.index; - const auto window = windowManager.getWindow(index); - auto indices = splitToInts(value, ','); - if (indices.size() == 0) { - for (auto w : windowManager.windows) { - if (w != nullptr) { - indices.push_back(w->opts.index); - } - } - } - const auto result = windowManager.json(indices).str(); - window->resolvePromise(message.get("seq"), OK_STATE, result); - return; - } - - if (message.name == "window.create") { - const auto seq = message.get("seq"); - - auto currentWindowIndex = message.index < 0 ? 0 : message.index; - auto currentWindow = windowManager.getWindow(currentWindowIndex); - - auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : 0; - targetWindowIndex = targetWindowIndex < 0 ? 0 : targetWindowIndex; - - if (targetWindowIndex >= SSC_MAX_WINDOWS && message.get("headless") != "true" && message.get("debug") != "true") { - const JSON::Object json = JSON::Object::Entries { - { "err", String("Cannot create window with an index beyond ") + std::to_string(SSC_MAX_WINDOWS) } - }; - currentWindow->resolvePromise(seq, ERROR_STATE, json); - return; - } - - auto targetWindow = windowManager.getWindow(targetWindowIndex); - auto targetWindowStatus = windowManager.getWindowStatus(targetWindowIndex); - - if (targetWindow && targetWindowStatus != WindowManager::WindowStatus::WINDOW_NONE) { - const JSON::Object json = JSON::Object::Entries { - { "err", "Window with index " + std::to_string(targetWindowIndex) + " already exists" } - }; - currentWindow->resolvePromise(seq, ERROR_STATE, json); - return; - } - - SSC::String error = getNavigationError(cwd, message.get("url")); - if (error.size() > 0) { - const JSON::Object json = SSC::JSON::Object::Entries { - {"err", JSON::Object::Entries { - {"message", error} - }} - }; - currentWindow->resolvePromise(seq, ERROR_STATE, json); - return; - } - - // fill the options - auto options = WindowOptions {}; - - options.title = message.get("title"); - options.url = message.get("url"); - - if (message.get("port").size() > 0) { - options.port = std::stoi(message.get("port")); - } - - auto screen = currentWindow->getScreenSize(); - - options.width = message.get("width").size() ? currentWindow->getSizeInPixels(message.get("width"), screen.width) : 0; - options.height = message.get("height").size() ? currentWindow->getSizeInPixels(message.get("height"), screen.height) : 0; - - options.minWidth = message.get("minWidth").size() ? currentWindow->getSizeInPixels(message.get("minWidth"), screen.width) : 0; - options.minHeight = message.get("minHeight").size() ? currentWindow->getSizeInPixels(message.get("minHeight"), screen.height) : 0; - options.maxWidth = message.get("maxWidth").size() ? currentWindow->getSizeInPixels(message.get("maxWidth"), screen.width) : screen.width; - options.maxHeight = message.get("maxHeight").size() ? currentWindow->getSizeInPixels(message.get("maxHeight"), screen.height) : screen.height; - - options.canExit = message.get("canExit") == "true" ? true : false; - options.headless = message.get("headless") == "true" ? true : false; - options.resizable = message.get("resizable") == "true" ? true : false; - options.frameless = message.get("frameless") == "true" ? true : false; - options.utility = message.get("utility") == "true" ? true : false; - options.debug = message.get("debug") == "true" ? true : false; - options.index = targetWindowIndex; - - targetWindow = windowManager.createWindow(options); - - targetWindow->show(EMPTY_SEQ); - - if (options.url.size() > 0) { - targetWindow->navigate(seq, options.url); - } - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json() }, - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - currentWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - return; - } - - if (message.name == "window.close") { - const auto index = message.index; - const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : index; - const auto window = windowManager.getWindow(index); - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - auto targetWindowStatus = windowManager.getWindowStatus(targetWindowIndex); - if (targetWindow) { - if (targetWindow->opts.canExit) { - targetWindow->exit(0); - } else { - targetWindow->close(0); - } - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json()}, - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - window->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - } - return; - } - - if (message.name == "window.show") { - auto targetWindowIndex = std::stoi(message.get("targetWindowIndex")); - targetWindowIndex = targetWindowIndex < 0 ? 0 : targetWindowIndex; - auto index = message.index < 0 ? 0 : message.index; - auto targetWindow = windowManager.getWindow(targetWindowIndex); - auto targetWindowStatus = windowManager.getWindowStatus(targetWindowIndex); - auto resolveWindow = windowManager.getWindow(index); - - if (!targetWindow || targetWindowStatus == WindowManager::WindowStatus::WINDOW_NONE) { - JSON::Object json = JSON::Object::Entries { - { "err", targetWindowStatus } - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - resolveWindow->resolvePromise( - message.seq, - ERROR_STATE, - result.json() - ); - return; - } - - targetWindow->show(EMPTY_SEQ); - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json() } - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - resolveWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - return; - } - - if (message.name == "window.hide") { - auto targetWindowIndex = std::stoi(message.get("targetWindowIndex")); - targetWindowIndex = targetWindowIndex < 0 ? 0 : targetWindowIndex; - auto index = message.index < 0 ? 0 : message.index; - auto targetWindow = windowManager.getWindow(targetWindowIndex); - auto targetWindowStatus = windowManager.getWindowStatus(targetWindowIndex); - auto resolveWindow = windowManager.getWindow(index); - - if (!targetWindow || targetWindowStatus == WindowManager::WindowStatus::WINDOW_NONE) { - JSON::Object json = JSON::Object::Entries { - { "err", targetWindowStatus } - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - resolveWindow->resolvePromise( - message.seq, - ERROR_STATE, - result.json() - ); - return; - } - - targetWindow->hide(EMPTY_SEQ); - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json() } - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - resolveWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - - return; - } - - if (message.name == "window.setTitle") { - const auto seq = message.seq; - const auto currentIndex = message.index; - const auto currentWindow = windowManager.getWindow(currentIndex); - const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : currentIndex; - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - - targetWindow->setTitle(value); - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json() }, - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - currentWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - - return; - } - - if (message.name == "window.navigate") { - const auto seq = message.seq; - const auto currentIndex = message.index; - const auto currentWindow = windowManager.getWindow(currentIndex); - const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : currentIndex; - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - const auto url = message.get("url"); - const auto error = getNavigationError(cwd, url); - - if (error.size() > 0) { - JSON::Object json = JSON::Object::Entries { - { "err", error } - }; - - currentWindow->resolvePromise( - message.seq, - ERROR_STATE, - json.str() - ); - - return; - } - - targetWindow->navigate(seq, url); - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json() }, - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - currentWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - - return; - } - - if (message.name == "window.showInspector") { - const auto seq = message.seq; - const auto currentIndex = message.index; - const auto currentWindow = windowManager.getWindow(currentIndex); - const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : currentIndex; - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - - targetWindow->showInspector(); - - JSON::Object json = JSON::Object::Entries { - { "data", true }, - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - currentWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - - return; - } - - if (message.name == "window.setBackgroundColor") { - int red = 0; - int green = 0; - int blue = 0; - float alpha = 1; - - try { - red = std::stoi(message.get("red")); - green = std::stoi(message.get("green")); - blue = std::stoi(message.get("blue")); - alpha = std::stof(message.get("alpha")); - } catch (...) { + process->write(output); } - - const auto seq = message.seq; - const auto currentIndex = message.index; - const auto currentWindow = windowManager.getWindow(currentIndex); - const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : currentIndex; - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - - targetWindow->setBackgroundColor(red, green, blue, alpha); - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json() }, - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - currentWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - return; - } - - if (message.name == "window.setSize") { - const auto seq = message.seq; - const auto currentIndex = message.index; - const auto currentWindow = windowManager.getWindow(currentIndex); - const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : currentIndex; - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - - auto screen = currentWindow->getScreenSize(); - - float width = currentWindow->getSizeInPixels(message.get("width"), screen.width); - float height = currentWindow->getSizeInPixels(message.get("height"), screen.height); - - targetWindow->setSize(width, height, 0); - - JSON::Object json = JSON::Object::Entries { - { "data", targetWindow->json() }, - }; - - auto result = SSC::IPC::Result(message.seq, message, json); - currentWindow->resolvePromise( - message.seq, - OK_STATE, - result.json() - ); - return; - } - - if (message.name == "application.setTrayMenu") { - const auto seq = message.get("seq"); - window->setTrayMenu(seq, value); - return; - } - - if (message.name == "application.setSystemMenu") { - const auto seq = message.get("seq"); - window->setSystemMenu(seq, value); - return; - } - - if (message.name == "application.setSystemMenuItemEnabled") { - const auto seq = message.get("seq"); - const auto enabled = message.get("enabled").find("true") != -1; - int indexMain = 0; - int indexSub = 0; - - try { - indexMain = std::stoi(message.get("indexMain")); - indexSub = std::stoi(message.get("indexSub")); - } catch (...) { - window->resolvePromise( - message.seq, - OK_STATE, - SSC::JSON::null - ); - return; - } - - window->setSystemMenuItemEnabled(enabled, indexMain, indexSub); - window->resolvePromise( - message.seq, - OK_STATE, - SSC::JSON::null - ); - return; - } - - bool isMaximize = message.name == "window.maximize"; - bool isMinimize = message.name == "window.minimize"; - bool isRestore = message.name == "window.restore"; - - if (isMaximize || isMinimize || isRestore) { - const auto currentIndex = message.index; - const auto currentWindow = windowManager.getWindow(currentIndex); - const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 - ? std::stoi(message.get("targetWindowIndex")) - : currentIndex; - const auto targetWindow = windowManager.getWindow(targetWindowIndex); - - if (isMaximize) { - targetWindow->maximize(); - window->resolvePromise(message.seq, OK_STATE, SSC::JSON::null); - } - - if (isMinimize) { - targetWindow->minimize(); - window->resolvePromise(message.seq, OK_STATE, SSC::JSON::null); - } - - if (isRestore) { - targetWindow->restore(); - window->resolvePromise(message.seq, OK_STATE, SSC::JSON::null); - } - - return; - } - - if (message.name == "window.setContextMenu") { - auto seq = message.get("seq"); - window->setContextMenu(seq, value); - window->resolvePromise( - message.seq, - OK_STATE, - SSC::JSON::null - ); + window->resolvePromise(message.seq, OK_STATE, JSON::null); return; } @@ -1334,11 +880,11 @@ MAIN { {"err", JSON::Object::Entries { {"message", "Not found"}, {"type", "NotFoundError"}, - {"url", out} + {"url", output} }} }; - auto result = SSC::IPC::Result(message.seq, message, err); + auto result = IPC::Result(message.seq, message, err); window->resolvePromise( message.seq, ERROR_STATE, @@ -1352,146 +898,302 @@ MAIN { // When a window or the app wants to exit, // we clean up the windows and the backend process. // - shutdownHandler = [&](int code) { - #if defined(__linux__) + shutdownHandler = [](int code) { + #if SOCKET_RUNTIME_PLATFORM_LINUX unlink(appInstanceLock.c_str()); #endif if (process != nullptr) { - process->kill(); + process->kill(code); + process = nullptr; } - windowManager.destroy(); - - #if defined(__APPLE__) - if (app_ptr->fromSSC) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (app.wasLaunchedFromCli) { debug("__EXIT_SIGNAL__=%d", 0); CLI::notify(); } #endif - app_ptr->kill(); + app.stop(); exit(code); }; app.onExit = shutdownHandler; - Vector<String> properties = { - "window_width", "window_height", - "window_min_width", "window_min_height", - "window_max_width", "window_max_height" - }; + // + // If this is being run in a terminal/multiplexer + // +#if !SOCKET_RUNTIME_PLATFORM_WINDOWS + installSignalHandler(SIGHUP, signalHandler); +#endif - auto setDefaultValue = [&](String property) { - // for min values set 0 - if (property.find("min") != -1) { - return "0"; - // for other values set 100% - } else { - return "100%"; - } - }; +#if defined(SIGUSR1) + installSignalHandler(SIGUSR1, signalHandler); +#endif + + installSignalHandler(SIGINT, signalHandler); + installSignalHandler(SIGTERM, signalHandler); - // Regular expression to match a float number or a percentage - std::regex validPattern("^\\d*\\.?\\d+%?$"); + const auto signalsDisabled = userConfig["application_signals"] == "false"; + const auto signals = parseStringList(userConfig["application_signals"]); - for (const auto& property : properties) { - if (app.appData[property].size() > 0) { - auto value = app.appData[property]; - if (!std::regex_match(value, validPattern)) { - app.appData[property] = setDefaultValue(property); - debug("Invalid value for %s: \"%s\". Setting it to \"%s\"", property.c_str(), value.c_str(), app.appData[property].c_str()); +#define SET_DEFAULT_WINDOW_SIGNAL_HANDLER(sig) { \ + const auto name = String(CONVERT_TO_STRING(sig)); \ + if ( \ + !signalsDisabled || \ + std::find(signals.begin(), signals.end(), name) != signals.end() \ + ) { \ + installSignalHandler(sig, defaultWindowSignalHandler); \ + } \ +} + +#if defined(SIGQUIT) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGQUIT) +#endif +#if defined(SIGILL) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGILL) +#endif +#if defined(SIGTRAP) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGTRAP) +#endif +#if defined(SIGABRT) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGABRT) +#endif +#if defined(SIGIOT) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGIOT) +#endif +#if defined(SIGBUS) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGBUS) +#endif +#if defined(SIGFPE) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGFPE) +#endif +#if defined(SIGKILL) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGKILL) +#endif +#if defined(SIGUSR2) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGUSR2) +#endif +#if defined(SIGPIPE) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGPIPE) +#endif +#if defined(SIGALRM) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGALRM) +#endif +#if defined(SIGCHLD) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGCHLD) +#endif +#if defined(SIGCONT) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGCONT) +#endif +#if defined(SIGSTOP) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGSTOP) +#endif +#if defined(SIGTSTP) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGTSTP) +#endif +#if defined(SIGTTIN) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGTTIN) +#endif +#if defined(SIGTTOU) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGTTOU) +#endif +#if defined(SIGURG) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGURG) +#endif +#if defined(SIGXCPU) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGXCPU) +#endif +#if defined(SIGXFSZ) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGXFSZ) +#endif +#if defined(SIGVTALRM) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGVTALRM) +#endif +#if defined(SIGPROF) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGPROF) +#endif +#if defined(SIGWINCH) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGWINCH) +#endif +#if defined(SIGIO) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGIO) +#endif +#if defined(SIGINFO) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGINFO) +#endif +#if defined(SIGSYS) + SET_DEFAULT_WINDOW_SIGNAL_HANDLER(SIGSYS) +#endif + + Vector<String> properties = { + "window_width", "window_height", + "window_min_width", "window_min_height", + "window_max_width", "window_max_height" + }; + + auto setDefaultValue = [](String property) { + // for min values set 0 + if (property.find("min") != -1) { + return "0"; + // for other values set 100% + } else { + return "100%"; + } + }; + + // Regular expression to match a float number or a percentage + std::regex validPattern("^\\d*\\.?\\d+%?$"); + + for (const auto& property : properties) { + if (app.userConfig[property].size() > 0) { + auto value = app.userConfig[property]; + if (!std::regex_match(value, validPattern)) { + app.userConfig[property] = setDefaultValue(property); + debug("Invalid value for %s: \"%s\". Setting it to \"%s\"", property.c_str(), value.c_str(), app.userConfig[property].c_str()); + } + // set default value if it's not set in socket.ini + } else { + app.userConfig[property] = setDefaultValue(property); } - // set default value if it's not set in socket.ini - } else { - app.appData[property] = setDefaultValue(property); } - } - windowManager.configure(WindowManagerOptions { - .defaultHeight = app.appData["window_height"], - .defaultWidth = app.appData["window_width"], - .defaultMinWidth = app.appData["window_min_width"], - .defaultMinHeight = app.appData["window_min_height"], - .defaultMaxWidth = app.appData["window_max_width"], - .defaultMaxHeight = app.appData["window_max_height"], - .headless = isHeadless, - .isTest = isTest, - .argv = argvArray.str(), - .cwd = cwd, - .appData = app.appData, - .onMessage = onMessage, - .onExit = shutdownHandler - }); + auto getProperty = [](String property) { + if (userConfig.count(property) > 0) { + return userConfig[property]; + } - auto defaultWindow = windowManager.createDefaultWindow(WindowOptions { - .resizable = app.appData["window_resizable"] == "false" ? false : true, - .frameless = app.appData["window_frameless"] == "true" ? true : false, - .utility = app.appData["window_utility"] == "true" ? true : false, - .canExit = true, - .onExit = shutdownHandler - }); + return String(""); + }; - defaultWindow->show(EMPTY_SEQ); + auto windowManagerOptions = WindowManagerOptions { + .defaultHeight = getProperty("window_height"), + .defaultWidth = getProperty("window_width"), + .defaultMinWidth = getProperty("window_min_width"), + .defaultMinHeight = getProperty("window_min_height"), + .defaultMaxWidth = getProperty("window_max_width"), + .defaultMaxHeight = getProperty("window_max_height") + }; - if (_port > 0) { - defaultWindow->navigate(EMPTY_SEQ, _host + ":" + std::to_string(_port)); - defaultWindow->setSystemMenu(EMPTY_SEQ, String( - "Developer Mode: \n" - " Reload: r + CommandOrControl\n" - " Quit: q + CommandOrControl\n" - ";" - )); - } else { - if (app.appData["webview_root"].size() != 0) { - defaultWindow->navigate( - EMPTY_SEQ, - "socket://" + app.appData["meta_bundle_identifier"] + app.appData["webview_root"] + windowManagerOptions.features.useTestScript = isTest; + windowManagerOptions.userConfig = app.userConfig; + windowManagerOptions.argv = argvArray; + windowManagerOptions.onMessage = onMessage; + windowManagerOptions.onExit = shutdownHandler; + + app.windowManager.configure(windowManagerOptions); + + auto isMaximizable = getProperty("window_maximizable"); + auto isMinimizable = getProperty("window_minimizable"); + auto isClosable = getProperty("window_closable"); + + auto defaultWindow = app.windowManager.createDefaultWindow(Window::Options { + .minimizable = (isMinimizable == "" || isMinimizable == "true") ? true : false, + .maximizable = (isMaximizable == "" || isMaximizable == "true") ? true : false, + .resizable = getProperty("window_resizable") == "false" ? false : true, + .closable = (isClosable == "" || isClosable == "true") ? true : false, + .frameless = getProperty("window_frameless") == "true" ? true : false, + .utility = getProperty("window_utility") == "true" ? true : false, + .shouldExitApplicationOnClose = true, + .titlebarStyle = getProperty("window_titlebar_style"), + .windowControlOffsets = getProperty("mac_window_control_offsets"), + .backgroundColorLight = getProperty("window_background_color_light"), + .backgroundColorDark = getProperty("window_background_color_dark") + }); + + if ( + userConfig["webview_service_worker_mode"] != "hybrid" && + userConfig["permissions_allow_service_worker"] != "false" + ) { + auto serviceWorkerWindowOptions = Window::Options {}; + auto serviceWorkerUserConfig = userConfig; + auto screen = defaultWindow->getScreenSize(); + + serviceWorkerUserConfig["webview_watch_reload"] = "false"; + // if the service worker window dies, then the app should too + serviceWorkerWindowOptions.shouldExitApplicationOnClose = true; + serviceWorkerWindowOptions.minHeight = defaultWindow->getSizeInPixels("30%", screen.height); + serviceWorkerWindowOptions.height = defaultWindow->getSizeInPixels("80%", screen.height); + serviceWorkerWindowOptions.minWidth = defaultWindow->getSizeInPixels("40%", screen.width); + serviceWorkerWindowOptions.width = defaultWindow->getSizeInPixels("80%", screen.width); + serviceWorkerWindowOptions.index = SOCKET_RUNTIME_SERVICE_WORKER_CONTAINER_WINDOW_INDEX; + serviceWorkerWindowOptions.headless = Env::get("SOCKET_RUNTIME_SERVICE_WORKER_DEBUG").size() == 0; + serviceWorkerWindowOptions.userConfig = serviceWorkerUserConfig; + serviceWorkerWindowOptions.features.useGlobalCommonJS = false; + serviceWorkerWindowOptions.features.useGlobalNodeJS = false; + + auto serviceWorkerWindow = app.windowManager.createWindow(serviceWorkerWindowOptions); + + app.serviceWorkerContainer.init(&serviceWorkerWindow->bridge); + serviceWorkerWindow->navigate( + "socket://" + userConfig["meta_bundle_identifier"] + "/socket/service-worker/index.html" ); + + if (Env::get("SOCKET_RUNTIME_SERVICE_WORKER_DEBUG").size() > 0) { + serviceWorkerWindow->show(); + } + } else if (userConfig["webview_service_worker_mode"] == "hybrid") { + app.serviceWorkerContainer.init(&defaultWindow->bridge); + } + + msleep(256); + defaultWindow->show(); + + if (devPort > 0) { + defaultWindow->navigate(devHost + ":" + std::to_string(devPort)); + defaultWindow->setSystemMenu(String( + "Developer Mode: \n" + " Reload: r + CommandOrControl\n" + " Quit: q + CommandOrControl\n" + ";" + )); } else { - defaultWindow->navigate( - EMPTY_SEQ, - "socket://" + app.appData["meta_bundle_identifier"] + "/index.html" - ); + if (app.userConfig["webview_root"].size() != 0) { + defaultWindow->navigate( + "socket://" + app.userConfig["meta_bundle_identifier"] + app.userConfig["webview_root"] + ); + } else { + defaultWindow->navigate( + "socket://" + app.userConfig["meta_bundle_identifier"] + "/index.html" + ); + } } - } - // - // If this is being run in a terminal/multiplexer - // - #ifndef _WIN32 - signal(SIGHUP, signalHandler); - #endif + if (isReadingStdin) { + String value; + std::getline(std::cin, value); - signal(SIGINT, signalHandler); - signal(SIGTERM, signalHandler); + auto t = Thread([](String value) { + auto app = App::sharedApplication(); + auto defaultWindow = app->windowManager.getWindow(0); - if (isReadingStdin) { - std::string value; - std::thread t([&]() { - do { - if (value.size() == 0) { - std::cin >> value; + while (!app->core->platform.wasFirstDOMContentLoadedEventDispatched) { + msleep(128); } - if (value.size() > 0 && defaultWindow->bridge->router.isReady) { - defaultWindow->eval(getEmitToRenderProcessJavaScript("process.stdin", value)); - value.clear(); - } - } while (true); - }); + do { + if (value.size() == 0) { + std::getline(std::cin, value); + } - std::cin >> value; - t.detach(); - } + if (value.size() > 0) { + defaultWindow->eval(getEmitToRenderProcessJavaScript("process.stdin", value)); + value.clear(); + } + } while (true); + }, value); - // - // # Event Loop - // start the platform specific event loop for the main - // thread and run it until it returns a non-zero int. - // - while (app.run() == 0); + t.detach(); + } + + // + // # Event Loop + // start the platform specific event loop for the main + // thread and run it until it returns a non-zero int. + // + while (app.run(argc, argv) == 0); -#if defined(__linux__) +#if SOCKET_RUNTIME_PLATFORM_LINUX dbus_connection_unref(connection); #endif diff --git a/src/extension/context.cc b/src/extension/context.cc index 1a7a23d3b0..1e48e5b78a 100644 --- a/src/extension/context.cc +++ b/src/extension/context.cc @@ -45,7 +45,7 @@ bool sapi_context_dispatch ( return false; } - return ctx->router->dispatch([=]() { + return ctx->router->bridge->dispatch([=]() { callback(ctx, data); }); } diff --git a/src/extension/extension.cc b/src/extension/extension.cc index 3d1d01c499..62276a53e1 100644 --- a/src/extension/extension.cc +++ b/src/extension/extension.cc @@ -5,40 +5,6 @@ namespace SSC { static Vector<String> initializedExtensions; static Mutex mutex; - static String getcwd () { - String cwd; - #if defined(__linux__) && !defined(__ANDROID__) - try { - auto canonical = fs::canonical("/proc/self/exe"); - cwd = fs::path(canonical).parent_path().string(); - } catch (...) {} - #elif defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR - auto fileManager = [NSFileManager defaultManager]; - auto currentDirectory = [fileManager currentDirectoryPath]; - cwd = String([currentDirectory UTF8String]); - #elif defined(__APPLE__) - NSString* resourcePath = [[NSBundle mainBundle] resourcePath]; - cwd = String([[resourcePath stringByAppendingPathComponent: @"ui"] UTF8String]); - - #elif defined(_WIN32) - wchar_t filename[MAX_PATH]; - GetModuleFileNameW(NULL, filename, MAX_PATH); - auto path = fs::path { filename }.remove_filename(); - cwd = path.string(); - size_t last_pos = 0; - while ((last_pos = cwd.find('\\', last_pos)) != std::string::npos) { - cwd.replace(last_pos, 1, "\\\\"); - last_pos += 2; - } - #endif - - #ifndef _WIN32 - std::replace(cwd.begin(), cwd.end(), '\\', '/'); - #endif - - return cwd; - } - // explicit template instantiations template char* SSC::Extension::Context::Memory::alloc<char> (size_t); template sapi_context_t* @@ -146,7 +112,7 @@ namespace SSC { bool Extension::Context::release () { if (this->retain_count == 0) { - debug("WARN - Double release of SSC extension context"); + debug("WARN - Double release of runtime extension context"); return false; } if (--this->retain_count == 0) { @@ -220,7 +186,7 @@ namespace SSC { String Extension::getExtensionsDirectory (const String& name) { auto cwd = getcwd(); - #if defined(_WIN32) + #if SOCKET_RUNTIME_PLATFORM_WINDOWS return cwd + "\\socket\\extensions\\" + name + "\\"; #else return cwd + "/socket/extensions/" + name + "/"; @@ -265,11 +231,11 @@ namespace SSC { bool Extension::setHandle (const String& name, void* handle) { if (!extensions.contains(name)) { - std::cout << "WARN - extensions does not contain " << name << std::endl; + IO::write("WARN - extensions does not contain " + name); return false; } - std::cout << "Registering extension handle " << name << std::endl; + IO::write("Registering extension handle " + name); extensions.at(name)->handle = handle; return true; } @@ -306,7 +272,7 @@ namespace SSC { } String Extension::getExtensionType (const String& name) { - const auto libraryPath = getExtensionsDirectory(name) + (name + RUNTIME_EXTENSION_FILE_EXT); + const auto libraryPath = getExtensionsDirectory(name) + (name + SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME); const auto wasmPath = getExtensionsDirectory(name) + (name + ".wasm"); if (fs::exists(wasmPath)) { return "wasm32"; @@ -326,7 +292,7 @@ namespace SSC { } if (type == "shared") { - return getExtensionsDirectory(name) + (name + RUNTIME_EXTENSION_FILE_EXT); + return getExtensionsDirectory(name) + (name + SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME); } return ""; @@ -339,16 +305,16 @@ namespace SSC { // check if extension is already known if (isLoaded(name)) return true; - auto path = getExtensionsDirectory(name) + (name + RUNTIME_EXTENSION_FILE_EXT); + auto path = getExtensionsDirectory(name) + (name + SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME); - #if defined(_WIN32) + #if SOCKET_RUNTIME_PLATFORM_WINDOWS auto handle = LoadLibrary(path.c_str()); if (handle == nullptr) return false; auto __sapi_extension_init = (sapi_extension_registration_entry) GetProcAddress(handle, "__sapi_extension_init"); if (!__sapi_extension_init) return false; #else - #if defined(__ANDROID__) - auto handle = dlopen(String("libextension-" + name + RUNTIME_EXTENSION_FILE_EXT).c_str(), RTLD_NOW | RTLD_LOCAL); + #if SOCKET_RUNTIME_PLATFORM_ANDROID + auto handle = dlopen(String("libextension-" + name + SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME).c_str(), RTLD_NOW | RTLD_LOCAL); #else auto handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); #endif @@ -420,7 +386,7 @@ namespace SSC { return true; } - #if defined(_WIN32) + #if SOCKET_RUNTIME_PLATFORM_WINDOWS if (!FreeLibrary(reinterpret_cast<HMODULE>(extension->handle))) { return false; } @@ -604,24 +570,21 @@ void sapi_log (const sapi_context_t* ctx, const char* message) { output = message; } -#if defined(__linux__) && defined(__ANDROID__) - __android_log_print(ANDROID_LOG_INFO, "Console", "%s", message); -#else - SSC::IO::write(output, false); -#endif - -#if defined(__APPLE__) - static auto userConfig = SSC::getUserConfig(); - static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; - static auto SSC_OS_LOG_BUNDLE = os_log_create(bundleIdentifier.c_str(), -#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - "socket.runtime.mobile" -#else - "socket.runtime.desktop" -#endif - ); - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_INFO, "%{public}s", output.c_str()); -#endif + #if SOCKET_RUNTIME_PLATFORM_ANDROID + __android_log_print(ANDROID_LOG_INFO, "Console", "%s", message); + #else + SSC::IO::write(output, false); + #endif + + #if SOCKET_RUNTIME_PLATFORM_APPLE + static auto userConfig = SSC::getUserConfig(); + static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + static auto SOCKET_RUNTIME_OS_LOG_INFO = os_log_create( + bundleIdentifier.c_str(), + "socket.runtime" + ); + os_log_with_type(SOCKET_RUNTIME_OS_LOG_INFO, OS_LOG_TYPE_INFO, "%{public}s", output.c_str()); + #endif } void sapi_debug (const sapi_context_t* ctx, const char* message) { diff --git a/src/extension/extension.hh b/src/extension/extension.hh index 0cc55f7adf..670878a0c7 100644 --- a/src/extension/extension.hh +++ b/src/extension/extension.hh @@ -1,19 +1,15 @@ -#ifndef SSC_EXTENSION_H -#define SSC_EXTENSION_H - -#if !defined(_WIN32) -# include <dlfcn.h> -#endif +#ifndef SOCKET_RUNTIME_EXTENSION_EXTENSION_H +#define SOCKET_RUNTIME_EXTENSION_EXTENSION_H #include "../../include/socket/extension.h" -#include "../process/process.hh" +#include "../core/process.hh" #include "../core/core.hh" #include "../ipc/ipc.hh" -#if defined(_WIN32) -#define RUNTIME_EXTENSION_FILE_EXT ".dll" +#if SOCKET_RUNTIME_PLATFORM_WINDOWS +#define SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME ".dll" #else -#define RUNTIME_EXTENSION_FILE_EXT ".so" +#define SOCKET_RUNTIME_EXTENSION_FILENAME_EXTNAME ".so" #endif namespace SSC { @@ -190,6 +186,7 @@ extern "C" { sapi_process_spawn ( const char* command, const char* argv, + SSC::Vector<SSC::String> env, const char* path, sapi_process_spawn_stderr_callback_t onstdout, sapi_process_spawn_stderr_callback_t onstderr, @@ -197,6 +194,7 @@ extern "C" { ) : SSC::Process( command, argv, + env, path, [this, onstdout] (auto output) { if (onstdout) { onstdout(this, output.c_str(), output.size()); }}, [this, onstderr] (auto output) { if (onstderr) { onstderr(this, output.c_str(), output.size()); }}, @@ -278,5 +276,4 @@ extern "C" { {} }; }; - #endif diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index 915c9d265f..d3b45e9f3d 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -172,7 +172,7 @@ bool sapi_ipc_send_chunk ( "IPC method '" + result->message.name + "' must be invoked with HTTP"; return sapi_ipc_reply_with_error(result, error.c_str()); } - auto send_chunk_ptr = result->post.chunk_stream; + auto send_chunk_ptr = result->post.chunkStream; if (send_chunk_ptr == nullptr) { debug( "Cannot use 'sapi_ipc_send_chunk' before setting the \"Transfer-Encoding\"" @@ -203,7 +203,7 @@ bool sapi_ipc_send_event ( "IPC method '" + result->message.name + "' must be invoked with HTTP"; return sapi_ipc_reply_with_error(result, error.c_str()); } - auto send_event_ptr = result->post.event_stream; + auto send_event_ptr = result->post.eventStream; if (send_event_ptr == nullptr) { debug( "Cannot use 'sapi_ipc_send_event' before setting the \"Content-Type\"" @@ -234,12 +234,15 @@ bool sapi_ipc_send_bytes ( auto post = SSC::Post { .id = 0, .ttl = 0, - .body = new char[size]{0}, + .body = nullptr, .length = size, .headers = headers ? headers : "" }; - memcpy(post.body, bytes, size); + if (bytes != nullptr && size > 0) { + post.body = std::make_shared<char[]>(size); + memcpy(post.body.get(), bytes, size); + } if (message) { auto result = SSC::IPC::Result( @@ -249,11 +252,11 @@ bool sapi_ipc_send_bytes ( post ); - return ctx->router->send(result.seq, result.str(), result.post); + return ctx->router->bridge->send(result.seq, result.str(), result.post); } auto result = SSC::IPC::Result(SSC::JSON::null); - return ctx->router->send(result.seq, result.str(), post); + return ctx->router->bridge->send(result.seq, result.str(), post); } bool sapi_ipc_send_bytes_with_result ( @@ -270,14 +273,17 @@ bool sapi_ipc_send_bytes_with_result ( auto post = SSC::Post { .id = 0, .ttl = 0, - .body = new char[size]{0}, + .body = nullptr, .length = size, - .headers =headers ? headers : "" + .headers = headers ? headers : "" }; - memcpy(post.body, bytes, size); + if (bytes != nullptr && size > 0) { + post.body = std::make_shared<char[]>(size); + memcpy(post.body.get(), bytes, size); + } - return ctx->router->send(result->seq, result->str(), post); + return ctx->router->bridge->send(result->seq, result->str(), post); } bool sapi_ipc_send_json ( @@ -322,11 +328,11 @@ bool sapi_ipc_send_json ( value ); - return ctx->router->send(result.seq, result.str(), result.post); + return ctx->router->bridge->send(result.seq, result.str(), result.post); } auto result = SSC::IPC::Result(value); - return ctx->router->send(result.seq, result.str(), result.post); + return ctx->router->bridge->send(result.seq, result.str(), result.post); } bool sapi_ipc_send_json_with_result ( @@ -365,7 +371,7 @@ bool sapi_ipc_send_json_with_result ( } auto res = SSC::IPC::Result(result->seq, result->message, value); - return ctx->router->send(res.seq, res.str(), res.post); + return ctx->router->bridge->send(res.seq, res.str(), res.post); } bool sapi_ipc_emit ( @@ -373,7 +379,7 @@ bool sapi_ipc_emit ( const char* name, const char* data ) { - return ctx && ctx->router ? ctx->router->emit(name, data) : false; + return ctx && ctx->router ? ctx->router->bridge->emit(name, SSC::String(data)) : false; } bool sapi_ipc_invoke ( @@ -390,7 +396,14 @@ bool sapi_ipc_invoke ( uri = "ipc://" + uri; } - return ctx->router->invoke(uri, bytes, size, [ctx, callback](auto result) { + SSC::SharedPointer<char[]> data = nullptr; + + if (bytes != nullptr && size > 0) { + data.reset(new char[size]{0}); + memcpy(data.get(), bytes, size); + } + + return ctx->router->invoke(uri, data, size, [ctx, callback](auto result) { callback( reinterpret_cast<const sapi_ipc_result_t*>(&result), reinterpret_cast<const sapi_ipc_router_t*>(&ctx->router) @@ -487,7 +500,7 @@ const unsigned char* sapi_ipc_message_get_bytes ( const sapi_ipc_message_t* message ) { if (!message) return nullptr; - return reinterpret_cast<const unsigned char*>(message->buffer.bytes); + return reinterpret_cast<const unsigned char*>(message->buffer.bytes.get()); } unsigned int sapi_ipc_message_get_bytes_size ( @@ -652,9 +665,9 @@ void sapi_ipc_result_set_bytes ( unsigned char* bytes ) { if (result && size && bytes) { - auto pointer = const_cast<char*>(reinterpret_cast<const char*>(bytes)); result->post.length = size; - result->post.body = pointer; + result->post.body = std::make_shared<char[]>(size); + memcpy(result->post.body.get(), bytes, size); } } @@ -662,7 +675,7 @@ unsigned char* sapi_ipc_result_get_bytes ( const sapi_ipc_result_t* result ) { return result - ? reinterpret_cast<unsigned char*>(result->post.body) + ? reinterpret_cast<unsigned char*>(result->post.body.get()) : nullptr; } @@ -680,27 +693,23 @@ void sapi_ipc_result_set_header ( if (result && name && value) { result->headers.set(name, value); - #if !defined(_WIN32) - if (strcasecmp(name, "content-type") == 0 && - strcasecmp(value, "text/event-stream") == 0) { + if (result->headers.get("content-type") == "text/event-stream") { result->context->retain(); result->post = SSC::Post(); - result->post.event_stream = - std::make_shared<std::function<bool(const char*, const char*, bool)>>( - [result](const char* name, const char* data, bool finished) { - return false; - }); - } else if (strcasecmp(name, "transfer-encoding") == 0 && - strcasecmp(value, "chunked") == 0) { + result->post.eventStream = std::make_shared<SSC::Post::EventStreamCallback>( + [result](const char* name, const char* data, bool finished) { + return false; + } + ); + } else if (result->headers.get("transfer-encoding") == "chunked") { result->context->retain(); result->post = SSC::Post(); - result->post.chunk_stream = - std::make_shared<std::function<bool(const char*, size_t, bool)>>( - [result](const char* chunk, size_t chunk_size, bool finished) { - return false; - }); + result->post.chunkStream = std::make_shared<SSC::Post::ChunkStreamCallback>( + [result](const char* chunk, size_t chunk_size, bool finished) { + return false; + } + ); } - #endif } } diff --git a/src/extension/javascript.cc b/src/extension/javascript.cc index 6ad36dc9db..fb35373ef8 100644 --- a/src/extension/javascript.cc +++ b/src/extension/javascript.cc @@ -12,5 +12,5 @@ void sapi_javascript_evaluate ( } auto script = SSC::createJavaScript(name, source); - ctx->router->evaluateJavaScript(script); + ctx->router->bridge->evaluateJavaScript(script); } diff --git a/src/extension/json.cc b/src/extension/json.cc index 26487df383..7386ac87a2 100644 --- a/src/extension/json.cc +++ b/src/extension/json.cc @@ -1,5 +1,5 @@ -#include "extension.hh" #include <string.h> +#include "extension.hh" const sapi_json_type_t sapi_json_typeof (const sapi_json_any_t* json) { if (json->isNull()) return SAPI_JSON_TYPE_NULL; diff --git a/src/extension/process.cc b/src/extension/process.cc index e3d01e2629..c576f8ef05 100644 --- a/src/extension/process.cc +++ b/src/extension/process.cc @@ -4,10 +4,10 @@ const sapi_process_exec_t* sapi_process_exec ( sapi_context_t* ctx, const char* command ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_exec is not supported on this platform"); return nullptr; -#endif +#else if (ctx == nullptr) return nullptr; if (!ctx->isAllowed("process_exec")) { @@ -18,28 +18,29 @@ const sapi_process_exec_t* sapi_process_exec ( auto process = SSC::exec(command); process.output = SSC::trim(process.output); return ctx->memory.alloc<sapi_process_exec_t>(ctx, process); +#endif } int sapi_process_exec_get_exit_code ( const sapi_process_exec_t* process ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_exec_get_exit_code is not supported on this platform"); return -1; -#endif - +#else return process != nullptr ? process->exitCode : -1; +#endif } const char* sapi_process_exec_get_output ( const sapi_process_exec_t* process ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_exec_get_output is not supported on this platform"); return nullptr; -#endif - +#else return process != nullptr ? process->output.c_str() : nullptr; +#endif } sapi_process_spawn_t* sapi_process_spawn ( @@ -51,15 +52,15 @@ sapi_process_spawn_t* sapi_process_spawn ( sapi_process_spawn_stderr_callback_t onstderr, sapi_process_spawn_exit_callback_t onexit ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn is not supported on this platform"); return nullptr; -#endif - +#else auto process = ctx->memory.alloc<sapi_process_spawn_t>( ctx, command, argv, + SSC::Vector<SSC::String>{}, path, onstdout, onstderr, @@ -67,46 +68,51 @@ sapi_process_spawn_t* sapi_process_spawn ( ); process->open(); return process; +#endif } int sapi_process_spawn_get_exit_code ( const sapi_process_spawn_t* process ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn_get_exit_code is not supported on this platform"); return -1; -#endif +#else return process != nullptr ? process->status.load() : -1; +#endif } unsigned long sapi_process_spawn_get_pid ( const sapi_process_spawn_t* process ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn_get_pid is not supported on this platform"); return 0; -#endif +#else return process != nullptr ? process->id : 0; +#endif } sapi_context_t* sapi_process_spawn_get_context ( const sapi_process_spawn_t* process ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn_get_context is not supported on this platform"); return nullptr; -#endif +#else return process != nullptr ? process->context : nullptr; +#endif } int sapi_process_spawn_wait ( sapi_process_spawn_t* process ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn_wait is not supported on this platform"); return -1; -#endif +#else return process != nullptr ? process->wait() : -1; +#endif } bool sapi_process_spawn_write ( @@ -114,36 +120,39 @@ bool sapi_process_spawn_write ( const char* data, const size_t size ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn_write is not supported on this platform"); return false; -#endif +#else if (!process || process->closed) return false; process->write(data, size); return true; +#endif } bool sapi_process_spawn_close_stdin ( sapi_process_spawn_t* process ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn_close_stdin is not supported on this platform"); return false; -#endif +#else if (!process || process->closed) return false; - process->close_stdin(); + process->closeStdin(); return true; +#endif } bool sapi_process_spawn_kill ( sapi_process_spawn_t* process, int code ) { -#if defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_IOS debug("sapi_process_spawn_kill is not supported on this platform"); return false; -#endif +#else if (!process || process->closed) return false; process->kill(code); return true; +#endif } diff --git a/src/init.cc b/src/init.cc index 95ddde33a2..d150460237 100644 --- a/src/init.cc +++ b/src/init.cc @@ -1,30 +1,36 @@ -#include "core/config.hh" -#include "core/string.hh" -#include "core/types.hh" -#include "core/ini.hh" +#include <socket/_user-config-bytes.hh> #if defined(__cplusplus) -// These rely on project-specific, compile-time variables. -namespace SSC { - bool isDebugEnabled () { - return DEBUG == 1; +extern "C" { +#endif + const unsigned char* socket_runtime_init_get_user_config_bytes () { + return __socket_runtime_user_config_bytes; + } + + unsigned int socket_runtime_init_get_user_config_bytes_size () { + return sizeof(__socket_runtime_user_config_bytes); } - const Map getUserConfig () { - #include "user-config-bytes.hh" // NOLINT - return INI::parse(std::string( - (const char*) __ssc_config_bytes, - sizeof(__ssc_config_bytes) - )); + bool socket_runtime_init_is_debug_enabled () { + #if DEBUG + return true; + #endif + return false; } - const String getDevHost () { - static const auto host = String(HOST); - return host; + const char* socket_runtime_init_get_dev_host () { + #if defined(HOST) + return HOST; + #endif + return ""; } - int getDevPort () { + int socket_runtime_init_get_dev_port () { + #if defined(PORT) return PORT; + #endif + return 0; } +#if defined(__cplusplus) } #endif diff --git a/src/ios/main.mm b/src/ios/main.mm index 0d1d2e69fe..0853df4957 100644 --- a/src/ios/main.mm +++ b/src/ios/main.mm @@ -1,463 +1,13 @@ -#include "../core/core.hh" -#include "../ipc/ipc.hh" -#include "../window/window.hh" +#include "../app/app.hh" #include "../cli/cli.hh" -using namespace SSC; - -constexpr auto _debug = false; - -static dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_CONCURRENT, - QOS_CLASS_USER_INITIATED, - -1 -); - -static dispatch_queue_t queue = dispatch_queue_create( - "socket.runtime.app.queue", - qos -); - -@interface AppDelegate : UIResponder < - UIApplicationDelegate, - WKScriptMessageHandler, - UIScrollViewDelegate -> { - SSC::IPC::Bridge* bridge; - Core* core; -} -@property (strong, nonatomic) UIWindow* window; -@property (strong, nonatomic) SSCNavigationDelegate* navDelegate; -@property (strong, nonatomic) SSCBridgedWebView* webview; -@property (strong, nonatomic) WKUserContentController* content; - -- (BOOL) application: (UIApplication*) application - continueUserActivity: (NSUserActivity*) userActivity - restorationHandler: (void (^)(NSArray<id<UIUserActivityRestoring>>*)) restorationHandler; - - - (BOOL) application: (UIApplication*) application - willContinueUserActivityWithType: (NSString*) userActivityType; -@end - -// -// iOS has no "window". There is no navigation, just a single webview. It also -// has no "main" process, we want to expose some network functionality to the -// JavaScript environment so it can be used by the web app and the wasm layer. -// -@implementation AppDelegate -- (void) applicationDidEnterBackground: (UIApplication*) application { - [self.webview evaluateJavaScript: @"window.blur()" completionHandler: nil]; -} - -- (void) applicationWillEnterForeground: (UIApplication*) application { - [self.webview evaluateJavaScript: @"window.focus()" completionHandler: nil]; - bridge->bluetooth.startScanning(); -} - -- (void) applicationWillTerminate: (UIApplication*) application { - // @TODO(jwerle): what should we do here? -} - -- (void) applicationDidBecomeActive: (UIApplication*) application { - dispatch_async(queue, ^{ - self->core->resumeAllPeers(); - self->core->runEventLoop(); - }); -} - -- (void) applicationWillResignActive: (UIApplication*) application { - dispatch_async(queue, ^{ - self->core->stopEventLoop(); - self->core->pauseAllPeers(); - }); -} - -- (BOOL) application: (UIApplication*) application - continueUserActivity: (NSUserActivity*) userActivity - restorationHandler: (void (^)(NSArray<id<UIUserActivityRestoring>>*)) restorationHandler { - return [self - application: application - willContinueUserActivityWithType: userActivity.activityType - ]; -} - - - (BOOL) application: (UIApplication*) application - willContinueUserActivityWithType: (NSString*) userActivityType -{ - static auto userConfig = SSC::getUserConfig(); - auto webpageURL = application.userActivity.webpageURL; - - if (userActivityType == nullptr) { - return NO; - } - - if (webpageURL == nullptr) { - return NO; - } - - if (webpageURL.host == nullptr) { - return NO; - } - - auto activityType = SSC::String(userActivityType.UTF8String); - - if (activityType != SSC::String(NSUserActivityTypeBrowsingWeb.UTF8String)) { - return NO; - } - - auto host = SSC::String(webpageURL.host.UTF8String); - auto links = SSC::parseStringList(userConfig["meta_application_links"], ' '); - - if (links.size() == 0) { - return NO; - } - - bool exists = false; - - for (const auto& link : links) { - const auto parts = SSC::split(link, '?'); - if (host == parts[0]) { - exists = true; - break; - } - } - - if (!exists) { - return NO; - } - - auto url = SSC::String(webpageURL.absoluteString.UTF8String); - - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ "url", url }}; - - bool emitted = false; - - bridge->router.emit("applicationurl", json.str()); - - return YES; -} - -// -// When a message is received try to route it. -// Messages may also be received and routed via the custom scheme handler. -// -- (void) userContentController: (WKUserContentController*) userContentController - didReceiveScriptMessage: (WKScriptMessage*) scriptMessage -{ - id body = [scriptMessage body]; - - if (![body isKindOfClass:[NSString class]]) { - return; - } - - auto msg = IPC::Message([body UTF8String]); - - if (msg.name == "application.exit" || msg.name == "process.exit") { - auto code = std::stoi(msg.get("value", "0")); - - if (code > 0) { - CLI::notify(SIGTERM); - } else { - CLI::notify(SIGUSR2); - } - } else { - bridge->route([body UTF8String], nullptr, 0); - } -} - -- (void) keyboardWillHide: (NSNotification*) notification { - NSDictionary *info = [notification userInfo]; - NSValue* keyboardFrameBegin = [info valueForKey: UIKeyboardFrameEndUserInfoKey]; - CGRect rect = [keyboardFrameBegin CGRectValue]; - CGFloat height = rect.size.height; - - JSON::Object json = JSON::Object::Entries { - {"value", JSON::Object::Entries { - {"event", "will-hide"}, - {"height", height} - }} - }; - - self.webview.scrollView.scrollEnabled = YES; - bridge->router.emit("keyboard", json.str()); -} - -- (void) keyboardDidHide: (NSNotification*) notification { - - JSON::Object json = JSON::Object::Entries { - {"value", JSON::Object::Entries { - {"event", "did-hide"} - }} - }; - - bridge->router.emit("keyboard", json.str()); -} - -- (void) keyboardWillShow: (NSNotification*) notification { - NSDictionary *info = [notification userInfo]; - NSValue* keyboardFrameBegin = [info valueForKey: UIKeyboardFrameEndUserInfoKey]; - CGRect rect = [keyboardFrameBegin CGRectValue]; - CGFloat height = rect.size.height; - - JSON::Object json = JSON::Object::Entries { - {"value", JSON::Object::Entries { - {"event", "will-show"}, - {"height", height}, - }} - }; - - self.webview.scrollView.scrollEnabled = NO; - bridge->router.emit("keyboard", json.str()); -} - -- (void) keyboardDidShow: (NSNotification*) notification { - JSON::Object json = JSON::Object::Entries { - {"value", JSON::Object::Entries { - {"event", "did-show"} - }} - }; - - bridge->router.emit("keyboard", json.str()); -} - -- (void) keyboardWillChange: (NSNotification*) notification { - NSDictionary* keyboardInfo = [notification userInfo]; - NSValue* keyboardFrameBegin = [keyboardInfo valueForKey: UIKeyboardFrameEndUserInfoKey]; - CGRect rect = [keyboardFrameBegin CGRectValue]; - CGFloat width = rect.size.width; - CGFloat height = rect.size.height; - - JSON::Object json = JSON::Object::Entries { - {"value", JSON::Object::Entries { - {"event", "will-change"}, - {"width", width}, - {"height", height}, - }} - }; - - bridge->router.emit("keyboard", json.str()); -} - -- (void) scrollViewDidScroll: (UIScrollView*) scrollView { - scrollView.bounds = self.webview.bounds; -} - -- (BOOL) application: (UIApplication*) app - openURL: (NSURL*) url - options: (NSDictionary<UIApplicationOpenURLOptionsKey, id>*) options -{ - // TODO can this be escaped or is the url encoded property already? - JSON::Object json = JSON::Object::Entries {{"url", [url.absoluteString UTF8String]}}; - bridge->router.emit("applicationurl", json.str()); - return YES; -} - -- (BOOL) application: (UIApplication*) application - didFinishLaunchingWithOptions: (NSDictionary*) launchOptions -{ - using namespace SSC; - - core = new Core; - bridge = new IPC::Bridge(core); - bridge->router.dispatchFunction = [=] (auto callback) { - dispatch_async(queue, ^{ callback(); }); - }; - - bridge->router.evaluateJavaScriptFunction = [=](auto js) { - dispatch_async(dispatch_get_main_queue(), ^{ - auto script = [NSString stringWithUTF8String: js.c_str()]; - [self.webview evaluateJavaScript: script completionHandler: nil]; - }); - }; - - auto appFrame = [[UIScreen mainScreen] bounds]; - - self.window = [[UIWindow alloc] initWithFrame: appFrame]; - - UIViewController *viewController = [[UIViewController alloc] init]; - viewController.view.frame = appFrame; - self.window.rootViewController = viewController; - - auto userConfig = SSC::getUserConfig(); - - StringStream env; - - for (auto const &envKey : parseStringList(userConfig["build_env"])) { - auto cleanKey = trim(envKey); - - if (!Env::has(cleanKey)) { - continue; - } - - auto envValue = Env::get(cleanKey.c_str()); - - env << String( - cleanKey + "=" + encodeURIComponent(envValue) + "&" - ); - } - - env << String("width=" + std::to_string(appFrame.size.width) + "&"); - env << String("height=" + std::to_string(appFrame.size.height) + "&"); - - NSString* resourcePath = [[NSBundle mainBundle] resourcePath]; - NSString* cwd = [resourcePath stringByAppendingPathComponent: @"ui"]; - const auto argv = userConfig["ssc_argv"]; - - uv_chdir(cwd.UTF8String); - - WindowOptions opts { - .debug = isDebugEnabled(), - .isTest = argv.find("--test") != -1, - .argv = argv, - .env = env.str(), - .appData = userConfig - }; - - // Note: you won't see any logs in the preload script before the - // Web Inspector is opened - String preload = createPreload(opts); - - WKUserScript* initScript = [[WKUserScript alloc] - initWithSource: [NSString stringWithUTF8String: preload.c_str()] - injectionTime: WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly: NO]; - - WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; - - [config setURLSchemeHandler: bridge->router.schemeHandler - forURLScheme: @"ipc"]; - - [config setURLSchemeHandler: bridge->router.schemeHandler - forURLScheme: @"socket"]; - - self.content = [config userContentController]; - - [self.content addScriptMessageHandler:self name: @"external"]; - [self.content addUserScript: initScript]; - - self.webview = [[SSCBridgedWebView alloc] initWithFrame: appFrame configuration: config]; - self.webview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - - WKPreferences* prefs = self.webview.configuration.preferences; - - [self.webview.configuration - setValue: @YES - forKey: @"allowUniversalAccessFromFileURLs" - ]; - - [self.webview.configuration.preferences - setValue: @YES - forKey: @"allowFileAccessFromFileURLs" - ]; - - [self.webview.configuration.preferences - setValue: @YES - forKey: @"javaScriptEnabled" - ]; - - if (userConfig["permissions_allow_fullscreen"] == "false") { - [prefs setValue: @NO forKey: @"fullScreenEnabled"]; - } else { - [prefs setValue: @YES forKey: @"fullScreenEnabled"]; - } - - if (SSC::isDebugEnabled()) { - [prefs setValue:@YES forKey:@"developerExtrasEnabled"]; - if (@available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) { - [self.webview setInspectable: YES]; - } - } - - if (userConfig["permissions_allow_clipboard"] == "false") { - [prefs setValue: @NO forKey: @"javaScriptCanAccessClipboard"]; - } else { - [prefs setValue: @YES forKey: @"javaScriptCanAccessClipboard"]; - } - - if (userConfig["permissions_allow_data_access"] == "false") { - [prefs setValue: @NO forKey: @"storageAPIEnabled"]; - } else { - [prefs setValue: @YES forKey: @"storageAPIEnabled"]; - } - - if (userConfig["permissions_allow_device_orientation"] == "false") { - [prefs setValue: @NO forKey: @"deviceOrientationEventEnabled"]; - } else { - [prefs setValue: @YES forKey: @"deviceOrientationEventEnabled"]; - } - - if (userConfig["permissions_allow_notifications"] == "false") { - [prefs setValue: @NO forKey: @"appBadgeEnabled"]; - [prefs setValue: @NO forKey: @"notificationsEnabled"]; - [prefs setValue: @NO forKey: @"notificationEventEnabled"]; - } else { - [prefs setValue: @YES forKey: @"appBadgeEnabled"]; - [prefs setValue: @YES forKey: @"notificationsEnabled"]; - [prefs setValue: @YES forKey: @"notificationEventEnabled"]; - } - - NSNotificationCenter* ns = [NSNotificationCenter defaultCenter]; - [ns addObserver: self selector: @selector(keyboardDidShow:) name: UIKeyboardDidShowNotification object: nil]; - [ns addObserver: self selector: @selector(keyboardDidHide:) name: UIKeyboardDidHideNotification object: nil]; - [ns addObserver: self selector: @selector(keyboardWillShow:) name: UIKeyboardWillShowNotification object: nil]; - [ns addObserver: self selector: @selector(keyboardWillHide:) name: UIKeyboardWillHideNotification object: nil]; - [ns addObserver: self selector: @selector(keyboardWillChange:) name: UIKeyboardWillChangeFrameNotification object: nil]; - - self.navDelegate = [SSCNavigationDelegate new]; - self.navDelegate.bridge = bridge; - [self.webview setNavigationDelegate: self.navDelegate]; - - [viewController.view addSubview: self.webview]; - - NSString* allowed = [[NSBundle mainBundle] resourcePath]; - NSURL* url; - int port = getDevPort(); - - if (@available(iOS 16.4, *)) { - if (isDebugEnabled()) { - [self.webview setInspectable: YES]; - } - } - - if (isDebugEnabled() && port > 0) { - static const auto devHost = getDevHost(); - NSString* host = [NSString stringWithUTF8String: devHost.c_str()]; - url = [NSURL - URLWithString: [NSString stringWithFormat: @"%@:%d/", host, port] - ]; - - if (@available(iOS 15, *)) { - [self.webview loadFileRequest: [NSURLRequest requestWithURL: url] - allowingReadAccessToURL: [NSURL fileURLWithPath: allowed] - ]; - } else { - [self.webview loadRequest: [NSURLRequest requestWithURL: url]]; - } - } else { - if (userConfig["webview_root"].size() != 0) { - url = [NSURL URLWithString: @(("socket://" + userConfig["meta_bundle_identifier"] + userConfig["webview_root"]).c_str())]; - } else { - url = [NSURL URLWithString: @(("socket://" + userConfig["meta_bundle_identifier"] + "/index.html").c_str())]; - } - - auto request = [NSMutableURLRequest requestWithURL: url]; - [self.webview loadRequest: request]; - } - - self.webview.scrollView.delegate = self; - [self.window makeKeyAndVisible]; - - return YES; -} -@end - int main (int argc, char *argv[]) { struct rlimit limit; getrlimit(RLIMIT_NOFILE, &limit); limit.rlim_cur = 2048; setrlimit(RLIMIT_NOFILE, &limit); - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + SSC::App app(0); + return app.run(argc, argv); } } diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index f8329596a4..0a538a47a3 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -1,4412 +1,1114 @@ -#include <regex> -#include <unordered_map> - -#if defined(__APPLE__) -#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> -#endif - +#include "../serviceworker/protocols.hh" #include "../extension/extension.hh" #include "../window/window.hh" +#include "../core/version.hh" +#include "../app/app.hh" #include "ipc.hh" -#define SOCKET_MODULE_CONTENT_TYPE "text/javascript" -#define IPC_BINARY_CONTENT_TYPE "application/octet-stream" -#define IPC_JSON_CONTENT_TYPE "text/json" - extern const SSC::Map SSC::getUserConfig (); extern bool SSC::isDebugEnabled (); -using namespace SSC; -using namespace SSC::IPC; +namespace SSC::IPC { + static Vector<Bridge*> instances; + static Mutex mutex; + + // The `ESM_IMPORT_PROXY_TEMPLATE` is used to provide an ESM module as + // a proxy to a canonical URL for a module import. + static constexpr auto ESM_IMPORT_PROXY_TEMPLATE_WITH_DEFAULT_EXPORT = R"S( +/** + * This module exists to provide a proxy to a canonical URL for a module + * so `{{protocol}}:{{specifier}}` and `{{protocol}}://{bundle_identifier}/socket/{{pathname}}` + * resolve to the exact same module instance. + * @see {@link https://github.com/socketsupply/socket/blob/{{commit}}/api{{pathname}}} + */ +import module from '{{url}}' +export * from '{{url}}' +export default module +)S"; + + static constexpr auto ESM_IMPORT_PROXY_TEMPLATE_WITHOUT_DEFAULT_EXPORT = R"S( +/** + * This module exists to provide a proxy to a canonical URL for a module + * so `{{protocol}}:{{specifier}}` and `{{protocol}}://{bundle_identifier}/socket/{{pathname}}` + * resolve to the exact same module instance. + * @see {@link https://github.com/socketsupply/socket/blob/{{commit}}/api{{pathname}}} + */ +export * from '{{url}}' +)S"; + + static const Vector<String> allowedNodeCoreModules = { + "async_hooks", + "assert", + "buffer", + "console", + "constants", + "child_process", + "crypto", + "dgram", + "dns", + "dns/promises", + "events", + "fs", + "fs/constants", + "fs/promises", + "http", + "https", + "ip", + "module", + "net", + "os", + "os/constants", + "path", + "path/posix", + "path/win32", + "perf_hooks", + "process", + "querystring", + "stream", + "stream/web", + "string_decoder", + "sys", + "test", + "timers", + "timers/promises", + "tty", + "url", + "util", + "vm", + "worker_threads" + }; -#if defined(__APPLE__) -static std::map<String, Router*> notificationRouterMap; -static Mutex notificationRouterMapMutex; +#if SOCKET_RUNTIME_PLATFORM_DESKTOP + static FileSystemWatcher* developerResourcesFileSystemWatcher = nullptr; + static void initializeDeveloperResourcesFileSystemWatcher (SharedPointer<Core> core) { + auto defaultUserConfig = SSC::getUserConfig(); + if ( + developerResourcesFileSystemWatcher == nullptr && + isDebugEnabled() && + defaultUserConfig["webview_watch"] == "true" + ) { + developerResourcesFileSystemWatcher = new FileSystemWatcher(getcwd()); + developerResourcesFileSystemWatcher->core = core.get(); + developerResourcesFileSystemWatcher->start([=]( + const auto& path, + const auto& events, + const auto& context + ) mutable { + Lock lock(SSC::IPC::mutex); + + static const auto cwd = getcwd(); + const auto relativePath = fs::relative(path, cwd).string(); + const auto json = JSON::Object::Entries {{"path", relativePath}}; + const auto result = SSC::IPC::Result(json); + + for (auto& bridge : instances) { + auto userConfig = bridge->userConfig; + if ( + !platform.ios && + !platform.android && + userConfig["webview_watch"] == "true" && + bridge->userConfig["webview_service_worker_mode"] != "hybrid" && + (!userConfig.contains("webview_watch_reload") || userConfig.at("webview_watch_reload") != "false") + ) { + // check if changed path was a service worker, if so unregister it so it can be reloaded + for (const auto& entry : App::sharedApplication()->serviceWorkerContainer.registrations) { + const auto& registration = entry.second; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + auto scriptURL = String("https://"); + #else + auto scriptURL = String("socket://"); + #endif -static dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_CONCURRENT, - QOS_CLASS_USER_INITIATED, - -1 -); + scriptURL += userConfig["meta_bundle_identifier"]; -static dispatch_queue_t queue = dispatch_queue_create( - "socket.runtime.ipc.bridge.queue", - qos -); -#endif + if (!relativePath.starts_with("/")) { + scriptURL += "/"; + } -static JSON::Any validateMessageParameters ( - const Message& message, - const Vector<String> names -) { - for (const auto& name : names) { - if (!message.has(name) || message.get(name).size() == 0) { - return JSON::Object::Entries { - {"message", "Expecting '" + name + "' in parameters"} - }; + scriptURL += relativePath; + if (registration.scriptURL == scriptURL) { + // 1. unregister service worker + // 2. re-register service worker + // 3. wait for it to be registered + // 4. emit 'filedidchange' event + bridge->navigator.serviceWorker.unregisterServiceWorker(entry.first); + bridge->core->setTimeout(8, [bridge, result, ®istration] () { + bridge->core->setInterval(8, [bridge, result, ®istration] (auto cancel) { + if (registration.state == ServiceWorkerContainer::Registration::State::Activated) { + cancel(); + + uint64_t timeout = 500; + if (bridge->userConfig["webview_watch_service_worker_reload_timeout"].size() > 0) { + try { + timeout = std::stoull(bridge->userConfig["webview_watch_service_worker_reload_timeout"]); + } catch (...) {} + } + + bridge->core->setTimeout(timeout, [bridge, result] () { + bridge->emit("filedidchange", result.json().str()); + }); + } + }); + + bridge->navigator.serviceWorker.registerServiceWorker(registration.options); + }); + return; + } + } + } + + bridge->emit("filedidchange", result.json().str()); + } + }); } } +#endif + Bridge::Options::Options ( + const Map& userConfig, + const Preload::Options& preload + ) : userConfig(userConfig), + preload(preload) + {} + + Bridge::Bridge ( + SharedPointer<Core> core, + const Options& options + ) : core(core), + userConfig(options.userConfig), + router(this), + navigator(this), + schemeHandlers(this) + { + this->id = rand64(); + this->client.id = this->id; + #if SOCKET_RUNTIME_PLATFORM_ANDROID + this->isAndroidEmulator = App::sharedApplication()->isAndroidEmulator; + #endif - return nullptr; -} + this->bluetooth.sendFunction = [this]( + const String& seq, + const JSON::Any value, + const SSC::Post post + ) { + this->send(seq, value.str(), post); + }; -static struct { Mutex mutex; String value = ""; } cwdstate; + this->bluetooth.emitFunction = [this]( + const String& seq, + const JSON::Any value + ) { + this->emit(seq, value.str()); + }; -static void setcwd (String cwd) { - Lock lock(cwdstate.mutex); - cwdstate.value = cwd; -} + this->dispatchFunction = [] (auto callback) { + #if SOCKET_RUNTIME_PLATFORM_ANDROID + callback(); + #else + App::sharedApplication()->dispatch(callback); + #endif + }; -static String getcwd () { - Lock lock(cwdstate.mutex); - String cwd = cwdstate.value; -#if defined(__linux__) && !defined(__ANDROID__) - try { - auto canonical = fs::canonical("/proc/self/exe"); - cwd = fs::path(canonical).parent_path().string(); - } catch (...) {} -#elif defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) - NSString* resourcePath = [[NSBundle mainBundle] resourcePath]; - cwd = String([[resourcePath stringByAppendingPathComponent: @"ui"] UTF8String]); -#elif defined(__APPLE__) - auto fileManager = [NSFileManager defaultManager]; - auto currentDirectory = [fileManager currentDirectoryPath]; - cwd = String([currentDirectory UTF8String]); -#elif defined(_WIN32) - wchar_t filename[MAX_PATH]; - GetModuleFileNameW(NULL, filename, MAX_PATH); - auto path = fs::path { filename }.remove_filename(); - cwd = path.string(); - size_t last_pos = 0; - while ((last_pos = cwd.find('\\', last_pos)) != std::string::npos) { - cwd.replace(last_pos, 1, "\\\\"); - last_pos += 2; - } -#endif + core->networkStatus.addObserver(this->networkStatusObserver, [this](auto json) { + if (json.has("name")) { + this->emit(json["name"].str(), json.str()); + } + }); -#ifndef _WIN32 - std::replace(cwd.begin(), cwd.end(), '\\', '/'); -#endif + core->networkStatus.start(); - cwdstate.value = cwd; - return cwd; -} + core->geolocation.addPermissionChangeObserver(this->geolocationPermissionChangeObserver, [this] (auto json) { + JSON::Object event = JSON::Object::Entries { + {"name", "geolocation"}, + {"state", json["state"]} + }; + this->emit("permissionchange", event.str()); + }); + + // on Linux, much of the Notification API is supported so these observers + // below are not needed as those events already occur in the webview + // we are patching for the other platforms + #if !SOCKET_RUNTIME_PLATFORM_LINUX + core->notifications.addPermissionChangeObserver(this->notificationsPermissionChangeObserver, [this](auto json) { + JSON::Object event = JSON::Object::Entries { + {"name", "notifications"}, + {"state", json["state"]} + }; + this->emit("permissionchange", event.str()); + }); + + if (userConfig["permissions_allow_notifications"] != "false") { + core->notifications.addNotificationResponseObserver(this->notificationResponseObserver, [this](auto json) { + this->emit("notificationresponse", json.str()); + }); + + core->notifications.addNotificationPresentedObserver(this->notificationPresentedObserver, [this](auto json) { + this->emit("notificationpresented", json.str()); + }); + } + #endif -#define RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) \ - [message, reply](auto seq, auto json, auto post) { \ - reply(Result { seq, message, json, post }); \ + Lock lock(SSC::IPC::mutex); + instances.push_back(this); + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + initializeDeveloperResourcesFileSystemWatcher(core); + #endif } -#define REQUIRE_AND_GET_MESSAGE_VALUE(var, name, parse, ...) \ - try { \ - var = parse(message.get(name, ##__VA_ARGS__)); \ - } catch (...) { \ - return reply(Result::Err { message, JSON::Object::Entries { \ - {"message", "Invalid '" name "' given in parameters"} \ - }}); \ + Bridge::~Bridge () { + // remove observers + core->geolocation.removePermissionChangeObserver(this->geolocationPermissionChangeObserver); + core->networkStatus.removeObserver(this->networkStatusObserver); + core->notifications.removePermissionChangeObserver(this->notificationsPermissionChangeObserver); + core->notifications.removeNotificationResponseObserver(this->notificationResponseObserver); + core->notifications.removeNotificationPresentedObserver(this->notificationPresentedObserver); + + do { + Lock lock(SSC::IPC::mutex); + const auto cursor = std::find(instances.begin(), instances.end(), this); + if (cursor != instances.end()) { + instances.erase(cursor); + } + + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + if (instances.size() == 0) { + if (developerResourcesFileSystemWatcher) { + developerResourcesFileSystemWatcher->stop(); + delete developerResourcesFileSystemWatcher; + } + } + #endif + } while (0); } -#define CLEANUP_AFTER_INVOKE_CALLBACK(router, message, result) { \ - if (!router->hasMappedBuffer(message.index, message.seq)) { \ - if (message.buffer.bytes != nullptr) { \ - delete [] message.buffer.bytes; \ - message.buffer.bytes = nullptr; \ - } \ - } \ - \ - if (!router->core->hasPostBody(result.post.body)) { \ - if (result.post.body != nullptr) { \ - delete [] result.post.body; \ - } \ - } \ -} + void Bridge::init () { + this->router.init(); + this->navigator.init(); + this->schemeHandlers.init(); + } -static void initRouterTable (Router *router) { - static auto userConfig = SSC::getUserConfig(); -#if defined(__APPLE__) - static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; - static auto SSC_OS_LOG_BUNDLE = os_log_create(bundleIdentifier.c_str(), - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - "socket.runtime.mobile" - #else - "socket.runtime.desktop" - #endif - ); -#endif + void Bridge::configureWebView (WebView* webview) { + this->navigator.configureWebView(webview); + } - /** - * Starts a bluetooth service - * @param serviceId - */ - router->map("bluetooth.start", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"serviceId"}); + bool Bridge::evaluateJavaScript (const String& source) { + if (this->core->isShuttingDown) { + return false; + } - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); + if (this->evaluateJavaScriptFunction != nullptr) { + this->evaluateJavaScriptFunction(source); + return true; } - if (userConfig["permissions_allow_bluetooth"] == "false") { - auto err =JSON::Object::Entries { - {"message", "Bluetooth is not allowed"} - }; + return false; + } - return reply(Result::Err { message, err }); + bool Bridge::dispatch (const DispatchCallback& callback) { + if (!this->core || this->core->isShuttingDown) { + return false; } - router->bridge->bluetooth.startService( - message.seq, - message.get("serviceId"), - [reply, message](auto seq, auto json) { - reply(Result { seq, message, json }); - } - ); - }); - - /** - * Subscribes to a characteristic for a service. - * @param serviceId - * @param characteristicId - */ - router->map("bluetooth.subscribe", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, { - "characteristicId", - "serviceId" - }); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); + if (this->dispatchFunction != nullptr) { + this->dispatchFunction(callback); + return true; } - if (userConfig["permissions_allow_bluetooth"] == "false") { - auto err =JSON::Object::Entries { - {"message", "Bluetooth is not allowed"} - }; + return false; + } - return reply(Result::Err { message, err }); + bool Bridge::navigate (const String& url) { + if (!this->core || this->core->isShuttingDown) { + return false; } - router->bridge->bluetooth.subscribeCharacteristic( - message.seq, - message.get("serviceId"), - message.get("characteristicId"), - [reply, message](auto seq, auto json) { - reply(Result { seq, message, json }); - } - ); - }); - - /** - * Publishes data to a characteristic for a service. - * @param serviceId - * @param characteristicId - */ - router->map("bluetooth.publish", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, { - "characteristicId", - "serviceId" - }); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); + if (this->navigateFunction != nullptr) { + this->navigateFunction(url); + return true; } - if (userConfig["permissions_allow_bluetooth"] == "false") { - auto err =JSON::Object::Entries { - {"message", "Bluetooth is not allowed"} - }; + return false; + } + + bool Bridge::route (const String& uri, SharedPointer<char[]> bytes, size_t size) { + return this->route(uri, bytes, size, nullptr); + } - return reply(Result::Err { message, err }); + bool Bridge::route ( + const String& uri, + SharedPointer<char[]> bytes, + size_t size, + Router::ResultCallback callback + ) { + if (callback != nullptr) { + return this->router.invoke(uri, bytes, size, callback); + } else { + return this->router.invoke(uri, bytes, size); } + } - auto bytes = message.buffer.bytes; - auto size = message.buffer.size; + bool Bridge::send ( + const Message::Seq& seq, + const String& data, + const Post& post + ) { + if (this->core->isShuttingDown) { + return false; + } - if (bytes == nullptr) { - bytes = message.value.data(); - size = message.value.size(); + if (post.body != nullptr || seq == "-1") { + const auto script = this->core->createPost(seq, data, post); + return this->evaluateJavaScript(script); } - router->bridge->bluetooth.publishCharacteristic( - message.seq, - bytes, - size, - message.get("serviceId"), - message.get("characteristicId"), - [reply, message](auto seq, auto json) { - reply(Result { seq, message, json }); - } + const auto value = encodeURIComponent(data); + const auto script = getResolveToRenderProcessJavaScript( + seq.size() == 0 ? "-1" : seq, + "0", + value ); - }); - - /** - * Maps a message buffer bytes to an index + sequence. - * - * This setup allows us to push a byte array to the bridge and - * map it to an IPC call index and sequence pair, which is reused for an - * actual IPC call, subsequently. This is used for systems that do not support - * a POST/PUT body in XHR requests natively, so instead we decorate - * `message.buffer` with already an mapped buffer. - */ - router->map("buffer.map", false, [](auto message, auto router, auto reply) { - router->setMappedBuffer(message.index, message.seq, message.buffer); - reply(Result { message.seq, message }); - }); - - /** - * Look up an IP address by `hostname`. - * @param hostname Host name to lookup - * @param family IP address family to resolve [default = 0 (AF_UNSPEC)] - * @see getaddrinfo(3) - */ - router->map("dns.lookup", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"hostname"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); + + return this->evaluateJavaScript(script); + } + + bool Bridge::send (const Message::Seq& seq, const JSON::Any& json, const Post& post) { + return this->send(seq, json.str(), post); + } + + bool Bridge::emit (const String& name, const String& data) { + if (this->core->isShuttingDown) { + return false; } - int family = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(family, "family", std::stoi, "0"); + const auto value = encodeURIComponent(data); + const auto script = getEmitToRenderProcessJavaScript(name, value); + return this->evaluateJavaScript(script); + } - router->core->dns.lookup( - message.seq, - Core::DNS::LookupOptions { message.get("hostname"), family }, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - router->map("extension.stats", [](auto message, auto router, auto reply) { - auto extensions = Extension::all(); - auto name = message.get("name"); - - if (name.size() > 0) { - auto type = Extension::getExtensionType(name); - auto path = Extension::getExtensionPath(name); - auto json = JSON::Object::Entries { - {"source", "extension.stats"}, - {"data", JSON::Object::Entries { - {"abi", SOCKET_RUNTIME_EXTENSION_ABI_VERSION}, - {"name", name}, - {"type", type}, - // `path` is absolute to the location of the resources - {"path", String("/") + std::filesystem::relative(path, getcwd()).string()} - }} - }; + bool Bridge::emit (const String& name, const JSON::Any& json) { + return this->emit(name, json.str()); + } - reply(Result { message.seq, message, json }); - } else { - int loaded = 0; + void Bridge::configureSchemeHandlers ( + const SchemeHandlers::Configuration& configuration + ) { + this->schemeHandlers.configure(configuration); + this->schemeHandlers.registerSchemeHandler("ipc", [this]( + const auto request, + const auto bridge, + auto callbacks, + auto callback + ) { + auto message = Message(request->url(), true); - for (const auto& tuple : extensions) { - if (tuple.second != nullptr) { - loaded++; - } + if (request->method == "OPTIONS") { + auto response = SchemeHandlers::Response(request, 204); + return callback(response); } - auto json = JSON::Object::Entries { - {"source", "extension.stats"}, - {"data", JSON::Object::Entries { - {"abi", SOCKET_RUNTIME_EXTENSION_ABI_VERSION}, - {"loaded", loaded} - }} - }; + message.isHTTP = true; + message.cancel = std::make_shared<MessageCancellation>(); - reply(Result { message.seq, message, json }); - } - }); + callbacks->cancel = [message] () { + if (message.cancel->handler != nullptr) { + message.cancel->handler(message.cancel->data); + } + }; - /** - * Query for type of extension ('shared', 'wasm32', 'unknown') - * @param name - */ - router->map("extension.type", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"name"}); + const auto size = request->body.size; + const auto bytes = request->body.bytes; + const auto invoked = this->router.invoke(message.str(), request->body.bytes, size, [=](Result result) { + if (!request->isActive()) { + return; + } - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + auto response = SchemeHandlers::Response(request); + + response.setHeaders(result.headers); + + // handle event source streams + if (result.post.eventStream != nullptr) { + response.setHeader("content-type", "text/event-stream"); + response.setHeader("cache-control", "no-store"); + *result.post.eventStream = [request, response, message, callback]( + const char* name, + const char* data, + bool finished + ) mutable { + if (request->isCancelled()) { + if (message.cancel->handler != nullptr) { + message.cancel->handler(message.cancel->data); + } + return false; + } - auto name = message.get("name"); - auto type = Extension::getExtensionType(name); - auto json = SSC::JSON::Object::Entries { - {"source", "extension.type"}, - {"data", JSON::Object::Entries { - {"name", name}, - {"type", type} - }} - }; + response.writeHead(200); - reply(Result { message.seq, message, json }); - }); + const auto event = SchemeHandlers::Response::Event { name, data }; - /** - * Load a named native extension. - * @param name - * @param allow - */ - router->map("extension.load", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"name"}); + if (event.count() > 0) { + response.write(event.str()); + } - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (finished) { + callback(response); + } - auto name = message.get("name"); + return true; + }; + return; + } - if (!Extension::load(name)) { - #if defined(_WIN32) - auto error = FormatError(GetLastError(), "bridge"); - #else - auto err = dlerror(); - auto error = String(err ? err : "Unknown error"); - #endif + // handle chunk streams + if (result.post.chunkStream != nullptr) { + response.setHeader("transfer-encoding", "chunked"); + *result.post.chunkStream = [request, response, message, callback]( + const char* chunk, + size_t size, + bool finished + ) mutable { + if (request->isCancelled()) { + if (message.cancel->handler != nullptr) { + message.cancel->handler(message.cancel->data); + } + return false; + } - std::cout << "Load extension error: " << error << std::endl; + response.writeHead(200); + response.write(size, chunk); - return reply(Result::Err { message, JSON::Object::Entries { - {"message", "Failed to load extension: '" + name + "': " + error} - }}); - } + if (finished) { + callback(response); + } - auto extension = Extension::get(name); - auto allowed = split(message.get("allow"), ','); - auto context = Extension::getContext(name); - auto ctx = context->memory.template alloc<Extension::Context>(context, router); + return true; + }; + return; + } - for (const auto& value : allowed) { - auto policy = trim(value); - ctx->setPolicy(policy, true); - } + if (result.post.body != nullptr) { + response.write(result.post.length, result.post.body); + } else { + response.write(result.json()); + } - Extension::setRouterContext(name, router, ctx); + callback(response); + }); - /// init context - if (!Extension::initialize(ctx, name, nullptr)) { - if (ctx->state == Extension::Context::State::Error) { - auto json = JSON::Object::Entries { - {"source", "extension.load"}, - {"extension", name}, + if (!invoked) { + auto response = SchemeHandlers::Response(request, 404); + response.send(JSON::Object::Entries { {"err", JSON::Object::Entries { - {"code", ctx->error.code}, - {"name", ctx->error.name}, - {"message", ctx->error.message}, - {"location", ctx->error.location}, + {"message", "Not found"}, + {"type", "NotFoundError"}, + {"url", request->url()} }} - }; + }); - reply(Result { message.seq, message, json }); - } else { - auto json = JSON::Object::Entries { - {"source", "extension.load"}, - {"extension", name}, - {"err", JSON::Object::Entries { - {"message", "Failed to initialize extension: '" + name + "'"}, - }} - }; + return callback(response); + } + }); - reply(Result { message.seq, message, json }); + this->schemeHandlers.registerSchemeHandler("socket", [this]( + const auto request, + const auto bridge, + auto callbacks, + auto callback + ) { + auto userConfig = this->userConfig; + auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + auto app = App::sharedApplication(); + auto window = app->windowManager.getWindowForBridge(bridge); + + // if there was no window, then this is a bad request as scheme + // handlers should only be handled directly in a window with + // a navigator and a connected IPC bridge + if (window == nullptr) { + auto response = SchemeHandlers::Response(request); + response.writeHead(400); + callback(response); + return; } - } else { - auto json = JSON::Object::Entries { - {"source", "extension.load"}, - {"data", JSON::Object::Entries { - {"abi", (uint64_t) extension->abi}, - {"name", extension->name}, - {"version", extension->version}, - {"description", extension->description} - }} - }; - reply(Result { message.seq, message, json }); - } - }); + if (request->method == "OPTIONS") { + auto response = SchemeHandlers::Response(request); + response.writeHead(204); + callback(response); + return; + } - /** - * Unload a named native extension. - * @param name - */ - router->map("extension.unload", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"name"}); + // the location of static application resources + const auto applicationResources = FileResource::getResourcesPath().string(); + // default response is 404 + auto response = SchemeHandlers::Response(request, 404); - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + // the resouce path that may be request + String resourcePath; - auto name = message.get("name"); + // the content location relative to the request origin + String contentLocation; - if (!Extension::isLoaded(name)) { - return reply(Result::Err { message, JSON::Object::Entries { - #if defined(_WIN32) - {"message", "Extension '" + name + "' is not loaded"} - #else - {"message", "Extension '" + name + "' is not loaded" + String(dlerror())} - #endif - }}); - } + // application resource or service worker request at `socket://<bundle_identifier>/*` + if (request->hostname == bundleIdentifier) { + const auto resolved = this->navigator.location.resolve(request->pathname, applicationResources); - auto extension = Extension::get(name); - auto ctx = Extension::getRouterContext(name, router); + if (resolved.redirect) { + if (request->method == "GET") { + auto location = resolved.pathname; + if (request->query.size() > 0) { + location += "?" + request->query; + } - if (Extension::unload(ctx, name, extension->contexts.size() == 1)) { - Extension::removeRouterContext(name, router); - auto json = JSON::Object::Entries { - {"source", "extension.unload"}, - {"extension", name}, - {"data", JSON::Object::Entries {}} - }; - return reply(Result { message.seq, message, json }); - } + if (request->fragment.size() > 0) { + location += "#" + request->fragment; + } - if (ctx->state == Extension::Context::State::Error) { - auto json = JSON::Object::Entries { - {"source", "extension.unload"}, - {"extension", name}, - {"err", JSON::Object::Entries { - {"code", ctx->error.code}, - {"name", ctx->error.name}, - {"message", ctx->error.message}, - {"location", ctx->error.location}, - }} - }; + response.redirect(location); + return callback(response); + } + } else if (resolved.isResource()) { + resourcePath = applicationResources + resolved.pathname; + } else if (resolved.isMount()) { + resourcePath = resolved.mount.filename; + } else if (request->pathname == "" || request->pathname == "/") { + if (userConfig.contains("webview_default_index")) { + resourcePath = userConfig["webview_default_index"]; + if (resourcePath.starts_with("./")) { + resourcePath = applicationResources + resourcePath.substr(1); + } else if (resourcePath.starts_with("/")) { + resourcePath = applicationResources + resourcePath; + } else { + resourcePath = applicationResources + + "/" + resourcePath; + } + } + } - reply(Result { message.seq, message, json }); - } else { - auto json = JSON::Object::Entries { - {"source", "extension.unload"}, - {"extension", name}, - {"err", JSON::Object::Entries { - {"message", "Failed to unload extension: '" + name + "'"}, - }} - }; + if (resourcePath.size() == 0 && resolved.pathname.size() > 0) { + resourcePath = applicationResources + resolved.pathname; + } - reply(Result { message.seq, message, json }); - } - }); - - /** - * Checks if current user can access file at `path` with `mode`. - * @param path - * @param mode - * @see access(2) - */ - router->map("fs.access", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path", "mode"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + // handle HEAD and GET requests for a file resource + if (resourcePath.size() > 0) { + if (resourcePath.starts_with(applicationResources)) { + contentLocation = resourcePath.substr(applicationResources.size(), resourcePath.size()); + } - int mode = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); + auto resource = FileResource(resourcePath); - router->core->fs.access( - message.seq, - message.get("path"), - mode, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Returns a mapping of file system constants. - */ - router->map("fs.constants", [](auto message, auto router, auto reply) { - router->core->fs.constants(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - /** - * Changes `mode` of file at `path`. - * @param path - * @param mode - * @see chmod(2) - */ - router->map("fs.chmod", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path", "mode"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (!resource.exists()) { + response.writeHead(404); + } else { + if (contentLocation.size() > 0) { + response.setHeader("content-location", contentLocation); + } - int mode = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); + if (request->method == "OPTIONS") { + response.writeHead(204); + } - router->core->fs.chmod( - message.seq, - message.get("path"), - mode, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Changes uid and gid of file at `path`. - * @param path - * @param uid - * @param gid - * @see chown(2) - */ - router->map("fs.chown", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path", "uid", "gid"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (request->method == "HEAD") { + const auto contentType = resource.mimeType(); + const auto contentLength = resource.size(); - int uid = 0; - int gid = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(uid, "uid", std::stoi); - REQUIRE_AND_GET_MESSAGE_VALUE(gid, "gid", std::stoi); - - router->core->fs.chown( - message.seq, - message.get("path"), - static_cast<uv_uid_t>(uid), - static_cast<uv_gid_t>(gid), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Changes uid and gid of symbolic link at `path`. - * @param path - * @param uid - * @param gid - * @see lchown(2) - */ - router->map("fs.lchown", [=](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path", "uid", "gid"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (contentType.size() > 0) { + response.setHeader("content-type", contentType); + } - int uid = 0; - int gid = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(uid, "uid", std::stoi); - REQUIRE_AND_GET_MESSAGE_VALUE(gid, "gid", std::stoi); - - router->core->fs.lchown( - message.seq, - message.get("path"), - static_cast<uv_uid_t>(uid), - static_cast<uv_gid_t>(gid), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Closes underlying file descriptor handle. - * @param id - * @see close(2) - */ - router->map("fs.close", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (contentLength > 0) { + response.setHeader("content-length", contentLength); + } - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + response.writeHead(200); + } - router->core->fs.close(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); + if (request->method == "GET") { + if (resource.mimeType() != "text/html") { + response.setHeader("cache-control", "public"); + response.send(resource); + } else { + const auto html = this->client.preload.insertIntoHTML(resource.str(), { + .protocolHandlerSchemes = this->navigator.serviceWorker.protocols.getSchemes() + }); + + response.setHeader("content-type", "text/html"); + response.setHeader("content-length", html.size()); + response.setHeader("cache-control", "public"); + response.writeHead(200); + response.write(html); + } + } + } - /** - * Closes underlying directory descriptor handle. - * @param id - * @see closedir(3) - */ - router->map("fs.closedir", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); + return callback(response); + } - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (this->navigator.serviceWorker.registrations.size() > 0) { + const auto fetch = ServiceWorkerContainer::FetchRequest { + request->method, + request->scheme, + request->hostname, + request->pathname, + request->query, + request->headers, + ServiceWorkerContainer::FetchBody { request->body.size, request->body.bytes }, + request->client + }; - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + const auto fetched = this->navigator.serviceWorker.fetch(fetch, [request, callback, response] (auto res) mutable { + if (!request->isActive()) { + return; + } - router->core->fs.closedir(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); + if (res.statusCode == 0) { + response.fail("ServiceWorker request failed"); + } else { + response.writeHead(res.statusCode, res.headers); + response.write(res.body.size, res.body.bytes); + } - /** - * Closes an open file or directory descriptor handle. - * @param id - * @see close(2) - * @see closedir(3) - */ - router->map("fs.closeOpenDescriptor", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); + callback(response); + }); - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (fetched) { + this->core->setTimeout(32000, [request] () mutable { + if (request->isActive()) { + auto response = SchemeHandlers::Response(request, 408); + response.fail("ServiceWorker request timed out."); + } + }); + return; + } + } - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + response.writeHead(404); + return callback(response); + } - router->core->fs.closeOpenDescriptor( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Closes all open file and directory descriptors, optionally preserving - * explicitly retrained descriptors. - * @param preserveRetained (default: true) - * @see close(2) - * @see closedir(3) - */ - router->map("fs.closeOpenDescriptors", [](auto message, auto router, auto reply) { - router->core->fs.closeOpenDescriptor( - message.seq, - message.get("preserveRetained") != "false", - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Copy file at path `src` to path `dest`. - * @param src - * @param dest - * @param flags - * @see copyfile(3) - */ - router->map("fs.copyFile", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"src", "dest", "flags"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + // module or stdlib import/fetch `socket:<module>/<path>` which will just + // proxy an import into a normal resource request above + if (request->hostname.size() == 0) { + auto pathname = request->pathname; - int flags = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(flags, "flags", std::stoi); + if (pathname.ends_with("/")) { + pathname = pathname.substr(0, pathname.size() - 1); + } - router->core->fs.copyFile( - message.seq, - message.get("src"), - message.get("dest"), - flags, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Creates a link at `dest` - * @param src - * @param dest - * @see link(2) - */ - router->map("fs.link", [=](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"src", "dest"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + const auto specifier = pathname.substr(1); - router->core->fs.link( - message.seq, - message.get("src"), - message.get("dest"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Creates a symlink at `dest` - * @param src - * @param dest - * @param flags - * @see symlink(2) - */ - router->map("fs.symlink", [=](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"src", "dest", "flags"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + if (!pathname.ends_with(".js")) { + pathname += ".js"; + } - int flags = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(flags, "flags", std::stoi); + if (!pathname.starts_with("/")) { + pathname = "/" + pathname; + } - router->core->fs.symlink( - message.seq, - message.get("src"), - message.get("dest"), - flags, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Computes stats for an open file descriptor. - * @param id - * @see stat(2) - * @see fstat(2) - */ - router->map("fs.fstat", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + resourcePath = applicationResources + "/socket" + pathname; + contentLocation = "/socket" + pathname; - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + auto resource = FileResource(resourcePath, { .cache = true }); - router->core->fs.fstat(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); + if (resource.exists()) { + const auto url = ( + #if SOCKET_RUNTIME_PLATFORM_ANDROID + "https://" + + #else + "socket://" + + #endif + bundleIdentifier + + contentLocation + + (request->query.size() > 0 ? "?" + request->query : "") + ); - /** - * Synchronize a file's in-core state with storage device - * @param id - * @see fsync(2) - */ - router->map("fs.fsync", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); + const auto moduleImportProxy = tmpl( + String(resource.read()).find("export default") != String::npos + ? ESM_IMPORT_PROXY_TEMPLATE_WITH_DEFAULT_EXPORT + : ESM_IMPORT_PROXY_TEMPLATE_WITHOUT_DEFAULT_EXPORT, + Map { + {"url", url}, + {"commit", VERSION_HASH_STRING}, + {"protocol", "socket"}, + {"pathname", pathname}, + {"specifier", specifier}, + {"bundle_identifier", bundleIdentifier} + } + ); - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + const auto contentType = resource.mimeType(); - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + if (contentType.size() > 0) { + response.setHeader("content-type", contentType); + } - router->core->fs.fsync( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Truncates opened file - * @param id - * @param offset - * @see ftruncate(2) - */ - router->map("fs.ftruncate", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id", "offset"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } + response.setHeader("content-length", moduleImportProxy.size()); - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + if (contentLocation.size() > 0) { + response.setHeader("content-location", contentLocation); + } - int64_t offset; - REQUIRE_AND_GET_MESSAGE_VALUE(offset, "offset", std::stoll); + response.writeHead(200); + response.write(moduleImportProxy); + return callback(response); + } + response.setHeader("content-type", "text/javascript"); + } - router->core->fs.ftruncate( - message.seq, - id, - offset, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Returns all open file or directory descriptors. - */ - router->map("fs.getOpenDescriptors", [](auto message, auto router, auto reply) { - router->core->fs.getOpenDescriptors( - message.seq, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Computes stats for a symbolic link at `path`. - * @param path - * @see stat(2) - * @see lstat(2) - */ - router->map("fs.lstat", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - router->core->fs.lstat( - message.seq, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Creates a directory at `path` with an optional mode and an optional recursive flag. - * @param path - * @param mode - * @param recursive - * @see mkdir(2) - */ - router->map("fs.mkdir", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path", "mode"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - int mode = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); - - router->core->fs.mkdir( - message.seq, - message.get("path"), - mode, - message.get("recursive") == "true", - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - - /** - * Opens a file descriptor at `path` for `id` with `flags` and `mode` - * @param id - * @param path - * @param flags - * @param mode - * @see open(2) - */ - router->map("fs.open", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, { - "id", - "path", - "flags", - "mode" - }); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - int mode = 0; - int flags = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); - REQUIRE_AND_GET_MESSAGE_VALUE(flags, "flags", std::stoi); - - router->core->fs.open( - message.seq, - id, - message.get("path"), - flags, - mode, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Opens a directory descriptor at `path` for `id` with `flags` and `mode` - * @param id - * @param path - * @see opendir(3) - */ - router->map("fs.opendir", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id", "path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->fs.opendir( - message.seq, - id, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Reads `size` bytes at `offset` from the underlying file descriptor. - * @param id - * @param size - * @param offset - * @see read(2) - */ - router->map("fs.read", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id", "size", "offset"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - int size = 0; - int offset = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(size, "size", std::stoi); - REQUIRE_AND_GET_MESSAGE_VALUE(offset, "offset", std::stoi); - - router->core->fs.read( - message.seq, - id, - size, - offset, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Reads next `entries` of from the underlying directory descriptor. - * @param id - * @param entries (default: 256) - */ - router->map("fs.readdir", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - int entries = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(entries, "entries", std::stoi); - - router->core->fs.readdir( - message.seq, - id, - entries, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Read value of a symbolic link at 'path' - * @param path - * @see readlink(2) - */ - router->map("fs.readlink", [=](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - router->core->fs.readlink( - message.seq, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Get the realpath at 'path' - * @param path - * @see realpath(2) - */ - router->map("fs.realpath", [=](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - router->core->fs.realpath( - message.seq, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Marks a file or directory descriptor as retained. - * @param id - */ - router->map("fs.retainOpenDescriptor", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->fs.retainOpenDescriptor( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Renames file at path `src` to path `dest`. - * @param src - * @param dest - * @see rename(2) - */ - router->map("fs.rename", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"src", "dest"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - router->core->fs.rename( - message.seq, - message.get("src"), - message.get("dest"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Removes file at `path`. - * @param path - * @see rmdir(2) - */ - router->map("fs.rmdir", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - router->core->fs.rmdir( - message.seq, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Computes stats for a file at `path`. - * @param path - * @see stat(2) - */ - router->map("fs.stat", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - router->core->fs.stat( - message.seq, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Stops a already started watcher - */ - router->map("fs.stopWatch", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->fs.watch( - message.seq, - id, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Removes a file or empty directory at `path`. - * @param path - * @see unlink(2) - */ - router->map("fs.unlink", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - router->core->fs.unlink( - message.seq, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * TODO - */ - router->map("fs.watch", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id", "path"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->fs.watch( - message.seq, - id, - message.get("path"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Writes buffer at `message.buffer.bytes` of size `message.buffers.size` - * at `offset` for an opened file handle. - * @param id Handle ID for an open file descriptor - * @param offset The offset to start writing at - * @see write(2) - */ - router->map("fs.write", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id", "offset"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - if (message.buffer.bytes == nullptr || message.buffer.size == 0) { - auto err = JSON::Object::Entries {{ "message", "Missing buffer in message" }}; - return reply(Result::Err { message, err }); - } - - - uint64_t id; - int offset = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(offset, "offset", std::stoi); - - router->core->fs.write( - message.seq, - id, - message.buffer.bytes, - message.buffer.size, - offset, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - -#if defined(__APPLE__) - router->map("geolocation.getCurrentPosition", [](auto message, auto router, auto reply) { - if (!router->locationObserver) { - auto err = JSON::Object::Entries {{ "message", "Location observer is not initialized", }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - - auto performedActivation = [router->locationObserver getCurrentPositionWithCompletion: ^(NSError* error, CLLocation* location) { - if (error != nullptr) { - auto message = String( - error.localizedDescription.UTF8String != nullptr - ? error.localizedDescription.UTF8String - : "An unknown error occurred" - ); - auto err = JSON::Object::Entries {{ "message", message }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - - auto heading = router->locationObserver.locationManager.heading; - auto json = JSON::Object::Entries { - {"coords", JSON::Object::Entries { - {"latitude", location.coordinate.latitude}, - {"longitude", location.coordinate.longitude}, - {"altitude", location.altitude}, - {"accuracy", location.horizontalAccuracy}, - {"altitudeAccuracy", location.verticalAccuracy}, - {"floorLevel", location.floor.level}, - {"heading", heading.trueHeading}, - {"speed", location.speed} - }} - }; - - reply(Result { message.seq, message, json }); - }]; - - if (!performedActivation) { - auto err = JSON::Object::Entries {{ "message", "Location observer could not be activated" }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - }); - - router->map("geolocation.watchPosition", [](auto message, auto router, auto reply) { - if (!router->locationObserver) { - auto err = JSON::Object::Entries {{ "message", "Location observer is not initialized", }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - int id = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoi); - - const int identifier = [router->locationObserver watchPositionForIdentifier: id completion: ^(NSError* error, CLLocation* location) { - if (error != nullptr) { - auto message = String( - error.localizedDescription.UTF8String != nullptr - ? error.localizedDescription.UTF8String - : "An unknown error occurred" - ); - auto err = JSON::Object::Entries {{ "message", message }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - - auto heading = router->locationObserver.locationManager.heading; - auto json = JSON::Object::Entries { - {"watch", JSON::Object::Entries { - {"identifier", identifier}, - }}, - {"coords", JSON::Object::Entries { - {"latitude", location.coordinate.latitude}, - {"longitude", location.coordinate.longitude}, - {"altitude", location.altitude}, - {"accuracy", location.horizontalAccuracy}, - {"altitudeAccuracy", location.verticalAccuracy}, - {"floorLevel", location.floor.level}, - {"heading", heading.trueHeading}, - {"speed", location.speed} - }} - }; - - reply(Result { "-1", message, json }); - }]; - - if (identifier == -1) { - auto err = JSON::Object::Entries {{ "message", "Location observer could not be activated" }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - - auto json = JSON::Object::Entries { - {"watch", JSON::Object::Entries { - {"identifier", identifier} - }} - }; - - reply(Result { message.seq, message, json }); - }); - - router->map("geolocation.clearWatch", [](auto message, auto router, auto reply) { - if (!router->locationObserver) { - auto err = JSON::Object::Entries {{ "message", "Location observer is not initialized", }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - int id = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoi); - - [router->locationObserver clearWatch: id]; - - reply(Result { message.seq, message, JSON::Object{} }); - }); -#endif - - /** - * A private API for artifically setting the current cached CWD value. - * This is only useful on platforms that need to set this value from an - * external source, like Android or ChromeOS. - */ - router->map("internal.setcwd", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"value"}); - - if (err.type != JSON::Type::Null) { - return reply(Result { message.seq, message, err }); - } - - setcwd(message.value); - reply(Result { message.seq, message, JSON::Object{} }); - }); - - /** - * Log `value to stdout` with platform dependent logger. - * @param value - */ - router->map("log", [](auto message, auto router, auto reply) { - auto value = message.value.c_str(); - #if defined(__APPLE__) - NSLog(@"%s", value); - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_INFO, "%{public}s", value); - #elif defined(__ANDROID__) - __android_log_print(ANDROID_LOG_DEBUG, "", "%s", value); - #else - printf("%s\n", value); - #endif - }); - -#if defined(__APPLE__) - router->map("notification.show", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, { - "id", - "title" - }); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; - auto attachments = [NSMutableArray new]; - auto userInfo = [NSMutableDictionary new]; - auto content = [UNMutableNotificationContent new]; - auto __block id = message.get("id"); - - if (message.has("tag")) { - userInfo[@"tag"] = @(message.get("tag").c_str()); - content.threadIdentifier = @(message.get("tag").c_str()); - } - - if (message.has("lang")) { - userInfo[@"lang"] = @(message.get("lang").c_str()); - } - - if (!message.has("silent") && message.get("silent") == "false") { - content.sound = [UNNotificationSound defaultSound]; - } - - if (message.has("icon")) { - NSError* error = nullptr; - auto url = [NSURL URLWithString: @(message.get("icon").c_str())]; - - if (message.get("icon").starts_with("socket://")) { - url = [NSURL URLWithString: [NSBundle.mainBundle.resourcePath - stringByAppendingPathComponent: [NSString - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - stringWithFormat: @"/ui/%s", url.path.UTF8String - #else - stringWithFormat: @"/%s", url.path.UTF8String - #endif - ] - ]]; - - url = [NSURL fileURLWithPath: url.path]; - } - - auto types = [UTType - typesWithTag: url.pathExtension - tagClass: UTTagClassFilenameExtension - conformingToType: nullptr - ]; - - auto options = [NSMutableDictionary new]; - - if (types.count > 0) { - options[UNNotificationAttachmentOptionsTypeHintKey] = types.firstObject.preferredMIMEType; - }; - - auto attachment = [UNNotificationAttachment - attachmentWithIdentifier: @("") - URL: url - options: options - error: &error - ]; - - if (error != nullptr) { - auto message = String( - error.localizedDescription.UTF8String != nullptr - ? error.localizedDescription.UTF8String - : "An unknown error occurred" - ); - - auto err = JSON::Object::Entries { { "message", message } }; - return reply(Result::Err { message, err }); - } - - [attachments addObject: attachment]; - } else { - // using an asset from the resources directory will require a code signed application - #if WAS_CODESIGNED - NSError* error = nullptr; - auto url = [NSURL URLWithString: [NSBundle.mainBundle.resourcePath - stringByAppendingPathComponent: [NSString - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - stringWithFormat: @"/ui/icon.png" - #else - stringWithFormat: @"/icon.png" - #endif - ] - ]]; - - url = [NSURL fileURLWithPath: url.path]; - - auto types = [UTType - typesWithTag: url.pathExtension - tagClass: UTTagClassFilenameExtension - conformingToType: nullptr - ]; - - auto options = [NSMutableDictionary new]; - - auto attachment = [UNNotificationAttachment - attachmentWithIdentifier: @("") - URL: url - options: options - error: &error - ]; - - if (error != nullptr) { - auto message = String( - error.localizedDescription.UTF8String != nullptr - ? error.localizedDescription.UTF8String - : "An unknown error occurred" - ); - - auto err = JSON::Object::Entries { { "message", message } }; - - return reply(Result::Err { message, err }); - } - - [attachments addObject: attachment]; - #endif - } - - if (message.has("image")) { - NSError* error = nullptr; - auto url = [NSURL URLWithString: @(message.get("image").c_str())]; - - if (message.get("image").starts_with("socket://")) { - url = [NSURL URLWithString: [NSBundle.mainBundle.resourcePath - stringByAppendingPathComponent: [NSString - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - stringWithFormat: @"/ui/%s", url.path.UTF8String - #else - stringWithFormat: @"/%s", url.path.UTF8String - #endif - ] - ]]; - - url = [NSURL fileURLWithPath: url.path]; - } - - auto types = [UTType - typesWithTag: url.pathExtension - tagClass: UTTagClassFilenameExtension - conformingToType: nullptr - ]; - - auto options = [NSMutableDictionary new]; - - if (types.count > 0) { - options[UNNotificationAttachmentOptionsTypeHintKey] = types.firstObject.preferredMIMEType; - }; - - auto attachment = [UNNotificationAttachment - attachmentWithIdentifier: @("") - URL: url - options: options - error: &error - ]; - - if (error != nullptr) { - auto message = String( - error.localizedDescription.UTF8String != nullptr - ? error.localizedDescription.UTF8String - : "An unknown error occurred" - ); - auto err = JSON::Object::Entries {{ "message", message }}; - - return reply(Result::Err { message, err }); - } - - [attachments addObject: attachment]; - } - - content.attachments = attachments; - content.userInfo = userInfo; - content.title = @(message.get("title").c_str()); - content.body = @(message.get("body", "").c_str()); - - auto request = [UNNotificationRequest - requestWithIdentifier: @(id.c_str()) - content: content - trigger: nil - ]; - - { - Lock lock(notificationRouterMapMutex); - notificationRouterMap.insert_or_assign(id, router); - } - - [notificationCenter addNotificationRequest: request withCompletionHandler: ^(NSError* error) { - if (error != nullptr) { - auto message = String( - error.localizedDescription.UTF8String != nullptr - ? error.localizedDescription.UTF8String - : "An unknown error occurred" - ); - - auto err = JSON::Object::Entries { - { "message", message } - }; - - reply(Result::Err { message, err }); - Lock lock(notificationRouterMapMutex); - notificationRouterMap.erase(id); - return; - } - - reply(Result { message.seq, message, JSON::Object::Entries { - {"id", request.identifier.UTF8String} - }}); - }]; - }); - - router->map("notification.close", [](auto message, auto router, auto reply) { - auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; - auto err = validateMessageParameters(message, { "id" }); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - auto id = message.get("id"); - auto identifiers = @[@(id.c_str())]; - - [notificationCenter removePendingNotificationRequestsWithIdentifiers: identifiers]; - [notificationCenter removeDeliveredNotificationsWithIdentifiers: identifiers]; - - reply(Result { message.seq, message, JSON::Object::Entries { - {"id", id} - }}); - - Lock lock(notificationRouterMapMutex); - if (notificationRouterMap.contains(id)) { - auto notificationRouter = notificationRouterMap.at(id); - JSON::Object json = JSON::Object::Entries { - {"id", id}, - {"action", "dismiss"} - }; - - notificationRouter->emit("notificationresponse", json.str()); - notificationRouterMap.erase(id); - } - }); - - router->map("notification.list", [](auto message, auto router, auto reply) { - auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; - [notificationCenter getDeliveredNotificationsWithCompletionHandler: ^(NSArray<UNNotification*> *notifications) { - JSON::Array::Entries entries; - - Lock lock(notificationRouterMapMutex); - for (UNNotification* notification in notifications) { - auto id = String(notification.request.identifier.UTF8String); - - if ( - !notificationRouterMap.contains(id) || - notificationRouterMap.at(id) != router - ) { - continue; - } - - entries.push_back(JSON::Object::Entries { - {"id", id} - }); - } - - reply(Result { message.seq, message, entries }); - }]; - }); -#endif - - /** - * Read or modify the `SEND_BUFFER` or `RECV_BUFFER` for a peer socket. - * @param id Handle ID for the buffer to read/modify - * @param size If given, the size to set in the buffer [default = 0] - * @param buffer The buffer to read/modify (SEND_BUFFER, RECV_BUFFER) [default = 0 (SEND_BUFFER)] - */ - router->map("os.bufferSize", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - int buffer = 0; - int size = 0; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(buffer, "buffer", std::stoi, "0"); - REQUIRE_AND_GET_MESSAGE_VALUE(size, "size", std::stoi, "0"); - - router->core->os.bufferSize( - message.seq, - id, - size, - buffer, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Returns a mapping of network interfaces. - */ - router->map("os.networkInterfaces", [](auto message, auto router, auto reply) { - router->core->os.networkInterfaces(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - /** - * Returns an array of CPUs available to the process. - */ - router->map("os.cpus", [](auto message, auto router, auto reply) { - router->core->os.cpus(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - router->map("os.rusage", [](auto message, auto router, auto reply) { - router->core->os.rusage(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - router->map("os.uptime", [](auto message, auto router, auto reply) { - router->core->os.uptime(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - router->map("os.uname", [](auto message, auto router, auto reply) { - router->core->os.uname(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - router->map("os.hrtime", [](auto message, auto router, auto reply) { - router->core->os.hrtime(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - router->map("os.availableMemory", [](auto message, auto router, auto reply) { - router->core->os.availableMemory(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - router->map("os.paths", [](auto message, auto router, auto reply) { - JSON::Object data; - - // paths - String resources = getcwd(); - String downloads; - String documents; - String pictures; - String desktop; - String videos; - String music; - String home; - - #if defined(__APPLE__) - static const auto uid = getuid(); - static const auto pwuid = getpwuid(uid); - static const auto HOME = pwuid != nullptr - ? String(pwuid->pw_dir) - : Env::get("HOME", getcwd()); - - static const auto fileManager = NSFileManager.defaultManager; - - #define DIRECTORY_PATH_FROM_FILE_MANAGER(type) ( \ - String([fileManager \ - URLForDirectory: type \ - inDomain: NSUserDomainMask \ - appropriateForURL: nil \ - create: NO \ - error: nil \ - ].path.UTF8String) \ - ) - - // overload with main bundle resources path for macos/ios - resources = String(NSBundle.mainBundle.resourcePath.UTF8String); - downloads = DIRECTORY_PATH_FROM_FILE_MANAGER(NSDownloadsDirectory); - documents = DIRECTORY_PATH_FROM_FILE_MANAGER(NSDocumentDirectory); - pictures = DIRECTORY_PATH_FROM_FILE_MANAGER(NSPicturesDirectory); - desktop = DIRECTORY_PATH_FROM_FILE_MANAGER(NSDesktopDirectory); - videos = DIRECTORY_PATH_FROM_FILE_MANAGER(NSMoviesDirectory); - music = DIRECTORY_PATH_FROM_FILE_MANAGER(NSMusicDirectory); - home = String(NSHomeDirectory().UTF8String); - - #undef DIRECTORY_PATH_FROM_FILE_MANAGER - - #elif defined(__linux__) - static const auto uid = getuid(); - static const auto pwuid = getpwuid(uid); - static const auto HOME = pwuid != nullptr - ? String(pwuid->pw_dir) - : Env::get("HOME", getcwd()); - - static const auto XDG_DOCUMENTS_DIR = Env::get("XDG_DOCUMENTS_DIR"); - static const auto XDG_DOWNLOAD_DIR = Env::get("XDG_DOWNLOAD_DIR"); - static const auto XDG_PICTURES_DIR = Env::get("XDG_PICTURES_DIR"); - static const auto XDG_DESKTOP_DIR = Env::get("XDG_DESKTOP_DIR"); - static const auto XDG_VIDEOS_DIR = Env::get("XDG_VIDEOS_DIR"); - static const auto XDG_MUSIC_DIR = Env::get("XDG_MUSIC_DIR"); - - if (XDG_DOCUMENTS_DIR.size() > 0) { - documents = XDG_DOCUMENTS_DIR; - } else { - documents = (Path(HOME) / "Documents").string(); - } - - if (XDG_DOWNLOAD_DIR.size() > 0) { - downloads = XDG_DOWNLOAD_DIR; - } else { - downloads = (Path(HOME) / "Downloads").string(); - } - - if (XDG_DESKTOP_DIR.size() > 0) { - desktop = XDG_DESKTOP_DIR; - } else { - desktop = (Path(HOME) / "Desktop").string(); - } - - if (XDG_PICTURES_DIR.size() > 0) { - pictures = XDG_PICTURES_DIR; - } else if (fs::exists(Path(HOME) / "Images")) { - pictures = (Path(HOME) / "Images").string(); - } else if (fs::exists(Path(HOME) / "Photos")) { - pictures = (Path(HOME) / "Photos").string(); - } else { - pictures = (Path(HOME) / "Pictures").string(); - } - - if (XDG_VIDEOS_DIR.size() > 0) { - videos = XDG_VIDEOS_DIR; - } else { - videos = (Path(HOME) / "Videos").string(); - } - - if (XDG_MUSIC_DIR.size() > 0) { - music = XDG_MUSIC_DIR; - } else { - music = (Path(HOME) / "Music").string(); - } - - home = Path(HOME).string(); - #elif defined(_WIN32) - static const auto HOME = Env::get("HOMEPATH", Env::get("HOME")); - static const auto USERPROFILE = Env::get("USERPROFILE", HOME); - downloads = (Path(USERPROFILE) / "Downloads").string(); - documents = (Path(USERPROFILE) / "Documents").string(); - desktop = (Path(USERPROFILE) / "Desktop").string(); - pictures = (Path(USERPROFILE) / "Pictures").string(); - videos = (Path(USERPROFILE) / "Videos").string(); - music = (Path(USERPROFILE) / "Music").string(); - home = Path(USERPROFILE).string(); - #endif - - data["resources"] = resources; - data["downloads"] = downloads; - data["documents"] = documents; - data["pictures"] = pictures; - data["desktop"] = desktop; - data["videos"] = videos; - data["music"] = music; - data["home"] = home; - - return reply(Result::Data { message, data }); - }); - - router->map("permissions.query", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"name"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - auto name = message.get("name"); - - #if defined(__APPLE__) - if (name == "geolocation") { - if (router->locationObserver.isAuthorized) { - auto data = JSON::Object::Entries {{"state", "granted"}}; - return reply(Result::Data { message, data }); - } else if (router->locationObserver.locationManager) { - auto authorizationStatus = ( - router->locationObserver.locationManager.authorizationStatus - ); - - if (authorizationStatus == kCLAuthorizationStatusDenied) { - auto data = JSON::Object::Entries {{"state", "denied"}}; - return reply(Result::Data { message, data }); - } else { - auto data = JSON::Object::Entries {{"state", "prompt"}}; - return reply(Result::Data { message, data }); - } - } - - auto data = JSON::Object::Entries {{"state", "denied"}}; - return reply(Result::Data { message, data }); - } - - if (name == "notifications") { - auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; - [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { - if (!settings) { - auto err = JSON::Object::Entries {{ "message", "Failed to reach user notification settings" }}; - return reply(Result::Err { message, err }); - } - - if (settings.authorizationStatus == UNAuthorizationStatusDenied) { - auto data = JSON::Object::Entries {{"state", "denied"}}; - return reply(Result::Data { message, data }); - } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { - auto data = JSON::Object::Entries {{"state", "prompt"}}; - return reply(Result::Data { message, data }); - } - - auto data = JSON::Object::Entries {{"state", "granted"}}; - return reply(Result::Data { message, data }); - }]; - } - #endif - }); - - router->map("permissions.request", [](auto message, auto router, auto reply) { - static auto userConfig = SSC::getUserConfig(); - auto err = validateMessageParameters(message, {"name"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - auto name = message.get("name"); - - if (name == "geolocation") { - #if defined(__APPLE__) - auto performedActivation = [router->locationObserver attemptActivationWithCompletion: ^(BOOL isAuthorized) { - if (!isAuthorized) { - auto reason = @("Location observer could not be activated"); - - if (!router->locationObserver.locationManager) { - reason = @("Location observer manager is not initialized"); - } else if (!router->locationObserver.locationManager.location) { - reason = @("Location observer manager could not provide location"); - } - - auto error = [NSError - errorWithDomain: @(userConfig["bundle_identifier"].c_str()) - code: -1 - userInfo: @{ - NSLocalizedDescriptionKey: reason - } - ]; - } - - if (isAuthorized) { - auto data = JSON::Object::Entries {{"state", "granted"}}; - return reply(Result::Data { message, data }); - } else if (router->locationObserver.locationManager.authorizationStatus == kCLAuthorizationStatusNotDetermined) { - auto data = JSON::Object::Entries {{"state", "prompt"}}; - return reply(Result::Data { message, data }); - } else { - auto data = JSON::Object::Entries {{"state", "denied"}}; - return reply(Result::Data { message, data }); - } - }]; - - if (!performedActivation) { - auto err = JSON::Object::Entries {{ "message", "Location observer could not be activated" }}; - err["type"] = "GeolocationPositionError"; - return reply(Result::Err { message, err }); - } - - return; - #endif - } - - if (name == "notifications") { - #if defined(__APPLE__) - UNAuthorizationOptions options = UNAuthorizationOptionProvisional; - auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; - auto requestAlert = message.get("alert") == "true"; - auto requestBadge = message.get("badge") == "true"; - auto requestSound = message.get("sound") == "true"; - - if (requestAlert) { - options |= UNAuthorizationOptionAlert; - } - - if (requestBadge) { - options |= UNAuthorizationOptionBadge; - } - - if (requestSound) { - options |= UNAuthorizationOptionSound; - } - - if (requestAlert && requestSound) { - options |= UNAuthorizationOptionCriticalAlert; - } - - [notificationCenter - requestAuthorizationWithOptions: options - completionHandler: ^(BOOL granted, NSError *error) { - [notificationCenter - getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { - if (settings.authorizationStatus == UNAuthorizationStatusDenied) { - auto data = JSON::Object::Entries {{"state", "denied"}}; - return reply(Result::Data { message, data }); - } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { - if (error) { - auto message = String( - error.localizedDescription.UTF8String != nullptr - ? error.localizedDescription.UTF8String - : "An unknown error occurred" - ); - - auto err = JSON::Object::Entries { - { "message", message } - }; - - return reply(Result::Err { message, err }); - } - - auto data = JSON::Object::Entries { - {"state", granted ? "granted" : "denied" } - }; - - return reply(Result::Data { message, data }); - } - - auto data = JSON::Object::Entries {{"state", "granted"}}; - return reply(Result::Data { message, data }); - }]; - }]; - #endif - } - }); - - /** - * Simply returns `pong`. - */ - router->map("ping", [](auto message, auto router, auto reply) { - auto result = Result { message.seq, message }; - result.data = "pong"; - reply(result); - }); - - /** - * Handles platform events. - * @param value The event name [domcontentloaded] - * @param data Optional data associated with the platform event. - */ - router->map("platform.event", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"value"}); - - if (err.type != JSON::Type::Null) { - return reply(Result { message.seq, message, err }); - } - - if (!router->isReady) router->isReady = true; - - router->core->platform.event( - message.seq, - message.value, - message.get("data"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Requests a notification with `title` and `body` to be shown. - * @param title - * @param body - */ - router->map("platform.notify", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"body", "title"}); - - if (err.type != JSON::Type::Null) { - return reply(Result { message.seq, message, err }); - } - - router->core->platform.notify( - message.seq, - message.get("title"), - message.get("body"), - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Requests a URL to be opened externally. - * @param value - */ - router->map("platform.openExternal", [](auto message, auto router, auto reply) mutable { - static const auto applicationProtocol = userConfig["meta_application_protocol"]; - auto err = validateMessageParameters(message, {"value"}); - - if (err.type != JSON::Type::Null) { - return reply(Result { message.seq, message, err }); - } - - if (applicationProtocol.size() > 0 && message.value.starts_with(applicationProtocol + ":")) { - SSC::JSON::Object json = SSC::JSON::Object::Entries { - { "url", message.value } - }; - - router->bridge->router.emit("applicationurl", json.str()); - reply(Result { - message.seq, - message, - SSC::JSON::Object::Entries { - {"data", json} - } - }); - return; - } - - router->core->platform.openExternal( - message.seq, - message.value, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Return Socket Runtime primordials. - */ - router->map("platform.primordials", [](auto message, auto router, auto reply) { - std::regex platform_pattern("^mac$", std::regex_constants::icase); - auto platformRes = std::regex_replace(platform.os, platform_pattern, "darwin"); - auto arch = std::regex_replace(platform.arch, std::regex("x86_64"), "x64"); - arch = std::regex_replace(arch, std::regex("x86"), "ia32"); - arch = std::regex_replace(arch, std::regex("arm(?!64).*"), "arm"); - auto json = JSON::Object::Entries { - {"source", "platform.primordials"}, - {"data", JSON::Object::Entries { - {"arch", arch}, - {"cwd", getcwd()}, - {"platform", platformRes}, - {"version", JSON::Object::Entries { - {"full", SSC::VERSION_FULL_STRING}, - {"short", SSC::VERSION_STRING}, - {"hash", SSC::VERSION_HASH_STRING}} - }, - {"host-operating-system", - #if defined(__APPLE__) - #if TARGET_IPHONE_SIMULATOR - "iphonesimulator" - #elif TARGET_OS_IPHONE - "iphoneos" - #else - "macosx" - #endif - #elif defined(__ANDROID__) - (router->bridge->isAndroidEmulator ? "android-emulator" : "android") - #elif defined(__WIN32) - "win32" - #elif defined(__linux__) - "linux" - #elif defined(__unix__) || defined(__unix) - "unix" - #else - "unknown" - #endif - } - }} - }; - reply(Result { message.seq, message, json }); - }); - - /** - * Returns pending post data typically returned in the response of an - * `ipc://post` IPC call intercepted by an XHR request. - * @param id The id of the post data. - */ - router->map("post", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - if (!router->core->hasPost(id)) { - return reply(Result::Err { message, JSON::Object::Entries { - {"id", std::to_string(id)}, - {"message", "Post not found for given 'id'"} - }}); - } - - auto result = Result { message.seq, message }; - result.post = router->core->getPost(id); - reply(result); - router->core->removePost(id); - }); - - /** - * Prints incoming message value to stdout. - */ - router->map("stdout", [](auto message, auto router, auto reply) { - #if defined(__APPLE__) - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_INFO, "%{public}s", message.value.c_str()); - #endif - IO::write(message.value, false); - reply(Result::Data { message, JSON::Object {}}); - }); - - /** - * Prints incoming message value to stderr. - */ - router->map("stderr", [](auto message, auto router, auto reply) { - #if defined(__APPLE__) - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", message.value.c_str()); - #endif - IO::write(message.value, true); - reply(Result::Data { message, JSON::Object {}}); - }); - - /** - * Binds an UDP socket to a specified port, and optionally a host - * address (default: 0.0.0.0). - * @param id Handle ID of underlying socket - * @param port Port to bind the UDP socket to - * @param address The address to bind the UDP socket to (default: 0.0.0.0) - * @param reuseAddr Reuse underlying UDP socket address (default: false) - */ - router->map("udp.bind", [](auto message, auto router, auto reply) { - Core::UDP::BindOptions options; - auto err = validateMessageParameters(message, {"id", "port"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(options.port, "port", std::stoi); - - options.reuseAddr = message.get("reuseAddr") == "true"; - options.address = message.get("address", "0.0.0.0"); - - router->core->udp.bind( - message.seq, - id, - options, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Close socket handle and underlying UDP socket. - * @param id Handle ID of underlying socket - */ - router->map("udp.close", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->udp.close(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); - }); - - /** - * Connects an UDP socket to a specified port, and optionally a host - * address (default: 0.0.0.0). - * @param id Handle ID of underlying socket - * @param port Port to connect the UDP socket to - * @param address The address to connect the UDP socket to (default: 0.0.0.0) - */ - router->map("udp.connect", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id", "port"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - Core::UDP::ConnectOptions options; - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(options.port, "port", std::stoi); - - options.address = message.get("address", "0.0.0.0"); - - router->core->udp.connect( - message.seq, - id, - options, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Disconnects a connected socket handle and underlying UDP socket. - * @param id Handle ID of underlying socket - */ - router->map("udp.disconnect", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->udp.disconnect( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Returns connected peer socket address information. - * @param id Handle ID of underlying socket - */ - router->map("udp.getPeerName", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->udp.getPeerName( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Returns local socket address information. - * @param id Handle ID of underlying socket - */ - router->map("udp.getSockName", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->udp.getSockName( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Returns socket state information. - * @param id Handle ID of underlying socket - */ - router->map("udp.getState", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->udp.getState( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Initializes socket handle to start receiving data from the underlying - * socket and route through the IPC bridge to the WebView. - * @param id Handle ID of underlying socket - */ - router->map("udp.readStart", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->udp.readStart( - message.seq, - id, - [message, reply](auto seq, auto json, auto post) { - reply(Result { seq, message, json, post }); - } - ); - }); - - /** - * Stops socket handle from receiving data from the underlying - * socket and routing through the IPC bridge to the WebView. - * @param id Handle ID of underlying socket - */ - router->map("udp.readStop", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - - router->core->udp.readStop( - message.seq, - id, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - /** - * Broadcasts a datagram on the socket. For connectionless sockets, the - * destination port and address must be specified. Connected sockets, on the - * other hand, will use their associated remote endpoint, so the port and - * address arguments must not be set. - * @param id Handle ID of underlying socket - * @param port The port to send data to - * @param size The size of the bytes to send - * @param bytes A pointer to the bytes to send - * @param address The address to send to (default: 0.0.0.0) - * @param ephemeral Indicates that the socket handle, if created is ephemeral and should eventually be destroyed - */ - router->map("udp.send", [](auto message, auto router, auto reply) { - auto err = validateMessageParameters(message, {"id", "port"}); - - if (err.type != JSON::Type::Null) { - return reply(Result::Err { message, err }); - } - - Core::UDP::SendOptions options; - uint64_t id; - REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); - REQUIRE_AND_GET_MESSAGE_VALUE(options.port, "port", std::stoi); - - options.size = message.buffer.size; - options.bytes = message.buffer.bytes; - options.address = message.get("address", "0.0.0.0"); - options.ephemeral = message.get("ephemeral") == "true"; - - router->core->udp.send( - message.seq, - id, - options, - RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) - ); - }); - - router->map("window.showFileSystemPicker", [](auto message, auto router, auto reply) { - const auto allowMultiple = message.get("allowMultiple") == "true"; - const auto allowFiles = message.get("allowFiles") == "true"; - const auto allowDirs = message.get("allowDirs") == "true"; - const auto isSave = message.get("type") == "save"; - - const auto contentTypeSpecs = message.get("contentTypeSpecs"); - const auto defaultName = message.get("defaultName"); - const auto defaultPath = message.get("defaultPath"); - const auto title = message.get("title", isSave ? "Save" : "Open"); - - Dialog dialog; - auto options = Dialog::FileSystemPickerOptions { - .directories = allowDirs, - .multiple = allowMultiple, - .files = allowFiles, - .contentTypes = contentTypeSpecs, - .defaultName = defaultName, - .defaultPath = defaultPath, - .title = title - }; - - if (isSave) { - const auto result = dialog.showSaveFilePicker(options); - - if (result.size() == 0) { - auto err = JSON::Object::Entries {{"type", "AbortError"}}; - reply(Result::Err { message, err }); - } else { - auto data = JSON::Object::Entries { - {"paths", JSON::Array::Entries{result}} - }; - reply(Result::Data { message, data }); - } - } else { - JSON::Array paths; - const auto results = ( - allowFiles && !allowDirs - ? dialog.showOpenFilePicker(options) - : dialog.showDirectoryPicker(options) - ); - - for (const auto& result : results) { - paths.push(result); - } - - auto data = JSON::Object::Entries { - {"paths", paths} - }; - - reply(Result::Data { message, data }); - } - }); -} - -static void registerSchemeHandler (Router *router) { - static auto userConfig = SSC::getUserConfig(); - static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; - -#if defined(__linux__) && !defined(__ANDROID__) - auto ctx = router->webkitWebContext; - auto security = webkit_web_context_get_security_manager(ctx); - - webkit_web_context_register_uri_scheme(ctx, "ipc", [](auto request, auto ptr) { - auto uri = String(webkit_uri_scheme_request_get_uri(request)); - auto router = reinterpret_cast<Router *>(ptr); - auto invoked = router->invoke(uri, [=](auto result) { - auto json = result.str(); - auto size = result.post.body != nullptr ? result.post.length : json.size(); - auto body = result.post.body != nullptr ? result.post.body : json.c_str(); - - char* data = nullptr; - - if (size > 0) { - data = new char[size]{0}; - memcpy(data, body, size); - } - - auto stream = g_memory_input_stream_new_from_data(data, size, nullptr); - auto headers = soup_message_headers_new(SOUP_MESSAGE_HEADERS_RESPONSE); - auto response = webkit_uri_scheme_response_new(stream, size); - - for (const auto& header : result.headers.entries) { - soup_message_headers_append(headers, header.key.c_str(), header.value.c_str()); - } - - if (result.post.body) { - webkit_uri_scheme_response_set_content_type(response, IPC_BINARY_CONTENT_TYPE); - } else { - webkit_uri_scheme_response_set_content_type(response, IPC_JSON_CONTENT_TYPE); - } - - webkit_uri_scheme_request_finish_with_response(request, response); - g_input_stream_close_async(stream, 0, nullptr, +[]( - GObject* object, - GAsyncResult* asyncResult, - gpointer userData - ) { - auto stream = (GInputStream*) object; - g_input_stream_close_finish(stream, asyncResult, nullptr); - g_object_unref(stream); - g_idle_add_full( - G_PRIORITY_DEFAULT_IDLE, - (GSourceFunc) [](gpointer userData) { - return G_SOURCE_REMOVE; - }, - userData, - [](gpointer userData) { - delete [] static_cast<char *>(userData); - } - ); - }, data); - }); - - if (!invoked) { - auto err = JSON::Object::Entries { - {"source", uri}, - {"err", JSON::Object::Entries { - {"message", "Not found"}, - {"type", "NotFoundError"}, - {"url", uri} - }} - }; - - auto msg = JSON::Object(err).str(); - auto size = msg.size(); - auto bytes = msg.c_str(); - auto stream = g_memory_input_stream_new_from_data(bytes, size, 0); - auto response = webkit_uri_scheme_response_new(stream, msg.size()); - - webkit_uri_scheme_response_set_status(response, 404, "Not found"); - webkit_uri_scheme_response_set_content_type(response, IPC_JSON_CONTENT_TYPE); - webkit_uri_scheme_request_finish_with_response(request, response); - g_object_unref(stream); - } - }, - router, - 0); - - webkit_web_context_register_uri_scheme(ctx, "socket", [](auto request, auto ptr) { - static auto userConfig = SSC::getUserConfig(); - bool isModule = false; - auto uri = String(webkit_uri_scheme_request_get_uri(request)); - auto cwd = getcwd(); - - if (uri.starts_with("socket:///")) { - uri = uri.substr(10); - } else if (uri.starts_with("socket://")) { - uri = uri.substr(9); - } else if (uri.starts_with("socket:")) { - uri = uri.substr(7); - } - - auto path = String( - uri.starts_with(bundleIdentifier) - ? uri.substr(bundleIdentifier.size()) - : "socket/" + uri - ); - - auto ext = fs::path(path).extension().string(); - - if (ext.size() > 0 && !ext.starts_with(".")) { - ext = "." + ext; - } - - if (!uri.starts_with(bundleIdentifier)) { - if (ext.size() == 0 && !path.ends_with(".js")) { - path += ".js"; - ext = ".js"; - } - - uri = "socket://" + bundleIdentifier + "/" + path; - auto moduleSource = trim(tmpl( - moduleTemplate, - Map { {"url", String(uri)} } - )); - - auto size = moduleSource.size(); - auto bytes = moduleSource.data(); - auto stream = g_memory_input_stream_new_from_data(bytes, size, 0); - auto response = webkit_uri_scheme_response_new(stream, size); - - webkit_uri_scheme_response_set_content_type(response, SOCKET_MODULE_CONTENT_TYPE); - webkit_uri_scheme_request_finish_with_response(request, response); - g_object_unref(stream); - return; - } - - auto parsedPath = Router::parseURL(path); - auto resolved = Router::resolveURLPathForWebView(parsedPath.path, cwd); - auto mount = Router::resolveNavigatorMountForWebView(parsedPath.path); - path = resolved.path; - - if (mount.path.size() > 0) { - if (mount.resolution.redirect) { - auto redirectURL = resolved.path; - if (parsedPath.queryString.size() > 0) { - redirectURL += "?" + parsedPath.queryString; - } - - if (parsedPath.fragment.size() > 0) { - redirectURL += "#" + parsedPath.fragment; - } - - auto redirectSource = String( - "<meta http-equiv=\"refresh\" content=\"0; url='" + redirectURL + "'\" />" - ); - - auto size = redirectSource.size(); - auto bytes = redirectSource.data(); - auto stream = g_memory_input_stream_new_from_data(bytes, size, 0); - auto headers = soup_message_headers_new(SOUP_MESSAGE_HEADERS_RESPONSE); - auto response = webkit_uri_scheme_response_new(stream, (gint64) size); - - soup_message_headers_append(headers, "location", redirectURL.c_str()); - soup_message_headers_append(headers, "content-location", redirectURL.c_str()); - - webkit_uri_scheme_response_set_http_headers(response, headers); - webkit_uri_scheme_response_set_content_type(response, "text/html"); - webkit_uri_scheme_request_finish_with_response(request, response); - - g_object_unref(stream); - return; - } - } else if (path.size() == 0 && userConfig.contains("webview_default_index")) { - path = userConfig["webview_default_index"]; - } else if (resolved.redirect) { - auto redirectURL = resolved.path; - if (parsedPath.queryString.size() > 0) { - redirectURL += "?" + parsedPath.queryString; - } - - if (parsedPath.fragment.size() > 0) { - redirectURL += "#" + parsedPath.fragment; - } - - auto redirectSource = String( - "<meta http-equiv=\"refresh\" content=\"0; url='" + redirectURL + "'\" />" - ); - - auto size = redirectSource.size(); - auto bytes = redirectSource.data(); - auto stream = g_memory_input_stream_new_from_data(bytes, size, 0); - auto headers = soup_message_headers_new(SOUP_MESSAGE_HEADERS_RESPONSE); - auto response = webkit_uri_scheme_response_new(stream, (gint64) size); - - soup_message_headers_append(headers, "location", redirectURL.c_str()); - soup_message_headers_append(headers, "content-location", redirectURL.c_str()); - - webkit_uri_scheme_response_set_http_headers(response, headers); - webkit_uri_scheme_response_set_content_type(response, "text/html"); - webkit_uri_scheme_request_finish_with_response(request, response); - - g_object_unref(stream); - return; - } - - if (mount.path.size() > 0) { - path = mount.path; - } else if (path.size() > 0) { - path = fs::absolute(fs::path(cwd) / path.substr(1)).string(); - } - - if (path.size() == 0 || !fs::exists(path)) { - auto stream = g_memory_input_stream_new_from_data(nullptr, 0, 0); - auto response = webkit_uri_scheme_response_new(stream, 0); - - webkit_uri_scheme_response_set_status(response, 404, "Not found"); - webkit_uri_scheme_request_finish_with_response(request, response); - g_object_unref(stream); - return; - } - - GError* error = nullptr; - - auto file = g_file_new_for_path(path.c_str()); - auto stream = (GInputStream*) g_file_read(file, nullptr, &error); - - if (!stream) { - webkit_uri_scheme_request_finish_error(request, error); - g_error_free(error); - return; - } - - gchar* mimeType = nullptr; - auto size = fs::file_size(path); - auto headers = soup_message_headers_new(SOUP_MESSAGE_HEADERS_RESPONSE); - auto response = webkit_uri_scheme_response_new(stream, (gint64) size); - auto webviewHeaders = split(userConfig["webview_headers"], '\n'); - - soup_message_headers_append(headers, "access-control-allow-origin", "*"); - soup_message_headers_append(headers, "access-control-allow-methods", "*"); - soup_message_headers_append(headers, "access-control-allow-headers", "*"); - - for (const auto& line : webviewHeaders) { - auto pair = split(trim(line), ':'); - auto key = trim(pair[0]); - auto value = trim(pair[1]); - soup_message_headers_append(headers, key.c_str(), value.c_str()); - } - - webkit_uri_scheme_response_set_http_headers(response, headers); - - if (path.ends_with(".wasm")) { - webkit_uri_scheme_response_set_content_type(response, "application/wasm"); - } else if (path.ends_with(".cjs") || path.ends_with(".mjs")) { - webkit_uri_scheme_response_set_content_type(response, "text/javascript"); - } else if (path.ends_with(".ts")) { - webkit_uri_scheme_response_set_content_type(response, "application/typescript"); - } else { - mimeType = g_content_type_guess(path.c_str(), nullptr, 0, nullptr); - if (mimeType) { - webkit_uri_scheme_response_set_content_type(response, mimeType); - } else { - webkit_uri_scheme_response_set_content_type(response, SOCKET_MODULE_CONTENT_TYPE); - } - } - - webkit_uri_scheme_request_finish_with_response(request, response); - g_object_unref(stream); - - if (mimeType) { - g_free(mimeType); - } - }, - router, - 0); - - webkit_security_manager_register_uri_scheme_as_display_isolated(security, "ipc"); - webkit_security_manager_register_uri_scheme_as_cors_enabled(security, "ipc"); - webkit_security_manager_register_uri_scheme_as_secure(security, "ipc"); - webkit_security_manager_register_uri_scheme_as_local(security, "ipc"); - - static const auto devHost = SSC::getDevHost(); - - if (devHost.starts_with("http:")) { - webkit_security_manager_register_uri_scheme_as_display_isolated(security, "http"); - webkit_security_manager_register_uri_scheme_as_cors_enabled(security, "http"); - webkit_security_manager_register_uri_scheme_as_secure(security, "http"); - webkit_security_manager_register_uri_scheme_as_local(security, "http"); - } - - webkit_security_manager_register_uri_scheme_as_display_isolated(security, "socket"); - webkit_security_manager_register_uri_scheme_as_cors_enabled(security, "socket"); - webkit_security_manager_register_uri_scheme_as_secure(security, "socket"); - webkit_security_manager_register_uri_scheme_as_local(security, "socket"); -#endif -} - -#if defined(__APPLE__) -@implementation SSCIPCSchemeHandler -{ - SSC::Mutex mutex; - std::unordered_map<Task, IPC::Message> tasks; -} - -- (void) enqueueTask: (Task) task withMessage: (IPC::Message) message { - Lock lock(mutex); - if (task != nullptr && !tasks.contains(task)) { - tasks.emplace(task, message); - } -} - -- (void) finalizeTask: (Task) task { - Lock lock(mutex); - if (task != nullptr && tasks.contains(task)) { - tasks.erase(task); - } -} - -- (bool) waitingForTask: (Task) task { - Lock lock(mutex); - return task != nullptr && tasks.contains(task); -} - -- (void) webView: (SSCBridgedWebView*) webview stopURLSchemeTask: (Task) task { - Lock lock(mutex); - if (tasks.contains(task)) { - auto message = tasks[task]; - if (message.cancel->handler != nullptr) { - message.cancel->handler(message.cancel->data); - } - } - [self finalizeTask: task]; -} - -- (void) webView: (SSCBridgedWebView*) webview startURLSchemeTask: (Task) task { - static auto userConfig = SSC::getUserConfig(); - static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; - static auto fileManager = [[NSFileManager alloc] init]; - - auto request = task.request; - auto url = String(request.URL.absoluteString.UTF8String); - auto message = Message(url, true); - message.isHTTP = true; - message.cancel = std::make_shared<MessageCancellation>(); - - auto webviewHeaders = split(userConfig["webview_headers"], '\n'); - auto headers = [NSMutableDictionary dictionary]; - - for (const auto& line : webviewHeaders) { - auto pair = split(trim(line), ':'); - auto key = [NSString stringWithUTF8String: trim(pair[0]).c_str()]; - auto value = [NSString stringWithUTF8String: trim(pair[1]).c_str()]; - headers[key] = value; - } - - headers[@"access-control-allow-origin"] = @"*"; - headers[@"access-control-allow-methods"] = @"*"; - headers[@"access-control-allow-headers"] = @"*"; - - if (String(request.HTTPMethod.UTF8String) == "OPTIONS") { - auto response = [[NSHTTPURLResponse alloc] - initWithURL: request.URL - statusCode: 200 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - [task didFinish]; - #if !__has_feature(objc_arc) - [response release]; - #endif - - return; - } - - if (String(request.URL.scheme.UTF8String) == "socket") { - auto host = request.URL.host; - auto components = [NSURLComponents - componentsWithURL: request.URL - resolvingAgainstBaseURL: YES - ]; - - components.scheme = @"file"; - components.host = request.URL.host; - - NSData* data = nullptr; - bool isModule = false; - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - const auto basePath = String(NSBundle.mainBundle.resourcePath.UTF8String) + "/ui"; - #else - const auto basePath = String(NSBundle.mainBundle.resourcePath.UTF8String); - #endif - auto path = String(components.path.UTF8String); - - auto ext = String( - components.URL.pathExtension.length > 0 - ? components.URL.pathExtension.UTF8String - : "" - ); - - if (ext.size() > 0 && !ext.starts_with(".")) { - ext = "." + ext; - } - - // assumes `import 'socket:<bundle_identifier>/module'` syntax - if (host == nullptr && path.starts_with(bundleIdentifier)) { - host = @(path.substr(0, bundleIdentifier.size()).c_str()); - path = path.substr(bundleIdentifier.size()); - - if (ext.size() == 0 && !path.ends_with(".js")) { - path += ".js"; - } - - components.path = @(path.c_str()); - } - - if ( - host.UTF8String != nullptr && - String(host.UTF8String) == bundleIdentifier - ) { - auto parsedPath = Router::parseURL(path); - auto resolved = Router::resolveURLPathForWebView(path, basePath); - auto mount = Router::resolveNavigatorMountForWebView(path); - path = resolved.path; - - if (mount.path.size() > 0) { - if (mount.resolution.redirect) { - auto redirectURL = mount.resolution.path + "?" + parsedPath.queryString + "#" + parsedPath.fragment; - auto redirectSource = String( - "<meta http-equiv=\"refresh\" content=\"0; url='" + redirectURL + "'\" />" - ); - - data = [@(redirectSource.c_str()) dataUsingEncoding: NSUTF8StringEncoding]; - - auto response = [[NSHTTPURLResponse alloc] - initWithURL: [NSURL URLWithString: @(redirectURL.c_str())] - statusCode: 200 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - [task didReceiveData: data]; - [task didFinish]; - - #if !__has_feature(objc_arc) - [response release]; - #endif - return; - } else { - auto url = [NSURL fileURLWithPath: @(mount.path.c_str())]; - - if (path.ends_with(".wasm")) { - headers[@"content-type"] = @("application/wasm"); - } else if (path.ends_with(".ts")) { - headers[@"content-type"] = @("application/typescript"); - } else if (path.ends_with(".cjs") || path.ends_with(".mjs")) { - headers[@"content-type"] = @("text/javascript"); - } else if (components.URL.pathExtension != nullptr) { - auto types = [UTType - typesWithTag: components.URL.pathExtension - tagClass: UTTagClassFilenameExtension - conformingToType: nullptr - ]; - - if (types.count > 0 && types.firstObject.preferredMIMEType) { - headers[@"content-type"] = types.firstObject.preferredMIMEType; - } - } - - [url startAccessingSecurityScopedResource]; - const auto data = [NSData dataWithContentsOfURL: url]; - headers[@"content-length"] = [@(data.length) stringValue]; - [url stopAccessingSecurityScopedResource]; - - auto response = [[NSHTTPURLResponse alloc] - initWithURL: request.URL - statusCode: 200 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - [task didReceiveData: data]; - [task didFinish]; - - #if !__has_feature(objc_arc) - [response release]; - #endif - return; - } - } else if (path.size() == 0) { - if (userConfig.contains("webview_default_index")) { - path = userConfig["webview_default_index"]; - } else { - auto response = [[NSHTTPURLResponse alloc] - initWithURL: request.URL - statusCode: 404 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - [task didFinish]; - - #if !__has_feature(objc_arc) - [response release]; - #endif - return; - } - } else if (resolved.redirect) { - auto redirectURL = path + "?" + parsedPath.queryString + "#" + parsedPath.fragment; - auto redirectSource = String( - "<meta http-equiv=\"refresh\" content=\"0; url='" + redirectURL + "'\" />" - ); - - data = [@(redirectSource.c_str()) dataUsingEncoding: NSUTF8StringEncoding]; - - auto response = [[NSHTTPURLResponse alloc] - initWithURL: [NSURL URLWithString: @(redirectURL.c_str())] - statusCode: 200 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - [task didReceiveData: data]; - [task didFinish]; - - #if !__has_feature(objc_arc) - [response release]; - #endif - return; - } - - components.host = @(""); - - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - components.path = [[[NSBundle mainBundle] resourcePath] - stringByAppendingPathComponent: [NSString - stringWithFormat: @"/ui/%s", path.c_str() - ] - ]; - #else - components.path = [[[NSBundle mainBundle] resourcePath] - stringByAppendingPathComponent: [NSString - stringWithFormat: @"/%s", path.c_str() - ] - ]; - #endif - - if (String(request.HTTPMethod.UTF8String) == "GET") { - data = [NSData dataWithContentsOfURL: components.URL]; - } - - components.host = request.URL.host; - } else { - isModule = true; - if (ext.size() == 0 && !path.ends_with(".js")) { - path += ".js"; - } - - auto prefix = String( - path.starts_with(bundleIdentifier) - ? "" - : "socket/" - ); - - path = replace(path, bundleIdentifier + "/", ""); - - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - components.path = [[[NSBundle mainBundle] resourcePath] - stringByAppendingPathComponent: [NSString - stringWithFormat: @"/ui/%s%s", prefix.c_str(), path.c_str() - ] - ]; - #else - components.path = [[[NSBundle mainBundle] resourcePath] - stringByAppendingPathComponent: [NSString - stringWithFormat: @"/%s%s", prefix.c_str(), path.c_str() - ] - ]; - #endif - auto moduleUri = "socket://" + bundleIdentifier + "/" + prefix + path; - auto moduleSource = trim(tmpl( - moduleTemplate, - Map { {"url", String(moduleUri)} } - )); - - if (String(request.HTTPMethod.UTF8String) == "GET") { - data = [@(moduleSource.c_str()) dataUsingEncoding: NSUTF8StringEncoding]; - } - } - - auto exists = [fileManager - fileExistsAtPath: components.path - isDirectory: NULL]; - - components.path = @(path.c_str()); - - if (exists && data) { - headers[@"content-length"] = [@(data.length) stringValue]; - if (isModule && data.length > 0) { - headers[@"content-type"] = @"text/javascript"; - } else if (path.ends_with(".ts")) { - headers[@"content-type"] = @("application/typescript"); - } else if (path.ends_with(".cjs") || path.ends_with(".mjs")) { - headers[@"content-type"] = @("text/javascript"); - } else if (path.ends_with(".wasm")) { - headers[@"content-type"] = @("application/wasm"); - } else if (components.URL.pathExtension != nullptr) { - auto types = [UTType - typesWithTag: components.URL.pathExtension - tagClass: UTTagClassFilenameExtension - conformingToType: nullptr - ]; - - if (types.count > 0 && types.firstObject.preferredMIMEType) { - headers[@"content-type"] = types.firstObject.preferredMIMEType; - } - } - } - - components.scheme = @("socket"); - headers[@"content-location"] = components.URL.absoluteString; - - auto statusCode = exists ? 200 : 404; - auto response = [[NSHTTPURLResponse alloc] - initWithURL: components.URL - statusCode: statusCode - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - - if (data && data.length > 0) { - [task didReceiveData: data]; - } - [task didFinish]; - - #if !__has_feature(objc_arc) - [response release]; - #endif - - return; - } - - if (message.name == "post") { - auto id = std::stoull(message.get("id")); - auto post = self.router->core->getPost(id); - - headers[@"content-length"] = [@(post.length) stringValue]; - - if (post.headers.size() > 0) { - auto lines = SSC::split(SSC::trim(post.headers), '\n'); - - for (auto& line : lines) { - auto pair = split(trim(line), ':'); - auto key = [NSString stringWithUTF8String: trim(pair[0]).c_str()]; - auto value = [NSString stringWithUTF8String: trim(pair[1]).c_str()]; - headers[key] = value; - } - } - - auto response = [[NSHTTPURLResponse alloc] - initWithURL: request.URL - statusCode: 200 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - - if (post.body) { - auto data = [NSData dataWithBytes: post.body length: post.length]; - [task didReceiveData: data]; - } else { - auto string = [NSString stringWithUTF8String: ""]; - auto data = [string dataUsingEncoding: NSUTF8StringEncoding]; - [task didReceiveData: data]; - } - - [task didFinish]; - #if !__has_feature(objc_arc) - [response release]; - #endif - - self.router->core->removePost(id); - return; - } - - size_t bufsize = 0; - char *body = NULL; - - // if there is a body on the reuqest, pass it into the method router. - auto rawBody = request.HTTPBody; - - if (rawBody) { - const void* data = [rawBody bytes]; - bufsize = [rawBody length]; - body = (char *) data; - } - - [self enqueueTask: task withMessage: message]; - - auto invoked = self.router->invoke(message, body, bufsize, [=](Result result) { - // @TODO Communicate task cancellation to the route, so it can cancel its work. - if (![self waitingForTask: task]) { - return; - } - - auto id = result.id; - auto headers = [NSMutableDictionary dictionary]; - - headers[@"access-control-allow-origin"] = @"*"; - headers[@"access-control-allow-methods"] = @"*"; - headers[@"access-control-allow-headers"] = @"*"; - - for (const auto& header : result.headers.entries) { - auto key = [NSString stringWithUTF8String: trim(header.key).c_str()]; - auto value = [NSString stringWithUTF8String: trim(header.value.str()).c_str()]; - headers[key] = value; - } - - NSData* data = nullptr; - if (result.post.event_stream != nullptr) { - *result.post.event_stream = [=]( - const char* name, - const char* data, - bool finished - ) { - if (![self waitingForTask: task]) { - return false; - } - - auto event_name = [NSString stringWithUTF8String: name]; - auto event_data = [NSString stringWithUTF8String: data]; - - if (event_name.length > 0 || event_data.length > 0) { - auto event = - event_name.length > 0 && event_data.length > 0 - ? [NSString stringWithFormat:@"event: %@\ndata: %@\n\n", - event_name, event_data] - : event_data.length > 0 - ? [NSString stringWithFormat:@"data: %@\n\n", event_data] - : [NSString stringWithFormat:@"event: %@\n\n", event_name]; - - [task didReceiveData: [event dataUsingEncoding:NSUTF8StringEncoding]]; - } - - if (finished) { - [task didFinish]; - [self finalizeTask: task]; - } - - return true; - }; - headers[@"content-type"] = @"text/event-stream"; - headers[@"cache-control"] = @"no-store"; - } else if (result.post.chunk_stream != nullptr) { - *result.post.chunk_stream = [=]( - const char* chunk, - size_t chunk_size, - bool finished - ) { - if (![self waitingForTask: task]) { - return false; - } - - [task didReceiveData:[NSData dataWithBytes:chunk length:chunk_size]]; - - if (finished) { - [task didFinish]; - [self finalizeTask: task]; - } - return true; - }; - headers[@"transfer-encoding"] = @"chunked"; - } else { - std::string json; - const char* body; - size_t size; - if (result.post.body != nullptr) { - body = result.post.body; - size = result.post.length; - } else { - json = result.str(); - body = json.c_str(); - size = json.size(); - headers[@"content-type"] = @"application/json"; - } - headers[@"content-length"] = @(size).stringValue; - data = [NSData dataWithBytes: body length: size]; - } - - auto response = [[NSHTTPURLResponse alloc] - initWithURL: task.request.URL - statusCode: 200 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - if (data != nullptr) { - [task didReceiveData: data]; - [task didFinish]; - [self finalizeTask: task]; - } - - #if !__has_feature(objc_arc) - [response release]; - #endif - }); - - if (!invoked) { - auto headers = [NSMutableDictionary dictionary]; - auto json = JSON::Object::Entries { - {"err", JSON::Object::Entries { - {"message", "Not found"}, - {"type", "NotFoundError"}, - {"url", url} - }} - }; - - auto msg = JSON::Object(json).str(); - auto str = [NSString stringWithUTF8String: msg.c_str()]; - auto data = [str dataUsingEncoding: NSUTF8StringEncoding]; - - headers[@"access-control-allow-origin"] = @"*"; - headers[@"access-control-allow-headers"] = @"*"; - headers[@"content-length"] = [@(msg.size()) stringValue]; - - auto response = [[NSHTTPURLResponse alloc] - initWithURL: request.URL - statusCode: 404 - HTTPVersion: @"HTTP/1.1" - headerFields: headers - ]; - - [task didReceiveResponse: response]; - [task didReceiveData: data]; - [task didFinish]; - #if !__has_feature(objc_arc) - [response release]; - #endif - } -} -@end - -@implementation SSCLocationPositionWatcher -+ (SSCLocationPositionWatcher*) positionWatcherWithIdentifier: (NSInteger) identifier - completion: (void (^)(CLLocation*)) completion { - auto watcher= [SSCLocationPositionWatcher new]; - watcher.identifier = identifier; - watcher.completion = [completion copy]; - return watcher; -} -@end - -@implementation SSCLocationObserver -- (id) init { - self = [super init]; - self.delegate = [[SSCLocationManagerDelegate alloc] initWithLocationObserver: self]; - self.isAuthorized = NO; - self.locationWatchers = [NSMutableArray new]; - self.activationCompletions = [NSMutableArray new]; - self.locationRequestCompletions = [NSMutableArray new]; - - self.locationManager = [CLLocationManager new]; - self.locationManager.delegate = self.delegate; - self.locationManager.desiredAccuracy = CLAccuracyAuthorizationFullAccuracy; - self.locationManager.pausesLocationUpdatesAutomatically = NO; - -#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - self.locationManager.allowsBackgroundLocationUpdates = YES; - self.locationManager.showsBackgroundLocationIndicator = YES; -#endif - - if ([CLLocationManager locationServicesEnabled]) { - if ( - #if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR - self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorized || - #else - self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse || - #endif - self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways - ) { - self.isAuthorized = YES; - } - } - - return self; -} - -- (BOOL) attemptActivation { - if ([CLLocationManager locationServicesEnabled] == NO) { - return NO; - } - - if (self.isAuthorized) { - [self.locationManager requestLocation]; - return YES; - } - -#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE - [self.locationManager requestWhenInUseAuthorization]; -#else - [self.locationManager requestAlwaysAuthorization]; -#endif - - return YES; -} - -- (BOOL) attemptActivationWithCompletion: (void (^)(BOOL)) completion { - if (self.isAuthorized) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(YES); - }); - return YES; - } - - if ([self attemptActivation]) { - [self.activationCompletions addObject: [completion copy]]; - return YES; - } - - return NO; -} - -- (BOOL) getCurrentPositionWithCompletion: (void (^)(NSError*, CLLocation*)) completion { - return [self attemptActivationWithCompletion: ^(BOOL isAuthorized) { - static auto userConfig = SSC::getUserConfig(); - if (!isAuthorized) { - auto reason = @("Location observer could not be activated"); - - if (!self.locationManager) { - reason = @("Location observer manager is not initialized"); - } else if (!self.locationManager.location) { - reason = @("Location observer manager could not provide location"); - } - - auto error = [NSError - errorWithDomain: @(userConfig["bundle_identifier"].c_str()) - code: -1 - userInfo: @{ - NSLocalizedDescriptionKey: reason - } - ]; - - return completion(error, nullptr); - } - - auto location = self.locationManager.location; - if (location.timestamp.timeIntervalSince1970 > 0) { - completion(nullptr, self.locationManager.location); - } else { - [self.locationRequestCompletions addObject: [completion copy]]; - } - - [self.locationManager requestLocation]; - }]; -} - -- (int) watchPositionForIdentifier: (NSInteger) identifier - completion: (void (^)(NSError*, CLLocation*)) completion { - SSCLocationPositionWatcher* watcher = nullptr; - BOOL exists = NO; - - for (SSCLocationPositionWatcher* existing in self.locationWatchers) { - if (existing.identifier == identifier) { - watcher = existing; - exists = YES; - break; - } - } - - if (!watcher) { - watcher = [SSCLocationPositionWatcher - positionWatcherWithIdentifier: identifier - completion: ^(CLLocation* location) { - completion(nullptr, location); - }]; - } - - auto performedActivation = [self attemptActivationWithCompletion: ^(BOOL isAuthorized) { - static auto userConfig = SSC::getUserConfig(); - if (!isAuthorized) { - auto error = [NSError - errorWithDomain: @(userConfig["bundle_identifier"].c_str()) - code: -1 - userInfo: @{ - @"Error reason": @("Location observer could not be activated") - } - ]; - - return completion(error, nullptr); - } - - [self.locationManager startUpdatingLocation]; - - if (CLLocationManager.headingAvailable) { - [self.locationManager startUpdatingHeading]; - } - - [self.locationManager startMonitoringSignificantLocationChanges]; - }]; - - if (!performedActivation) { - #if !__has_feature(objc_arc) - [watcher release]; - #endif - return -1; - } - - if (!exists) { - [self.locationWatchers addObject: watcher]; - } - - return identifier; -} - -- (BOOL) clearWatch: (NSInteger) identifier { - for (SSCLocationPositionWatcher* watcher in self.locationWatchers) { - if (watcher.identifier == identifier) { - [self.locationWatchers removeObject: watcher]; - #if !__has_feature(objc_arc) - [watcher release]; - #endif - return YES; - } - } - - return NO; -} -@end - -@implementation SSCLocationManagerDelegate -- (id) initWithLocationObserver: (SSCLocationObserver*) locationObserver { - self = [super init]; - self.locationObserver = locationObserver; - locationObserver.delegate = self; - return self; -} - -- (void) locationManager: (CLLocationManager*) locationManager - didUpdateLocations: (NSArray<CLLocation*>*) locations { - auto locationRequestCompletions = [NSArray arrayWithArray: self.locationObserver.locationRequestCompletions]; - for (id item in locationRequestCompletions) { - auto completion = (void (^)(CLLocation*)) item; - completion(locations.firstObject); - [self.locationObserver.locationRequestCompletions removeObject: item]; - #if !__has_feature(objc_arc) - [completion release]; - #endif - } - - for (SSCLocationPositionWatcher* watcher in self.locationObserver.locationWatchers) { - watcher.completion(locations.firstObject); - } -} - -- (void) locationManager: (CLLocationManager*) locationManager - didFailWithError: (NSError*) error { - // TODO(@jwerle): handle location manager error - debug("locationManager:didFailWithError: %@", error); -} - -- (void) locationManager: (CLLocationManager*) locationManager - didFinishDeferredUpdatesWithError: (NSError*) error { - debug("locationManager:didFinishDeferredUpdatesWithError: %@", error); - // TODO(@jwerle): handle deferred error -} - -- (void) locationManagerDidPauseLocationUpdates: (CLLocationManager*) locationManager { - // TODO(@jwerle): handle pause for updates - debug("locationManagerDidPauseLocationUpdates"); -} - -- (void) locationManagerDidResumeLocationUpdates: (CLLocationManager*) locationManager { - // TODO(@jwerle): handle resume for updates - debug("locationManagerDidResumeLocationUpdates"); -} - -- (void) locationManager: (CLLocationManager*) locationManager - didVisit: (CLVisit*) visit { - auto locations = [NSArray arrayWithObject: locationManager.location]; - [self locationManager: locationManager didUpdateLocations: locations]; -} - -- (void) locationManager: (CLLocationManager*) locationManager - didChangeAuthorizationStatus: (CLAuthorizationStatus) status { - // XXX(@jwerle): this is a legacy callback - [self locationManagerDidChangeAuthorization: locationManager]; -} - -- (void) locationManagerDidChangeAuthorization: (CLLocationManager*) locationManager { - auto activationCompletions = [NSArray arrayWithArray: self.locationObserver.activationCompletions]; - if ( - #if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR - locationManager.authorizationStatus == kCLAuthorizationStatusAuthorized || - #else - locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse || - #endif - locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways - ) { - JSON::Object json = JSON::Object::Entries { - {"name", "geolocation"}, - {"state", "granted"} - }; - - self.locationObserver.router->emit("permissionchange", json.str()); - self.locationObserver.isAuthorized = YES; - for (id item in activationCompletions) { - auto completion = (void (^)(BOOL)) item; - completion(YES); - [self.locationObserver.activationCompletions removeObject: item]; - #if !__has_feature(objc_arc) - [completion release]; - #endif - } - } else { - JSON::Object json = JSON::Object::Entries { - {"name", "geolocation"}, - {"state", locationManager.authorizationStatus == kCLAuthorizationStatusNotDetermined - ? "prompt" - : "denied" - } - }; - - self.locationObserver.router->emit("permissionchange", json.str()); - self.locationObserver.isAuthorized = NO; - for (id item in activationCompletions) { - auto completion = (void (^)(BOOL)) item; - completion(NO); - [self.locationObserver.activationCompletions removeObject: item]; - #if !__has_feature(objc_arc) - [completion release]; - #endif - } - } -} -@end - -@implementation SSCUserNotificationCenterDelegate -- (void) userNotificationCenter: (UNUserNotificationCenter*) center - didReceiveNotificationResponse: (UNNotificationResponse*) response - withCompletionHandler: (void (^)(void)) completionHandler { - completionHandler(); - Lock lock(notificationRouterMapMutex); - auto id = String(response.notification.request.identifier.UTF8String); - Router* router = notificationRouterMap.find(id) != notificationRouterMap.end() - ? notificationRouterMap.at(id) - : nullptr; - - if (router) { - JSON::Object json = JSON::Object::Entries { - {"id", id}, - {"action", - [response.actionIdentifier isEqualToString: UNNotificationDefaultActionIdentifier] - ? "default" - : "dismiss" - } - }; - - notificationRouterMap.erase(id); - router->emit("notificationresponse", json.str()); - } -} - -- (void) userNotificationCenter: (UNUserNotificationCenter*) center - willPresentNotification: (UNNotification*) notification - withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler { - UNNotificationPresentationOptions options = UNNotificationPresentationOptionList; - - if (notification.request.content.sound != nullptr) { - options |= UNNotificationPresentationOptionSound; - } - - if (notification.request.content.attachments != nullptr) { - if (notification.request.content.attachments.count > 0) { - options |= UNNotificationPresentationOptionBanner; - } - } - - completionHandler(options); - - Lock lock(notificationRouterMapMutex); - auto __block id = String(notification.request.identifier.UTF8String); - Router* __block router = notificationRouterMap.find(id) != notificationRouterMap.end() - ? notificationRouterMap.at(id) - : nullptr; + response.writeHead(404); + callback(response); + }); - if (router) { - JSON::Object json = JSON::Object::Entries { - {"id", id} - }; + this->schemeHandlers.registerSchemeHandler("node", [this]( + const auto request, + const auto router, + auto callbacks, + auto callback + ) { + if (request->method == "OPTIONS") { + auto response = SchemeHandlers::Response(request); + response.writeHead(204); + callback(response); + return; + } - router->emit("notificationpresented", json.str()); - // look for dismissed notification - auto timer = [NSTimer timerWithTimeInterval: 2 repeats: YES block: ^(NSTimer* timer) { - auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; - [notificationCenter getDeliveredNotificationsWithCompletionHandler: ^(NSArray<UNNotification*> *notifications) { - BOOL found = NO; + auto userConfig = this->userConfig; + auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + // the location of static application resources + const auto applicationResources = FileResource::getResourcesPath().string(); + // default response is 404 + auto response = SchemeHandlers::Response(request, 404); + + // the resouce path that may be request + String resourcePath; + + // the content location relative to the request origin + String contentLocation; + + // module or stdlib import/fetch `socket:<module>/<path>` which will just + // proxy an import into a normal resource request above + if (request->hostname.size() == 0) { + const auto isAllowedNodeCoreModule = allowedNodeCoreModules.end() != std::find( + allowedNodeCoreModules.begin(), + allowedNodeCoreModules.end(), + request->pathname.substr(1) + ); - for (UNNotification* notification in notifications) { - if (String(notification.request.identifier.UTF8String) == id) { - return; - } + if (!isAllowedNodeCoreModule) { + response.writeHead(404); + return callback(response); } - [timer invalidate]; - JSON::Object json = JSON::Object::Entries { - {"id", id}, - {"action", "dismiss"} - }; + auto pathname = request->pathname; - router->emit("notificationresponse", json.str()); - - Lock lock(notificationRouterMapMutex); - if (notificationRouterMap.contains(id)) { - notificationRouterMap.erase(id); + if (!pathname.ends_with(".js")) { + pathname += ".js"; } - }]; - }]; - - [NSRunLoop.mainRunLoop - addTimer: timer - forMode: NSDefaultRunLoopMode - ]; - } -} -@end -#endif - -namespace SSC::IPC { - Bridge::Bridge (Core *core) : router() { - static auto userConfig = SSC::getUserConfig(); - - this->core = core; - this->router.core = core; - this->router.bridge = this; - this->bluetooth.sendFunction = [this]( - const String& seq, - const JSON::Any value, - const SSC::Post post - ) { - this->router.send(seq, value.str(), post); - }; - - this->bluetooth.emitFunction = [this]( - const String& seq, - const JSON::Any value - ) { - this->router.emit(seq, value.str()); - }; + if (!pathname.starts_with("/")) { + pathname = "/" + pathname; + } - #if !defined(__ANDROID__) && (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR)) - if (isDebugEnabled() && userConfig["webview_watch"] == "true") { - this->fileSystemWatcher = new FileSystemWatcher(getcwd()); - this->fileSystemWatcher->core = this->core; - this->fileSystemWatcher->start([=, this]( - const auto& path, - const auto& events, - const auto& context - ) mutable { - auto json = JSON::Object::Entries { - {"path", std::filesystem::relative(path, getcwd()).string()} - }; + contentLocation = "/socket" + pathname; + resourcePath = applicationResources + contentLocation; - auto result = SSC::IPC::Result(json); - this->router.emit("filedidchange", result.json().str()); - }); - } - #endif - } + auto resource = FileResource(resourcePath, { .cache = true }); - Bridge::~Bridge () { - #if !defined(__ANDROID__) && (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR)) - if (this->fileSystemWatcher) { - this->fileSystemWatcher->stop(); - delete this->fileSystemWatcher; - } - #endif - } + if (!resource.exists()) { + if (!pathname.ends_with(".js")) { + pathname = request->pathname; - bool Router::hasMappedBuffer (int index, const Message::Seq seq) { - Lock lock(this->mutex); - auto key = std::to_string(index) + seq; - return this->buffers.find(key) != this->buffers.end(); - } + if (!pathname.starts_with("/")) { + pathname = "/" + pathname; + } - MessageBuffer Router::getMappedBuffer (int index, const Message::Seq seq) { - if (this->hasMappedBuffer(index, seq)) { - Lock lock(this->mutex); - auto key = std::to_string(index) + seq; - return this->buffers.at(key); - } + if (pathname.ends_with("/")) { + pathname = pathname.substr(0, pathname.size() - 1); + } - return MessageBuffer {}; - } + contentLocation = "/socket" + pathname + "/index.js"; + resourcePath = applicationResources + contentLocation; + } - void Router::setMappedBuffer ( - int index, - const Message::Seq seq, - MessageBuffer buffer - ) { - Lock lock(this->mutex); - auto key = std::to_string(index) + seq; - this->buffers.insert_or_assign(key, buffer); - } + resource = FileResource(resourcePath, { .cache = true }); + } - void Router::removeMappedBuffer (int index, const Message::Seq seq) { - Lock lock(this->mutex); - if (this->hasMappedBuffer(index, seq)) { - auto key = std::to_string(index) + seq; - this->buffers.erase(key); - } - } + if (resource.exists()) { + const auto url = ( + #if SOCKET_RUNTIME_PLATFORM_ANDROID + "https://" + + #else + "socket://" + + #endif + bundleIdentifier + + "/socket" + + pathname + ); + const auto moduleImportProxy = tmpl( + String(resource.read()).find("export default") != String::npos + ? ESM_IMPORT_PROXY_TEMPLATE_WITH_DEFAULT_EXPORT + : ESM_IMPORT_PROXY_TEMPLATE_WITHOUT_DEFAULT_EXPORT, + Map { + {"url", url}, + {"commit", VERSION_HASH_STRING}, + {"protocol", "node"}, + {"pathname", pathname}, + {"specifier", pathname.substr(1)}, + {"bundle_identifier", bundleIdentifier} + } + ); - bool Bridge::route (const String& uri, const char *bytes, size_t size) { - return this->route(uri, bytes, size, nullptr); - } + const auto contentType = resource.mimeType(); - bool Bridge::route ( - const String& uri, - const char* bytes, - size_t size, - Router::ResultCallback callback - ) { - if (callback != nullptr) { - return this->router.invoke(uri, bytes, size, callback); - } else { - return this->router.invoke(uri, bytes, size); - } - } + if (contentType.size() > 0) { + response.setHeader("content-type", contentType); + } - /* - - . - ├── a-conflict-index - │ └── index.html - ├── a-conflict-index.html - ├── an-index-file - │ ├── a-html-file.html - │ └── index.html - ├── another-file.html - └── index.html - - Subtleties: - Direct file navigation always wins - /foo/index.html have precedent over foo.html - /foo redirects to /foo/ when there is a /foo/index.html - - '/' -> '/index.html' - '/index.html' -> '/index.html' - '/a-conflict-index' -> redirect to '/a-conflict-index/' - '/another-file' -> '/another-file.html' - '/another-file.html' -> '/another-file.html' - '/an-index-file/' -> '/an-index-file/index.html' - '/an-index-file' -> redirect to '/an-index-file/' - '/an-index-file/a-html-file' -> '/an-index-file/a-html-file.html' - */ - Router::WebViewURLPathResolution Router::resolveURLPathForWebView (String inputPath, const String& basePath) { - namespace fs = std::filesystem; - - if (inputPath.starts_with("/")) { - inputPath = inputPath.substr(1); - } + response.setHeader("content-length", moduleImportProxy.size()); - // Resolve the full path - fs::path fullPath = (fs::path(basePath) / fs::path(inputPath)).make_preferred(); + if (contentLocation.size() > 0) { + response.setHeader("content-location", contentLocation); + } - // 1. Try the given path if it's a file - if (fs::is_regular_file(fullPath)) { - return Router::WebViewURLPathResolution{"/" + replace(fs::relative(fullPath, basePath).string(), "\\\\", "/")}; - } + response.writeHead(200); + response.write(trim(moduleImportProxy)); + } - // 2. Try appending a `/` to the path and checking for an index.html - fs::path indexPath = fullPath / fs::path("index.html"); - if (fs::is_regular_file(indexPath)) { - if (fullPath.string().ends_with("\\") || fullPath.string().ends_with("/")) { - return Router::WebViewURLPathResolution{ - .path = "/" + replace(fs::relative(indexPath, basePath).string(), "\\\\", "/"), - .redirect = false - }; - } else { - return Router::WebViewURLPathResolution{ - .path = "/" + replace(fs::relative(fullPath, basePath).string(), "\\\\", "/") + "/", - .redirect = true - }; + return callback(response); } - } - - // 3. Check if appending a .html file extension gives a valid file - fs::path htmlPath = fullPath; - htmlPath.replace_extension(".html"); - if (fs::is_regular_file(htmlPath)) { - return Router::WebViewURLPathResolution{"/" + replace(fs::relative(htmlPath, basePath).string(), "\\\\", "/")}; - } - - // If no valid path is found, return empty string - return Router::WebViewURLPathResolution{}; - }; - - Router::WebViewURLComponents Router::parseURL(const SSC::String& url) { - Router::WebViewURLComponents components; - components.originalUrl = url; - - size_t queryPos = url.find('?'); - size_t fragmentPos = url.find('#'); - - if (queryPos != SSC::String::npos) { - components.path = url.substr(0, queryPos); - } else if (fragmentPos != SSC::String::npos) { - components.path = url.substr(0, fragmentPos); - } else { - components.path = url; - } - - if (queryPos != SSC::String::npos) { // Extract the query string - components.queryString = url.substr(queryPos + 1, fragmentPos != SSC::String::npos ? fragmentPos - queryPos - 1 : SSC::String::npos); - } - - if (fragmentPos != SSC::String::npos) { // Extract the fragment - components.fragment = url.substr(fragmentPos + 1); - } - - return components; - } - - static const Map getWebViewNavigatorMounts () { - static const auto userConfig = getUserConfig(); - #if defined(_WIN32) - static const auto HOME = Env::get("HOMEPATH", Env::get("USERPROFILE", Env::get("HOME"))); - #elif defined(__APPLE__) && (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR) - static const auto HOME = String(NSHomeDirectory().UTF8String); - #else - static const auto uid = getuid(); - static const auto pwuid = getpwuid(uid); - static const auto HOME = pwuid != nullptr - ? String(pwuid->pw_dir) - : Env::get("HOME", getcwd()); - #endif - - static Map mounts; - - if (mounts.size() > 0) { - return mounts; - } - Map mappings = { - {"\\$HOST_HOME", HOME}, - {"~", HOME}, - - {"\\$HOST_CONTAINER", - #if defined(__APPLE__) - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - [NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSUserDomainMask, YES) objectAtIndex: 0].UTF8String - #else - // `homeDirectoryForCurrentUser` resolves to sandboxed container - // directory when in "sandbox" mode, otherwise the user's HOME directory - NSFileManager.defaultManager.homeDirectoryForCurrentUser.absoluteString.UTF8String - #endif - #elif defined(__linux__) - // TODO(@jwerle): figure out `$HOST_CONTAINER` for Linux - getcwd(), - #elif defined(_WIN32) - // TODO(@jwerle): figure out `$HOST_CONTAINER` for Windows - getcwd(), - #endif - }, + response.writeHead(404); + callback(response); + }); - {"\\$HOST_PROCESS_WORKING_DIRECTORY", - #if defined(__APPLE__) - NSBundle.mainBundle.resourcePath.UTF8String - #else - getcwd(), - #endif - } + Map protocolHandlers = { + {"npm", "/socket/npm/service-worker.js"} }; - for (const auto& tuple : userConfig) { - if (tuple.first.starts_with("webview_navigator_mounts_")) { - auto key = replace(tuple.first, "webview_navigator_mounts_", ""); - - if (key.starts_with("android") && !platform.android) continue; - if (key.starts_with("ios") && !platform.ios) continue; - if (key.starts_with("linux") && !platform.linux) continue; - if (key.starts_with("mac") && !platform.mac) continue; - if (key.starts_with("win") && !platform.win) continue; - - key = replace(key, "android_", ""); - key = replace(key, "ios_", ""); - key = replace(key, "linux_", ""); - key = replace(key, "mac_", ""); - key = replace(key, "win_", ""); - - String path = key; - - for (const auto& map : mappings) { - path = replace(path, map.first, map.second); - } - - const auto& value = tuple.second; - mounts.insert_or_assign(path, value); + for (const auto& entry : split(this->userConfig["webview_protocol-handlers"], " ")) { + const auto scheme = replace(trim(entry), ":", ""); + if (this->navigator.serviceWorker.protocols.registerHandler(scheme)) { + protocolHandlers.insert_or_assign(scheme, ""); } } - return mounts; - } - - Router::WebViewNavigatorMount Router::resolveNavigatorMountForWebView (const String& path) { - static const auto mounts = getWebViewNavigatorMounts(); - - for (const auto& tuple : mounts) { - if (path.starts_with(tuple.second)) { - const auto relative = replace(path, tuple.second, ""); - const auto resolution = resolveURLPathForWebView(relative, tuple.first); - if (resolution.path.size() > 0) { - const auto resolved = Path(tuple.first) / resolution.path.substr(1); - return WebViewNavigatorMount { - resolution, - resolved.string(), - path - }; - } else { - const auto resolved = relative.starts_with("/") - ? Path(tuple.first) / relative.substr(1) - : Path(tuple.first) / relative; - - return WebViewNavigatorMount { - resolution, - resolved.string(), - path - }; + for (const auto& entry : this->userConfig) { + const auto& key = entry.first; + if (key.starts_with("webview_protocol-handlers_")) { + const auto scheme = replace(replace(trim(key), "webview_protocol-handlers_", ""), ":", "");; + const auto data = entry.second; + if (this->navigator.serviceWorker.protocols.registerHandler(scheme, { data })) { + protocolHandlers.insert_or_assign(scheme, data); } } } - return WebViewNavigatorMount {}; - } - - Router::Router () { - static auto userConfig = SSC::getUserConfig(); - - #if defined(__APPLE__) - this->networkStatusObserver = [SSCIPCNetworkStatusObserver new]; - this->locationObserver = [SSCLocationObserver new]; - this->schemeHandler = [SSCIPCSchemeHandler new]; - - [this->schemeHandler setRouter: this]; - [this->locationObserver setRouter: this]; - [this->networkStatusObserver setRouter: this]; - - #elif defined(__linux__) && !defined(__ANDROID__) - this->webkitWebContext = webkit_web_context_new(); - #endif - - initRouterTable(this); - registerSchemeHandler(this); - - this->preserveCurrentTable(); - - #if defined(__APPLE__) - [this->networkStatusObserver start]; + for (const auto& entry : protocolHandlers) { + const auto& scheme = entry.first; + const auto id = rand64(); - if (userConfig["permissions_allow_notifications"] != "false") { - auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + auto scriptURL = trim(entry.second); - if (!notificationCenter.delegate) { - notificationCenter.delegate = [SSCUserNotificationCenterDelegate new]; + if (scriptURL.size() == 0) { + continue; } - UNAuthorizationStatus __block currentAuthorizationStatus; - [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { - currentAuthorizationStatus = settings.authorizationStatus; - this->notificationPollTimer = [NSTimer timerWithTimeInterval: 2 repeats: YES block: ^(NSTimer* timer) { - // look for authorization status changes - [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { - if (currentAuthorizationStatus != settings.authorizationStatus) { - JSON::Object json; - currentAuthorizationStatus = settings.authorizationStatus; - if (settings.authorizationStatus == UNAuthorizationStatusDenied) { - json = JSON::Object::Entries { - {"name", "notifications"}, - {"state", "denied"} - }; - } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { - json = JSON::Object::Entries { - {"name", "notifications"}, - {"state", "prompt"} - }; - } else { - json = JSON::Object::Entries { - {"name", "notifications"}, - {"state", "granted"} - }; - } - - this->emit("permissionchange", json.str()); - } - }]; - }]; - - [NSRunLoop.mainRunLoop - addTimer: this->notificationPollTimer - forMode: NSDefaultRunLoopMode - ]; - }]; - } - #endif - } - - Router::~Router () { - #if defined(__APPLE__) - if (this->networkStatusObserver != nullptr) { - #if !__has_feature(objc_arc) - [this->networkStatusObserver release]; - #endif - } - - if (this->locationObserver != nullptr) { - #if !__has_feature(objc_arc) - [this->locationObserver release]; - #endif - } - - if (this->schemeHandler != nullptr) { - #if !__has_feature(objc_arc) - [this->schemeHandler release]; - #endif - } - - if (this->notificationPollTimer) { - [this->notificationPollTimer invalidate]; - #if !__has_feature(objc_arc) - [this->notificationPollTimer release]; - #endif - } - - this->notificationPollTimer = nullptr; - this->networkStatusObserver = nullptr; - this->locationObserver = nullptr; - this->schemeHandler = nullptr; - #endif - } - - void Router::preserveCurrentTable () { - Lock lock(mutex); - this->preserved = this->table; - } - - uint64_t Router::listen (const String& name, MessageCallback callback) { - Lock lock(mutex); - - if (!this->listeners.contains(name)) { - this->listeners[name] = std::vector<MessageCallbackListenerContext>(); - } - - auto& listeners = this->listeners.at(name); - auto token = rand64(); - listeners.push_back(MessageCallbackListenerContext { token , callback }); - return token; - } - - bool Router::unlisten (const String& name, uint64_t token) { - Lock lock(mutex); - - if (!this->listeners.contains(name)) { - return false; - } - - auto& listeners = this->listeners.at(name); - for (int i = 0; i < listeners.size(); ++i) { - const auto& listener = listeners[i]; - if (listener.token == token) { - listeners.erase(listeners.begin() + i); - return true; + if (!scriptURL.starts_with(".") && !scriptURL.starts_with("/")) { + continue; } - } - return false; - } + if (scriptURL.starts_with(".")) { + scriptURL = scriptURL.substr(1, scriptURL.size()); + } - void Router::map (const String& name, MessageCallback callback) { - return this->map(name, true, callback); - } + String scope = "/"; - void Router::map (const String& name, bool async, MessageCallback callback) { - Lock lock(mutex); + auto scopeParts = split(scriptURL, "/"); + if (scopeParts.size() > 0) { + scopeParts = Vector<String>(scopeParts.begin(), scopeParts.end() - 1); + scope = join(scopeParts, "/"); + } - String data = name; - // URI hostnames are not case sensitive. Convert to lowercase. - std::transform(data.begin(), data.end(), data.begin(), - [](unsigned char c) { return std::tolower(c); }); - if (callback != nullptr) { - table.insert_or_assign(data, MessageCallbackContext { async, callback }); - } - } + scriptURL = ( + #if SOCKET_RUNTIME_PLATFORM_ANDROID + "https://" + + #else + "socket://" + + #endif + this->userConfig["meta_bundle_identifier"] + + scriptURL + ); - void Router::unmap (const String& name) { - Lock lock(mutex); + this->navigator.serviceWorker.registerServiceWorker({ + .type = ServiceWorkerContainer::RegistrationOptions::Type::Module, + .scope = scope, + .scriptURL = scriptURL, + .scheme = scheme, + .id = id + }); - String data = name; - // URI hostnames are not case sensitive. Convert to lowercase. - std::transform(data.begin(), data.end(), data.begin(), - [](unsigned char c) { return std::tolower(c); }); - if (table.find(data) != table.end()) { - table.erase(data); - } - } + this->schemeHandlers.registerSchemeHandler(scheme, [this]( + auto request, + auto bridge, + auto callbacks, + auto callback + ) { + auto app = App::sharedApplication(); + auto window = app->windowManager.getWindowForBridge(bridge); - bool Router::invoke (const String& uri, const char *bytes, size_t size) { - return this->invoke(uri, bytes, size, [this](auto result) { - this->send(result.seq, result.str(), result.post); - }); - } + if (window == nullptr) { + auto response = SchemeHandlers::Response(request); + response.writeHead(400); + callback(response); + return; + } - bool Router::invoke (const String& uri, ResultCallback callback) { - return this->invoke(uri, nullptr, 0, callback); - } + if (request->method == "OPTIONS") { + auto response = SchemeHandlers::Response(request); + response.writeHead(204); + callback(response); + return; + } - bool Router::invoke ( - const String& uri, - const char *bytes, - size_t size, - ResultCallback callback - ) { - auto message = Message(uri, true); - return this->invoke(message, bytes, size, callback); - } + if (this->navigator.serviceWorker.registrations.size() > 0) { + auto hostname = request->hostname; + auto pathname = request->pathname; - bool Router::invoke ( - const Message& message, - const char *bytes, - size_t size, - ResultCallback callback - ) { - auto name = message.name; - MessageCallbackContext ctx; + if (request->scheme == "npm") { + if (hostname.size() > 0) { + pathname = "/" + hostname; + } + hostname = this->userConfig["meta_bundle_identifier"]; + } - // URI hostnames are not case sensitive. Convert to lowercase. - std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { - return std::tolower(c); - }); + const auto scope = this->navigator.serviceWorker.protocols.getServiceWorkerScope(request->scheme); - do { - // lookup router function in the preserved table, - // then the public table, return if unable to determine a context - if (this->preserved.find(name) != this->preserved.end()) { - ctx = this->preserved.at(name); - } else if (this->table.find(name) != this->table.end()) { - ctx = this->table.at(name); - } else { - return false; - } - } while (0); + if (scope.size() > 0) { + pathname = scope + pathname; + } - if (ctx.callback != nullptr) { - Message msg(message); - // decorate message with buffer if buffer was previously - // mapped with `ipc://buffer.map`, which we do on Linux - if (this->hasMappedBuffer(msg.index, msg.seq)) { - msg.buffer = this->getMappedBuffer(msg.index, msg.seq); - this->removeMappedBuffer(msg.index, msg.seq); - } else if (bytes != nullptr && size > 0) { - // alloc and copy `bytes` into `msg.buffer.bytes - caller owns `bytes` - // `msg.buffer.bytes` is free'd in CLEANUP_AFTER_INVOKE_CALLBACK - msg.buffer.bytes = new char[size]{0}; - msg.buffer.size = size; - memcpy(msg.buffer.bytes, bytes, size); - } + const auto fetch = ServiceWorkerContainer::FetchRequest { + request->method, + request->scheme, + hostname, + pathname, + request->query, + request->headers, + ServiceWorkerContainer::FetchBody { request->body.size, request->body.bytes }, + request->client + }; - // named listeners - do { - auto listeners = this->listeners[name]; - for (const auto& listener : listeners) { - listener.callback(msg, this, [](const auto& _) {}); - } - } while (0); + const auto fetched = this->navigator.serviceWorker.fetch(fetch, [request, callback] (auto res) mutable { + if (!request->isActive()) { + return; + } - // wild card (*) listeners - do { - auto listeners = this->listeners["*"]; - for (const auto& listener : listeners) { - listener.callback(msg, this, [](const auto& _) {}); - } - } while (0); + auto response = SchemeHandlers::Response(request); - if (ctx.async) { - auto dispatched = this->dispatch([ctx, msg, callback, this]() mutable { - ctx.callback(msg, this, [msg, callback, this](const auto result) mutable { - if (result.seq == "-1") { - this->send(result.seq, result.str(), result.post); + if (res.statusCode == 0) { + response.fail("ServiceWorker request failed"); } else { - callback(result); + response.writeHead(res.statusCode, res.headers); + response.write(res.body.size, res.body.bytes); } - CLEANUP_AFTER_INVOKE_CALLBACK(this, msg, result); + callback(response); }); - }); - - if (!dispatched) { - CLEANUP_AFTER_INVOKE_CALLBACK(this, msg, Result{}); - } - return dispatched; - } else { - ctx.callback(msg, this, [msg, callback, this](const auto result) mutable { - if (result.seq == "-1") { - this->send(result.seq, result.str(), result.post); - } else { - callback(result); + if (fetched) { + this->core->setTimeout(32000, [request] () mutable { + if (request->isActive()) { + auto response = SchemeHandlers::Response(request, 408); + response.fail("Protocol handler ServiceWorker request timed out."); + } + }); + return; } - CLEANUP_AFTER_INVOKE_CALLBACK(this, msg, result); - }); + } - return true; - } + auto response = SchemeHandlers::Response(request); + response.writeHead(404); + callback(response); + }); } + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + static const int MAX_ALLOWED_SCHEME_ORIGINS = 64; + static const int MAX_CUSTOM_SCHEME_REGISTRATIONS = 64; + Microsoft::WRL::ComPtr<ICoreWebView2EnvironmentOptions4> options; + Vector<SharedPointer<WString>> schemes; + Vector<SharedPointer<WString>> origins; - return false; - } + if (this->schemeHandlers.configuration.webview.As(&options) == S_OK) { + ICoreWebView2CustomSchemeRegistration* registrations[MAX_CUSTOM_SCHEME_REGISTRATIONS] = {}; + const WCHAR* allowedOrigins[MAX_ALLOWED_SCHEME_ORIGINS] = {}; - bool Router::send ( - const Message::Seq& seq, - const String data, - const Post post - ) { - if (post.body || seq == "-1") { - auto script = this->core->createPost(seq, data, post); - return this->evaluateJavaScript(script); - } + int registrationsCount = 0; + int allowedOriginsCount = 0; - auto value = encodeURIComponent(data); - auto script = getResolveToRenderProcessJavaScript( - seq.size() == 0 ? "-1" : seq, - "0", - value - ); + allowedOrigins[allowedOriginsCount++] = L"about://*"; + allowedOrigins[allowedOriginsCount++] = L"https://*"; - return this->evaluateJavaScript(script); - } + for (const auto& entry : this->schemeHandlers.handlers) { + const auto origin = entry.first + "://*"; + origins.push_back(std::make_shared<WString>(convertStringToWString(origin))); + allowedOrigins[allowedOriginsCount++] = origins.back()->c_str(); + } - bool Router::emit ( - const String& name, - const String data - ) { - auto value = encodeURIComponent(data); - auto script = getEmitToRenderProcessJavaScript(name, value); - return this->evaluateJavaScript(script); - } + // store registratino refs here + Set<Microsoft::WRL::ComPtr<CoreWebView2CustomSchemeRegistration>> registrationsSet; - bool Router::evaluateJavaScript (const String js) { - if (this->evaluateJavaScriptFunction != nullptr) { - this->evaluateJavaScriptFunction(js); - return true; - } + for (const auto& entry : this->schemeHandlers.handlers) { + schemes.push_back(std::make_shared<WString>(convertStringToWString(entry.first))); + auto registration = Microsoft::WRL::Make<CoreWebView2CustomSchemeRegistration>( + schemes.back()->c_str() + ); - return false; - } + registration->SetAllowedOrigins(allowedOriginsCount, allowedOrigins); + if (entry.first != "npm") { + registration->put_HasAuthorityComponent(true); + } + registration->put_TreatAsSecure(true); + registrations[registrationsCount++] = registration.Get(); + registrationsSet.insert(registration); + } - bool Router::dispatch (DispatchCallback callback) { - if (this->dispatchFunction != nullptr) { - this->dispatchFunction(callback); - return true; + options->SetCustomSchemeRegistrations( + registrationsCount, + static_cast<ICoreWebView2CustomSchemeRegistration**>(registrations) + ); } + #endif + } - return false; + void Bridge::configureNavigatorMounts () { + this->navigator.configureMounts(); } } -#if defined(__APPLE__) -@implementation SSCIPCNetworkStatusObserver -- (id) init { - self = [super init]; - dispatch_queue_attr_t attrs = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_SERIAL, - QOS_CLASS_UTILITY, - DISPATCH_QUEUE_PRIORITY_DEFAULT - ); - - _monitorQueue = dispatch_queue_create( - "co.socketsupply.queue.ipc.network-status-observer", - attrs - ); - - // self.monitor = nw_path_monitor_create_with_type(nw_interface_type_wifi); - _monitor = nw_path_monitor_create(); - nw_path_monitor_set_queue(_monitor, _monitorQueue); - nw_path_monitor_set_update_handler(_monitor, ^(nw_path_t path) { - if (path == nullptr) { - return; - } - - nw_path_status_t status = nw_path_get_status(path); - - String name; - String message; +#if SOCKET_RUNTIME_PLATFORM_ANDROID +extern "C" { + jboolean ANDROID_EXTERNAL(ipc, Bridge, emit) ( + JNIEnv* env, + jobject self, + jint index, + jstring eventString, + jstring dataString + ) { + using namespace SSC; + auto app = App::sharedApplication(); - switch (status) { - case nw_path_status_invalid: { - name = "offline"; - message = "Network path is invalid"; - break; - } - case nw_path_status_satisfied: { - name = "online"; - message = "Network is usable"; - break; - } - case nw_path_status_satisfiable: { - name = "online"; - message = "Network may be usable"; - break; - } - case nw_path_status_unsatisfied: { - name = "offline"; - message = "Network is not usable"; - break; - } + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return false; } - auto json = JSON::Object::Entries { - {"message", message} - }; - - auto router = self.router; - self.router->dispatch([router, json, name] () { - auto data = JSON::Object(json); - router->emit(name, data.str()); - }); - }); + const auto window = app->windowManager.getWindow(index); - return self; -} + if (!window) { + ANDROID_THROW(env, "Invalid window requested"); + return false; + } -- (void) start { - nw_path_monitor_start(_monitor); + const auto event = Android::StringWrap(env, eventString).str(); + const auto data = Android::StringWrap(env, dataString).str(); + return window->bridge.emit(event, data); + } } -@end #endif diff --git a/src/ipc/bridge.hh b/src/ipc/bridge.hh new file mode 100644 index 0000000000..a4d395fd8e --- /dev/null +++ b/src/ipc/bridge.hh @@ -0,0 +1,88 @@ +#ifndef SOCKET_RUNTIME_IPC_BRIDGE_H +#define SOCKET_RUNTIME_IPC_BRIDGE_H + +#include "../core/core.hh" +#include "../core/options.hh" +#include "../core/webview.hh" + +#include "client.hh" +#include "preload.hh" +#include "navigator.hh" +#include "router.hh" +#include "scheme_handlers.hh" + +namespace SSC::IPC { + class Bridge { + public: + using EvaluateJavaScriptFunction = Function<void(const String)>; + using NavigateFunction = Function<void(const String&)>; + using DispatchCallback = Function<void()>; + using DispatchFunction = Function<void(DispatchCallback)>; + + struct Options : public SSC::Options { + Map userConfig = {}; + Preload::Options preload; + Options ( + const Map& userConfig = {}, + const Preload::Options& preload = {} + ); + }; + + static Vector<Bridge*> getInstances(); + + const CoreNetworkStatus::Observer networkStatusObserver; + const CoreGeolocation::PermissionChangeObserver geolocationPermissionChangeObserver; + const CoreNotifications::PermissionChangeObserver notificationsPermissionChangeObserver; + const CoreNotifications::NotificationResponseObserver notificationResponseObserver; + const CoreNotifications::NotificationPresentedObserver notificationPresentedObserver; + + EvaluateJavaScriptFunction evaluateJavaScriptFunction = nullptr; + NavigateFunction navigateFunction = nullptr; + DispatchFunction dispatchFunction = nullptr; + + Bluetooth bluetooth; + Client client = {}; + Map userConfig; + Navigator navigator; + Router router; + SchemeHandlers schemeHandlers; + + SharedPointer<Core> core = nullptr; + uint64_t id = 0; + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + bool isAndroidEmulator = false; + #endif + + Bridge (SharedPointer<Core>core, const Options& options); + Bridge () = delete; + Bridge (const Bridge&) = delete; + Bridge (Bridge&&) = delete; + ~Bridge (); + + Bridge& operator = (const Bridge&) = delete; + Bridge& operator = (Bridge&&) = delete; + + void init (); + void configureWebView (WebView* webview); + void configureSchemeHandlers (const SchemeHandlers::Configuration& configuration); + void configureNavigatorMounts (); + + bool route (const String& uri, SharedPointer<char[]> bytes, size_t size); + bool route ( + const String& uri, + SharedPointer<char[]> bytes, + size_t size, + Router::ResultCallback + ); + + bool evaluateJavaScript (const String& source); + bool dispatch (const DispatchCallback& callback); + bool navigate (const String& url); + bool emit (const String& name, const String& data = ""); + bool emit (const String& name, const JSON::Any& json = {}); + bool send (const Message::Seq& seq, const String& data, const Post& post = {}); + bool send (const Message::Seq& seq, const JSON::Any& json, const Post& post = {}); + }; +} +#endif diff --git a/src/ipc/bridge.kt b/src/ipc/bridge.kt new file mode 100644 index 0000000000..629aca7dde --- /dev/null +++ b/src/ipc/bridge.kt @@ -0,0 +1,127 @@ +// vim: set sw=2: +package socket.runtime.ipc + +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.webkit.WebView +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse + +import socket.runtime.app.App +import socket.runtime.core.console +import socket.runtime.core.WebViewClient +import socket.runtime.ipc.Navigator +import socket.runtime.ipc.SchemeHandlers +import socket.runtime.window.Window +import socket.runtime.window.WindowManagerActivity + +private fun isAndroidAssetsUri (uri: Uri): Boolean { + if (uri.pathSegments.size == 0) { + return false + } + + val scheme = uri.scheme + val host = uri.host + // handle no path segments, not currently required but future proofing + val path = uri.pathSegments?.get(0) + + if (host == "appassets.androidplatform.net") { + return true + } + + if (scheme == "file" && host == "" && path == "android_asset") { + return true + } + + return false +} + +open class Bridge ( + val index: Int, + val activity: WindowManagerActivity, + val window: Window +): WebViewClient() { + open val schemeHandlers = SchemeHandlers(this) + open val navigator = Navigator(this) + open val buffers = mutableMapOf<String, ByteArray>() + + override fun shouldOverrideUrlLoading ( + view: WebView, + request: WebResourceRequest + ): Boolean { + if (isAndroidAssetsUri(request.url)) { + return false + } + + val app = App.getInstance() + val bundleIdentifier = app.getUserConfigValue("meta_bundle_identifier") + + if (request.url.host == bundleIdentifier) { + return false + } + + if ( + request.url.scheme == "ipc" || + request.url.scheme == "node" || + request.url.scheme == "npm" || + request.url.scheme == "socket" + ) { + return false + } + + val scheme = request.url.scheme + if (scheme != null && schemeHandlers.hasHandlerForScheme(scheme)) { + return true + } + + val allowed = this.navigator.isNavigationRequestAllowed( + view.url ?: "", + request.url.toString() + ) + + if (allowed) { + return true + } + + val intent = Intent(Intent.ACTION_VIEW, request.url) + + try { + this.activity.startActivity(intent) + } catch (err: Exception) { + // TODO(jwerle): handle this error gracefully + console.error(err.toString()) + return false + } + + return true + } + + override fun shouldInterceptRequest ( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + return this.schemeHandlers.handleRequest(request) + } + + override fun onPageStarted ( + view: WebView, + url: String, + favicon: Bitmap? + ) { + val uri = Uri.parse(url) + if (uri.authority != "__BUNDLE_IDENTIFIER__") { + val preloadUserScript = this.window.getPreloadUserScript() + view.evaluateJavascript(preloadUserScript, { _ -> }) + } + + super.onPageStarted(view, url, favicon) + } + + fun emit (event: String, data: String): Boolean { + return this.emit(this.index, event, data) + } + + @Throws(Exception::class) + external fun emit (index: Int, event: String, data: String): Boolean +} diff --git a/src/ipc/client.hh b/src/ipc/client.hh new file mode 100644 index 0000000000..6db3f1b66d --- /dev/null +++ b/src/ipc/client.hh @@ -0,0 +1,14 @@ +#ifndef SOCKET_RUNTIME_IPC_CLIENT_H +#define SOCKET_RUNTIME_IPC_CLIENT_H + +#include "../core/unique_client.hh" +#include "preload.hh" + +namespace SSC::IPC { + struct Client : public UniqueClient { + using ID = UniqueClient::ID; + ID id = 0; + IPC::Preload preload = {}; + }; +} +#endif diff --git a/src/ipc/ipc.cc b/src/ipc/ipc.cc deleted file mode 100644 index a3dab5b060..0000000000 --- a/src/ipc/ipc.cc +++ /dev/null @@ -1,248 +0,0 @@ -#include "../core/core.hh" -#include "ipc.hh" -namespace SSC { - #if defined(_WIN32) - SSC::String FormatError(DWORD error, SSC::String source) { - SSC::StringStream message; - LPVOID lpMsgBuf; - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &lpMsgBuf, - 0, NULL ); - - message << "Error " << error << " in " << source << ": " << (LPTSTR)lpMsgBuf; - LocalFree(lpMsgBuf); - - return message.str(); - } - #endif -} -namespace SSC::IPC { - Message::Message (const Message& message) { - this->buffer.bytes = message.buffer.bytes; - this->buffer.size = message.buffer.size; - this->value = message.value; - this->index = message.index; - this->name = message.name; - this->seq = message.seq; - this->uri = message.uri; - this->args = message.args; - this->isHTTP = message.isHTTP; - this->cancel = message.cancel; - } - - Message::Message (const String& source, char *bytes, size_t size) - : Message(source, false, bytes, size) - {} - - Message::Message ( - const String& source, - bool decodeValues, - char *bytes, - size_t size - ) : Message(source, decodeValues) - { - this->buffer.bytes = bytes; - this->buffer.size = size; - } - - Message::Message (const String& source) - : Message(source, false) - {} - - Message::Message (const String& source, bool decodeValues) { - String str = source; - uri = str; - - // bail if missing protocol prefix - if (str.find("ipc://") == -1) return; - - // bail if malformed - if (str.compare("ipc://") == 0) return; - if (str.compare("ipc://?") == 0) return; - - String query; - String path; - - auto raw = split(str, '?'); - path = raw[0]; - if (raw.size() > 1) query = raw[1]; - - auto parts = split(path, '/'); - if (parts.size() >= 1) name = parts[1]; - - if (raw.size() != 2) return; - auto pairs = split(raw[1], '&'); - - for (auto& rawPair : pairs) { - auto pair = split(rawPair, '='); - if (pair.size() <= 1) continue; - - if (pair[0].compare("index") == 0) { - try { - index = std::stoi(pair[1].size() > 0 ? pair[1] : "0"); - } catch (...) { - debug("Warning: received non-integer index"); - } - } - - if (pair[0].compare("value") == 0) { - value = decodeURIComponent(pair[1]); - } - - if (pair[0].compare("seq") == 0) { - seq = decodeURIComponent(pair[1]); - } - - if (decodeValues) { - args[pair[0]] = decodeURIComponent(pair[1]); - } else { - args[pair[0]] = pair[1]; - } - } - } - - bool Message::has (const String& key) const { - return ( - this->args.find(key) != this->args.end() && - this->args.at(key).size() > 0 - ); - } - - String Message::get (const String& key) const { - return this->get(key, ""); - } - - String Message::get (const String& key, const String &fallback) const { - return args.count(key) ? decodeURIComponent(args.at(key)) : fallback; - } - - Result::Result ( - const Message::Seq& seq, - const Message& message - ) { - this->id = rand64(); - this->seq = seq; - this->message = message; - this->source = message.name; - this->post.workerId = this->message.get("runtime-worker-id"); - } - - Result::Result ( - const Message::Seq& seq, - const Message& message, - JSON::Any value - ) : Result(seq, message, value, Post{}) { - } - - Result::Result ( - const Message::Seq& seq, - const Message& message, - JSON::Any value, - Post post - ) : Result(seq, message) { - this->post = post; - this->headers = Headers(post.headers); - - if (this->post.workerId.size() == 0) { - this->post.workerId = this->message.get("runtime-worker-id"); - } - - if (value.type != JSON::Type::Any) { - this->value = value; - } - } - - Result::Result (const JSON::Any value) { - this->id = rand64(); - this->value = value; - } - - Result::Result (const Err error): Result(error.message.seq, error.message) { - this->err = error.value; - this->source = error.message.name; - } - - Result::Result (const Data data): Result(data.message.seq, data.message) { - this->data = data.value; - this->post = data.post; - this->source = data.message.name; - this->headers = Headers(data.post.headers); - } - - JSON::Any Result::json () const { - if (!this->value.isNull()) { - if (this->value.isObject()) { - auto object = this->value.as<JSON::Object>(); - - if (object.has("data") || object.has("err")) { - object["source"] = this->source; - object["id"] = std::to_string(this->id); - } - - return object; - } - - return this->value; - } - - auto entries = JSON::Object::Entries { - {"source", this->source}, - {"id", std::to_string(this->id)} - }; - - if (!this->err.isNull()) { - entries["err"] = this->err; - if (this->err.isObject()) { - if (this->err.as<JSON::Object>().has("id")) { - entries["id"] = this->err.as<JSON::Object>().get("id"); - } - } - } else if (!this->data.isNull()) { - entries["data"] = this->data; - if (this->data.isObject()) { - if (this->data.as<JSON::Object>().has("id")) { - entries["id"] = this->data.as<JSON::Object>().get("id"); - } - } - } - - return JSON::Object(entries); - } - - String Result::str () const { - auto json = this->json(); - return json.str(); - } - - Result::Err::Err ( - const Message& message, - JSON::Any value - ) { - this->seq = message.seq; - this->message = message; - this->value = value; - } - - Result::Data::Data ( - const Message& message, - JSON::Any value - ) : Data(message, value, Post{}) { - } - - Result::Data::Data ( - const Message& message, - JSON::Any value, - Post post - ) { - this->seq = message.seq; - this->message = message; - this->value = value; - this->post = post; - } -} diff --git a/src/ipc/ipc.hh b/src/ipc/ipc.hh index 4f37c264b3..86f75abbe1 100644 --- a/src/ipc/ipc.hh +++ b/src/ipc/ipc.hh @@ -1,350 +1,12 @@ -#ifndef SSC_IPC_H -#define SSC_IPC_H +#ifndef SOCKET_RUNTIME_IPC_H +#define SOCKET_RUNTIME_IPC_H -#include "../core/core.hh" +#include "bridge.hh" +#include "client.hh" +#include "message.hh" +#include "navigator.hh" +#include "result.hh" +#include "router.hh" +#include "scheme_handlers.hh" -// only available on desktop -#if !defined(__ANDROID__) && (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR)) -#include "../core/file_system_watcher.hh" -#endif - -namespace SSC::IPC { - class Router; - class Bridge; -} - -// create a proxy module so imports of the module of concern are imported -// exactly once at the canonical URL (file:///...) in contrast to module -// URLs (socket:...) - -static constexpr auto moduleTemplate = -R"S( -import module from '{{url}}' -export * from '{{url}}' -export default module -)S"; - -#if defined(__APPLE__) -namespace SSC::IPC { - using Task = id<WKURLSchemeTask>; - using Tasks = std::map<String, Task>; -} - -@class SSCBridgedWebView; -@interface SSCIPCSchemeHandler : NSObject<WKURLSchemeHandler> -@property (nonatomic) SSC::IPC::Router* router; -- (void) webView: (SSCBridgedWebView*) webview - startURLSchemeTask: (SSC::IPC::Task) task; -- (void) webView: (SSCBridgedWebView*) webview - stopURLSchemeTask: (SSC::IPC::Task) task; -@end - -@interface SSCIPCSchemeTasks : NSObject { - std::unique_ptr<SSC::IPC::Tasks> tasks; - SSC::Mutex mutex; -} -- (SSC::IPC::Task) get: (SSC::String) id; -- (void) remove: (SSC::String) id; -- (bool) has: (SSC::String) id; -- (void) put: (SSC::String) id task: (SSC::IPC::Task) task; -@end - -@interface SSCIPCNetworkStatusObserver : NSObject -@property (nonatomic, assign) dispatch_queue_t monitorQueue; -@property (nonatomic, assign) SSC::IPC::Router* router; -@property (nonatomic, assign) nw_path_monitor_t monitor; -- (id) init; -- (void) start; -@end - -@class SSCLocationObserver; -@interface SSCLocationManagerDelegate : NSObject<CLLocationManagerDelegate> -@property (nonatomic, strong) SSCLocationObserver* locationObserver; - -- (id) initWithLocationObserver: (SSCLocationObserver*) locationObserver; - -- (void) locationManager: (CLLocationManager*) locationManager - didFailWithError: (NSError*) error; - -- (void) locationManager: (CLLocationManager*) locationManager - didUpdateLocations: (NSArray<CLLocation*>*) locations; - -- (void) locationManager: (CLLocationManager*) locationManager - didFinishDeferredUpdatesWithError: (NSError*) error; - -- (void) locationManagerDidPauseLocationUpdates: (CLLocationManager*) locationManager; -- (void) locationManagerDidResumeLocationUpdates: (CLLocationManager*) locationManager; -- (void) locationManager: (CLLocationManager*) locationManager - didVisit: (CLVisit*) visit; - -- (void) locationManager: (CLLocationManager*) locationManager - didChangeAuthorizationStatus: (CLAuthorizationStatus) status; -- (void) locationManagerDidChangeAuthorization: (CLLocationManager*) locationManager; -@end - -@interface SSCLocationPositionWatcher : NSObject -@property (nonatomic, assign) NSInteger identifier; -@property (nonatomic, assign) void(^completion)(CLLocation*); -+ (SSCLocationPositionWatcher*) positionWatcherWithIdentifier: (NSInteger) identifier - completion: (void (^)(CLLocation*)) completion; -@end - -@interface SSCLocationObserver : NSObject -@property (nonatomic, retain) CLLocationManager* locationManager; -@property (nonatomic, retain) SSCLocationManagerDelegate* delegate; -@property (atomic, retain) NSMutableArray* activationCompletions; -@property (atomic, retain) NSMutableArray* locationRequestCompletions; -@property (atomic, retain) NSMutableArray* locationWatchers; -@property (nonatomic) SSC::IPC::Router* router; -@property (atomic, assign) BOOL isAuthorized; -- (BOOL) attemptActivation; -- (BOOL) attemptActivationWithCompletion: (void (^)(BOOL)) completion; -- (BOOL) getCurrentPositionWithCompletion: (void (^)(NSError*, CLLocation*)) completion; -- (int) watchPositionForIdentifier: (NSInteger) identifier - completion: (void (^)(NSError*, CLLocation*)) completion; -- (BOOL) clearWatch: (NSInteger) identifier; -@end - -@interface SSCUserNotificationCenterDelegate : NSObject<UNUserNotificationCenterDelegate> -- (void) userNotificationCenter: (UNUserNotificationCenter*) center - didReceiveNotificationResponse: (UNNotificationResponse*) response - withCompletionHandler: (void (^)(void)) completionHandler; - -- (void) userNotificationCenter: (UNUserNotificationCenter*) center - willPresentNotification: (UNNotification*) notification - withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler; -@end -#endif - -namespace SSC::IPC { - struct MessageBuffer { - size_t size = 0; - char* bytes = nullptr; - MessageBuffer(char* bytes, size_t size) - : size(size), bytes(bytes) { } - #ifdef _WIN32 - ICoreWebView2SharedBuffer* shared_buf = nullptr; - MessageBuffer(ICoreWebView2SharedBuffer* buf, size_t size) - : size(size), shared_buf(buf) { - BYTE* b = reinterpret_cast<BYTE*>(bytes); - HRESULT r = buf->get_Buffer(&b); - if (r != S_OK) { - // TODO(trevnorris): Handle this - } - } - #endif - MessageBuffer() = default; - }; - - struct MessageCancellation { - void (*handler)(void*) = nullptr; - void *data = nullptr; - }; - - class Message { - public: - using Seq = String; - MessageBuffer buffer; - String value = ""; - String name = ""; - String uri = ""; - int index = -1; - Seq seq = ""; - Map args; - bool isHTTP = false; - std::shared_ptr<MessageCancellation> cancel; - - Message () = default; - Message (const Message& message); - Message (const String& source, bool decodeValues); - Message (const String& source); - Message (const String& source, bool decodeValues, char *bytes, size_t size); - Message (const String& source, char *bytes, size_t size); - bool has (const String& key) const; - String get (const String& key) const; - String get (const String& key, const String& fallback) const; - String str () const { return this->uri; } - const char * c_str () const { return this->uri.c_str(); } - }; - - class Result { - public: - class Err { - public: - Message message; - Message::Seq seq; - JSON::Any value; - Err () = default; - Err (const Message&, JSON::Any); - }; - - class Data { - public: - Message message; - Message::Seq seq; - JSON::Any value; - Post post; - - Data () = default; - Data (const Message&, JSON::Any); - Data (const Message&, JSON::Any, Post); - }; - - Message message; - Message::Seq seq = "-1"; - uint64_t id = 0; - String source = ""; - JSON::Any value = nullptr; - JSON::Any data = nullptr; - JSON::Any err = nullptr; - Headers headers; - Post post; - - Result () = default; - Result (const Result&) = default; - Result (const JSON::Any); - Result (const Err error); - Result (const Data data); - Result (const Message::Seq&, const Message&); - Result (const Message::Seq&, const Message&, JSON::Any); - Result (const Message::Seq&, const Message&, JSON::Any, Post); - String str () const; - JSON::Any json () const; - }; - - class Router { - public: - using EvaluateJavaScriptCallback = std::function<void(const String)>; - using DispatchCallback = std::function<void()>; - using ReplyCallback = std::function<void(const Result&)>; - using ResultCallback = std::function<void(Result)>; - using MessageCallback = std::function<void(const Message&, Router*, ReplyCallback)>; - using BufferMap = std::map<String, MessageBuffer>; - - struct MessageCallbackContext { - bool async = true; - MessageCallback callback; - }; - - struct MessageCallbackListenerContext { - uint64_t token; - MessageCallback callback; - }; - - using Table = std::map<String, MessageCallbackContext>; - using Listeners = std::map<String, std::vector<MessageCallbackListenerContext>>; - - struct WebViewURLPathResolution { - String path = ""; - bool redirect = false; - }; - - struct WebViewNavigatorMount { - WebViewURLPathResolution resolution; // absolute URL resolution - String path; // root path on host file system - String route; // root path in webview navigator - }; - - struct WebViewURLComponents { - String originalUrl; - String queryString; - String fragment; - String path; - }; - - static WebViewURLComponents parseURL(const String& url); - static WebViewURLPathResolution resolveURLPathForWebView (String inputPath, const String& basePath); - static WebViewNavigatorMount resolveNavigatorMountForWebView (const String& path); - - private: - Table preserved; - - public: - EvaluateJavaScriptCallback evaluateJavaScriptFunction = nullptr; - std::function<void(DispatchCallback)> dispatchFunction = nullptr; - BufferMap buffers; - bool isReady = false; - Mutex mutex; - Table table; - Listeners listeners; - Core *core = nullptr; - Bridge *bridge = nullptr; - #if defined(__APPLE__) - SSCIPCNetworkStatusObserver* networkStatusObserver = nullptr; - SSCLocationObserver* locationObserver = nullptr; - SSCIPCSchemeHandler* schemeHandler = nullptr; - SSCIPCSchemeTasks* schemeTasks = nullptr; - NSTimer* notificationPollTimer = nullptr; - #elif defined(__linux__) && !defined(__ANDROID__) - WebKitWebContext* webkitWebContext = nullptr; - #endif - - Router (); - Router (const Router &) = delete; - ~Router (); - - MessageBuffer getMappedBuffer (int index, const Message::Seq seq); - bool hasMappedBuffer (int index, const Message::Seq seq); - void removeMappedBuffer (int index, const Message::Seq seq); - void setMappedBuffer(int index, const Message::Seq seq, MessageBuffer msg_buf); - - void preserveCurrentTable (); - - uint64_t listen (const String& name, MessageCallback callback); - bool unlisten (const String& name, uint64_t token); - void map (const String& name, MessageCallback callback); - void map (const String& name, bool async, MessageCallback callback); - void unmap (const String& name); - bool dispatch (DispatchCallback callback); - bool emit (const String& name, const String data); - bool evaluateJavaScript (const String javaScript); - bool send (const Message::Seq& seq, const String data, const Post post); - bool invoke (const String& msg, ResultCallback callback); - bool invoke (const String& msg, const char *bytes, size_t size); - bool invoke ( - const String& msg, - const char *bytes, - size_t size, - ResultCallback callback - ); - bool invoke ( - const Message& msg, - const char *bytes, - size_t size, - ResultCallback callback - ); - }; - - class Bridge { - public: - Router router; - Bluetooth bluetooth; - Core *core = nullptr; - #if !defined(__ANDROID__) && (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR)) - FileSystemWatcher* fileSystemWatcher = nullptr; - #endif - - #if defined(__ANDROID__) - bool isAndroidEmulator = false; - #endif - - Bridge (Core *core); - ~Bridge (); - bool route (const String& msg, const char *bytes, size_t size); - bool route ( - const String& msg, - const char *bytes, - size_t size, - Router::ResultCallback - ); - }; - - inline String getResolveToMainProcessMessage ( - const String& seq, - const String& state, - const String& value - ) { - return String("ipc://resolve?seq=" + seq + "&state=" + state + "&value=" + value); - } -} // SSC::IPC #endif diff --git a/src/ipc/message.cc b/src/ipc/message.cc new file mode 100644 index 0000000000..0b514ea4de --- /dev/null +++ b/src/ipc/message.cc @@ -0,0 +1,95 @@ +#include "message.hh" + +namespace SSC::IPC { + Message::Message (const Message& message) + : value(message.value), + index(message.index), + name(message.name), + seq(message.seq), + uri(message.uri), + args(message.args), + isHTTP(message.isHTTP), + cancel(message.cancel), + buffer(message.buffer), + client(message.client) + {} + + Message::Message (const String& source) + : Message(source, false) + {} + + Message::Message (const String& source, bool decodeValues) { + String str = source; + uri = str; + + // bail if missing protocol prefix + if (str.find("ipc://") == -1) return; + + // bail if malformed + if (str.compare("ipc://") == 0) return; + if (str.compare("ipc://?") == 0) return; + if (str.compare("ipc:///?") == 0) return; + + String query; + String path; + + auto raw = split(str, '?'); + path = raw[0]; + if (raw.size() > 1) query = raw[1]; + + auto parts = split(path, '/'); + if (parts.size() > 1) name = parts[1]; + + if (raw.size() != 2) return; + auto pairs = split(raw[1], '&'); + + for (auto& rawPair : pairs) { + auto pair = split(rawPair, '='); + if (pair.size() <= 1) continue; + + if (pair[0].compare("index") == 0) { + try { + this->index = std::stoi(pair[1].size() > 0 ? pair[1] : "0"); + } catch (...) { + debug("SSC:IPC::Message: Warning: received non-integer index"); + } + } + + if (pair[0].compare("value") == 0) { + this->value = decodeURIComponent(pair[1]); + } + + if (pair[0].compare("seq") == 0) { + this->seq = decodeURIComponent(pair[1]); + } + + if (decodeValues) { + this->args[pair[0]] = decodeURIComponent(pair[1]); + } else { + this->args[pair[0]] = pair[1]; + } + } + } + + bool Message::has (const String& key) const { + return ( + this->args.contains(key) && + this->args.at(key).size() > 0 + ); + } + + String Message::get (const String& key) const { + return this->get(key, ""); + } + + String Message::get (const String& key, const String &fallback) const { + if (key == "value") return this->value; + return this->args.contains(key) + ? decodeURIComponent(args.at(key)) + : fallback; + } + + const Map Message::dump () const { + return this->args; + } +} diff --git a/src/ipc/message.hh b/src/ipc/message.hh new file mode 100644 index 0000000000..3b42962fd3 --- /dev/null +++ b/src/ipc/message.hh @@ -0,0 +1,65 @@ +#ifndef SOCKET_RUNTIME_IPC_MESSAGE_H +#define SOCKET_RUNTIME_IPC_MESSAGE_H + +#include "../core/core.hh" +#include "client.hh" + +namespace SSC::IPC { + struct MessageBuffer { + size_t size = 0; + SharedPointer<char[]> bytes = nullptr; + + MessageBuffer () = default; + MessageBuffer (SharedPointer<char[]> bytes, size_t size) + : size(size), + bytes(bytes) + {} + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + ICoreWebView2SharedBuffer* shared_buf = nullptr; + MessageBuffer (ICoreWebView2SharedBuffer* buf, size_t size) + : size(size), + shared_buf(buf) + { + BYTE* b = reinterpret_cast<BYTE*>(bytes.get()); + HRESULT r = buf->get_Buffer(&b); + if (r != S_OK) { + // TODO(trevnorris): Handle this + } + } + #endif + }; + + struct MessageCancellation { + void (*handler)(void*) = nullptr; + void *data = nullptr; + }; + + class Message { + public: + using Seq = String; + MessageBuffer buffer; + Client client; + + String value = ""; + String name = ""; + String uri = ""; + int index = -1; + Seq seq = ""; + Map args = {}; + bool isHTTP = false; + SharedPointer<MessageCancellation> cancel = nullptr; + + Message () = default; + Message (const Message& message); + Message (const String& source, bool decodeValues); + Message (const String& source); + bool has (const String& key) const; + String get (const String& key) const; + String get (const String& key, const String& fallback) const; + String str () const { return this->uri; } + const Map dump () const; + const char* c_str () const { return this->uri.c_str(); } + }; +} +#endif diff --git a/src/ipc/message.kt b/src/ipc/message.kt new file mode 100644 index 0000000000..2814ba8642 --- /dev/null +++ b/src/ipc/message.kt @@ -0,0 +1,88 @@ +// vim: set sw=2: +package socket.runtime.ipc + +import android.net.Uri + +/** + * `Message` is a container for a parsed IPC message (ipc://...) + */ +open class Message (message: String? = null) { + var uri: Uri? = + if (message != null) { + Uri.parse(message) + } else { + Uri.parse("ipc://") + } + + var name: String + get () = uri?.host ?: "" + set (name) { + uri = uri?.buildUpon()?.authority(name)?.build() + } + + var domain: String + get () { + val parts = name.split(".") + return parts.slice(0..(parts.size - 2)).joinToString(".") + } + set (_) {} + + var value: String + get () = get("value") + set (value) { + set("value", value) + } + + var seq: String + get () = get("seq") + set (seq) { + set("seq", seq) + } + + var bytes: ByteArray? = null + + fun get (key: String, defaultValue: String = ""): String { + val value = uri?.getQueryParameter(key) + + if (value != null && value.isNotEmpty()) { + return value + } + + return defaultValue + } + + fun has (key: String): Boolean { + return get(key).isNotEmpty() + } + + fun set (key: String, value: String): Boolean { + uri = uri?.buildUpon()?.appendQueryParameter(key, value)?.build() + return uri == null + } + + fun delete (key: String): Boolean { + if (uri?.getQueryParameter(key) == null) { + return false + } + + val params = uri?.queryParameterNames + val tmp = uri?.buildUpon()?.clearQuery() + + if (params != null) { + for (param: String in params) { + if (!param.equals(key)) { + val value = uri?.getQueryParameter(param) + tmp?.appendQueryParameter(param, value) + } + } + } + + uri = tmp?.build() + + return true + } + + override fun toString(): String { + return uri?.toString() ?: "" + } +} diff --git a/src/ipc/navigator.cc b/src/ipc/navigator.cc new file mode 100644 index 0000000000..bf8d784eee --- /dev/null +++ b/src/ipc/navigator.cc @@ -0,0 +1,458 @@ +#include "../app/app.hh" +#include "../window/window.hh" + +#include "bridge.hh" +#include "navigator.hh" + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@implementation SSCNavigationDelegate +- (void) webView: (WKWebView*) webview + didFailNavigation: (WKNavigation*) navigation + withError: (NSError*) error +{ + // TODO(@jwerle) +} + +- (void) webView: (WKWebView*) webview + didFailProvisionalNavigation: (WKNavigation*) navigation + withError: (NSError*) error +{ + // TODO(@jwerle) +} + +- (void) webView: (WKWebView*) webview + decidePolicyForNavigationAction: (WKNavigationAction*) navigationAction + decisionHandler: (void (^)(WKNavigationActionPolicy)) decisionHandler +{ + using namespace SSC; + if ( + webview != nullptr && + webview.URL != nullptr && + webview.URL.absoluteString.UTF8String != nullptr && + navigationAction != nullptr && + navigationAction.request.URL.absoluteString.UTF8String != nullptr + ) { + const auto currentURL = String(webview.URL.absoluteString.UTF8String); + const auto requestedURL = String(navigationAction.request.URL.absoluteString.UTF8String); + + if (!self.navigator->handleNavigationRequest(currentURL, requestedURL)) { + return decisionHandler(WKNavigationActionPolicyCancel); + } + } + + decisionHandler(WKNavigationActionPolicyAllow); +} + +- (void) webView: (WKWebView*) webview + decidePolicyForNavigationResponse: (WKNavigationResponse*) navigationResponse + decisionHandler: (void (^)(WKNavigationResponsePolicy)) decisionHandler +{ + decisionHandler(WKNavigationResponsePolicyAllow); +} +@end +#endif + +namespace SSC::IPC { + Navigator::Location::Location (Bridge* bridge) + : bridge(bridge), + URL() + {} + + void Navigator::Location::init () {} + + /** + * . + * ├── a-conflict-index + * │ └── index.html + * ├── a-conflict-index.html + * ├── an-index-file + * │ ├── a-html-file.html + * │ └── index.html + * ├── another-file.html + * └── index.html + * + * Subtleties: + * Direct file navigation always wins + * /foo/index.html have precedent over foo.html + * /foo redirects to /foo/ when there is a /foo/index.html + * + * '/' -> '/index.html' + * '/index.html' -> '/index.html' + * '/a-conflict-index' -> redirect to '/a-conflict-index/' + * '/another-file' -> '/another-file.html' + * '/another-file.html' -> '/another-file.html' + * '/an-index-file/' -> '/an-index-file/index.html' + * '/an-index-file' -> redirect to '/an-index-file/' + * '/an-index-file/a-html-file' -> '/an-index-file/a-html-file.html' + **/ + static const Navigator::Location::Resolution resolveLocationPathname ( + const String& pathname, + const String& dirname + ) { + auto result = pathname; + + if (result.starts_with("/")) { + result = result.substr(1); + } + + // Resolve the full path + const auto filename = (fs::path(dirname) / fs::path(result)).make_preferred(); + + // 1. Try the given path if it's a file + if (FileResource::isFile(filename)) { + return Navigator::Location::Resolution { + .pathname = "/" + replace(fs::relative(filename, dirname).string(), "\\\\", "/") + }; + } + + // 2. Try appending a `/` to the path and checking for an index.html + const auto index = filename / fs::path("index.html"); + if (FileResource::isFile(index)) { + if (filename.string().ends_with("\\") || filename.string().ends_with("/")) { + return Navigator::Location::Resolution { + .pathname = "/" + replace(fs::relative(index, dirname).string(), "\\\\", "/"), + .redirect = false + }; + } else { + return Navigator::Location::Resolution { + .pathname = "/" + replace(fs::relative(filename, dirname).string(), "\\\\", "/") + "/", + .redirect = true + }; + } + } + + // 3. Check if appending a .html file extension gives a valid file + const auto html = Path(filename).replace_extension(".html"); + if (FileResource::isFile(html)) { + return Navigator::Location::Resolution { + .pathname = "/" + replace(fs::relative(html, dirname).string(), "\\\\", "/") + }; + } + + // If no valid path is found, return empty string + return Navigator::Location::Resolution {}; + } + + const Navigator::Location::Resolution Navigator::Location::resolve (const Path& pathname, const Path& dirname) { + return this->resolve(pathname.string(), dirname.string()); + } + + const Navigator::Location::Resolution Navigator::Location::resolve (const String& pathname, const String& dirname) { + for (const auto& entry : this->mounts) { + if (pathname.starts_with(entry.second)) { + const auto relative = replace(pathname, entry.second, ""); + auto resolution = resolveLocationPathname(relative, entry.first); + if (resolution.pathname.size() > 0) { + const auto filename = Path(entry.first) / resolution.pathname.substr(1); + resolution.type = Navigator::Location::Resolution::Type::Mount; + resolution.mount.filename = filename.string(); + return resolution; + } + } + } + + auto resolution = resolveLocationPathname(pathname, dirname); + if (resolution.pathname.size() > 0) { + resolution.type = Navigator::Location::Resolution::Type::Resource; + } + return resolution; + } + + bool Navigator::Location::Resolution::isUnknown () const { + return this->type == Navigator::Location::Resolution::Type::Unknown; + } + + bool Navigator::Location::Resolution::isResource () const { + return this->type == Navigator::Location::Resolution::Type::Resource; + } + + bool Navigator::Location::Resolution::isMount () const { + return this->type == Navigator::Location::Resolution::Type::Mount; + } + + void Navigator::Location::assign (const String& url) { + this->set(url); + this->bridge->navigate(url); + } + + Navigator::Navigator (Bridge* bridge) + : bridge(bridge), + location(bridge), + serviceWorker(App::sharedApplication()->serviceWorkerContainer) + { + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->navigationDelegate = [SSCNavigationDelegate new]; + this->navigationDelegate.navigator = this; + #endif + } + + Navigator::~Navigator () { + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->navigationDelegate) { + this->navigationDelegate.navigator = nullptr; + + #if !__has_feature(objc_arc) + [this->navigationDelegate release]; + #endif + } + + this->navigationDelegate = nullptr; + #endif + } + + void Navigator::init () { + this->location.init(); + } + + void Navigator::configureWebView (WebView* webview) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + webview.navigationDelegate = this->navigationDelegate; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + g_signal_connect( + G_OBJECT(webview), + "decide-policy", + G_CALLBACK((+[]( + WebKitWebView* webview, + WebKitPolicyDecision* decision, + WebKitPolicyDecisionType decisionType, + gpointer userData + ) { + auto app = App::sharedApplication(); + auto window = app->windowManager.getWindowForWebView(webview); + + if (!window) { + webkit_policy_decision_ignore(decision); + return false; + } + + auto navigator = &window->bridge.navigator; + + if (decisionType != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { + webkit_policy_decision_use(decision); + return true; + } + + const auto navigation = WEBKIT_NAVIGATION_POLICY_DECISION(decision); + const auto action = webkit_navigation_policy_decision_get_navigation_action(navigation); + const auto request = webkit_navigation_action_get_request(action); + const auto currentURL = String(webkit_web_view_get_uri(webview)); + const auto requestedURL = String(webkit_uri_request_get_uri(request)); + + if (!navigator->handleNavigationRequest(currentURL, requestedURL)) { + webkit_policy_decision_ignore(decision); + return false; + } + + return true; + })), + this + ); + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + EventRegistrationToken token; + webview->add_NavigationStarting( + Microsoft::WRL::Callback<ICoreWebView2NavigationStartingEventHandler>( + [=, this](ICoreWebView2* webview, ICoreWebView2NavigationStartingEventArgs* args) { + PWSTR source; + PWSTR uri; + + args->get_Uri(&uri); + webview->get_Source(&source); + + if (uri == nullptr || source == nullptr) { + if (uri) CoTaskMemFree(uri); + if (source) CoTaskMemFree(source); + return E_POINTER; + } + + const auto requestedURL = convertWStringToString(uri); + const auto currentURL = convertWStringToString(source); + + if (!this->handleNavigationRequest(currentURL, requestedURL)) { + args->put_Cancel(true); + } + + CoTaskMemFree(uri); + CoTaskMemFree(source); + return S_OK; + } + ).Get(), + &token + ); + #endif + } + + bool Navigator::handleNavigationRequest ( + const String& currentURL, + const String& requestedURL + ) { + auto userConfig = this->bridge->userConfig; + const auto app = App::sharedApplication(); + const auto links = parseStringList(userConfig["meta_application_links"], ' '); + const auto window = app->windowManager.getWindowForBridge(this->bridge); + const auto applinks = parseStringList(userConfig["meta_application_links"], ' '); + const auto currentURLComponents = URL::Components::parse(currentURL); + + bool hasAppLink = false; + if (applinks.size() > 0 && currentURLComponents.authority.size() > 0) { + const auto host = currentURLComponents.authority; + for (const auto& applink : applinks) { + const auto parts = split(applink, '?'); + if (host == parts[0]) { + hasAppLink = true; + break; + } + } + } + + if (hasAppLink) { + if (window) { + window->handleApplicationURL(requestedURL); + return false; + } + + // should be unreachable, but... + return true; + } + + if ( + userConfig["meta_application_protocol"].size() > 0 && + requestedURL.starts_with(userConfig["meta_application_protocol"]) && + !requestedURL.starts_with("socket://" + userConfig["meta_bundle_identifier"]) + ) { + + if (window) { + window->handleApplicationURL(requestedURL); + return false; + } + + // should be unreachable, but... + return true; + } + + if (!this->isNavigationRequestAllowed(currentURL, requestedURL)) { + debug("IPC::Navigation: A navigation request was ignored for: %s", requestedURL.c_str()); + return false; + } + + return true; + } + + bool Navigator::isNavigationRequestAllowed ( + const String& currentURL, + const String& requestedURL + ) { + static const auto devHost = getDevHost(); + auto userConfig = this->bridge->userConfig; + const auto allowed = split(trim(userConfig["webview_navigator_policies_allowed"]), ' '); + + if (requestedURL == "about:blank") { + return true; + } + + for (const auto& entry : split(userConfig["webview_protocol-handlers"], " ")) { + const auto scheme = replace(trim(entry), ":", ""); + if (requestedURL.starts_with(scheme + ":")) { + return true; + } + } + + for (const auto& entry : userConfig) { + const auto& key = entry.first; + if (key.starts_with("webview_protocol-handlers_")) { + const auto scheme = replace(replace(trim(key), "webview_protocol-handlers_", ""), ":", "");; + if (requestedURL.starts_with(scheme + ":")) { + return true; + } + } + } + + for (const auto& entry : allowed) { + String pattern = entry; + pattern = replace(pattern, "\\.", "\\."); + pattern = replace(pattern, "\\*", "(.*)"); + pattern = replace(pattern, "\\.\\.\\*", "(.*)"); + pattern = replace(pattern, "\\/", "\\/"); + + try { + std::regex regex(pattern); + std::smatch match; + + if (std::regex_match(requestedURL, match, regex, std::regex_constants::match_any)) { + return true; + } + } catch (...) {} + } + + if ( + requestedURL.starts_with("socket:") || + requestedURL.starts_with("node:") || + requestedURL.starts_with("npm:") || + requestedURL.starts_with("ipc:") || + #if SOCKET_RUNTIME_PLATFORM_APPLE + #endif + requestedURL.starts_with(devHost) + ) { + return true; + } + + return false; + } + + void Navigator::configureMounts () { + static const auto wellKnownPaths = FileResource::getWellKnownPaths(); + this->location.mounts = FileResource::getMountedPaths(); + + for (const auto& entry : this->location.mounts) { + const auto& path = entry.first; + #if SOCKET_RUNTIME_PLATFORM_LINUX + auto webContext = webkit_web_context_get_default(); + if (path != wellKnownPaths.home.string()) { + webkit_web_context_add_path_to_sandbox(webContext, path.c_str(), false); + } + #endif + } + + #if SOCKET_RUNTIME_PLATFORM_LINUX + auto webContext = webkit_web_context_get_default(); + for (const auto& entry : wellKnownPaths.entries()) { + if (FileResource::isDirectory(entry) && entry != wellKnownPaths.home) { + webkit_web_context_add_path_to_sandbox(webContext, entry.c_str(), false); + } + } + #endif + } +} + +#if SOCKET_RUNTIME_PLATFORM_ANDROID +extern "C" { + using namespace SSC; + + jboolean ANDROID_EXTERNAL(ipc, Navigator, isNavigationRequestAllowed) ( + JNIEnv* env, + jobject self, + jint index, + jstring currentURLString, + jstring requestedURLString + ) { + auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return false; + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + ANDROID_THROW(env, "Invalid window requested"); + return false; + } + + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + const auto currentURL = Android::StringWrap(attachment.env, currentURLString).str(); + const auto requestedURL = Android::StringWrap(attachment.env, requestedURLString).str(); + + return window->bridge.navigator.isNavigationRequestAllowed(currentURL, requestedURL); + } +} +#endif diff --git a/src/ipc/navigator.hh b/src/ipc/navigator.hh new file mode 100644 index 0000000000..958bf649cc --- /dev/null +++ b/src/ipc/navigator.hh @@ -0,0 +1,99 @@ +#ifndef SOCKET_RUNTIME_IPC_NAVIGATOR_H +#define SOCKET_RUNTIME_IPC_NAVIGATOR_H + +#include "../core/config.hh" +#include "../core/resource.hh" +#include "../core/url.hh" +#include "../core/webview.hh" +#include "../serviceworker/container.hh" + +namespace SSC::IPC { + // forward + class Bridge; + class Navigator; +} + +#if SOCKET_RUNTIME_PLATFORM_APPLE +@interface SSCNavigationDelegate : NSObject<WKNavigationDelegate> +@property (nonatomic) SSC::IPC::Navigator* navigator; +- (void) webView: (WKWebView*) webView + didFailNavigation: (WKNavigation*) navigation + withError: (NSError*) error; + +- (void) webView: (WKWebView*) webView + didFailProvisionalNavigation: (WKNavigation*) navigation + withError: (NSError*) error; + +- (void) webView: (WKWebView*) webview + decidePolicyForNavigationAction: (WKNavigationAction*) navigationAction + decisionHandler: (void (^)(WKNavigationActionPolicy)) decisionHandler; + +- (void) webView: (WKWebView*) webView + decidePolicyForNavigationResponse: (WKNavigationResponse*) navigationResponse + decisionHandler: (void (^)(WKNavigationResponsePolicy)) decisionHandler; +@end +#endif + +namespace SSC::IPC { + class Navigator { + public: + struct Location : public URL { + struct Mount { + String filename; // root path on host file system + }; + + struct Resolution { + enum class Type { Unknown, Resource, Mount }; + Type type = Type::Unknown; + Mount mount; + String pathname = ""; + bool redirect = false; + + bool isUnknown () const; + bool isResource () const; + bool isMount () const; + }; + + Bridge* bridge = nullptr; + Map workers; + Map mounts; + + Location () = delete; + Location (const Location&) = delete; + Location (Bridge* bridge); + + void init (); + void assign (const String& url); + + const Resolution resolve ( + const Path& pathname, + const Path& dirname = FileResource::getResourcesPath() + ); + + const Resolution resolve ( + const String& pathname, + const String& dirname = FileResource::getResourcesPath().string() + ); + }; + + ServiceWorkerContainer& serviceWorker; + Location location; + Bridge* bridge = nullptr; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + SSCNavigationDelegate* navigationDelegate = nullptr; + #endif + + Navigator () = delete; + Navigator (const Navigator&) = delete; + Navigator (Bridge* bridge); + ~Navigator (); + + void init (); + void configureWebView (WebView* object); + bool handleNavigationRequest (const String& currentURL, const String& requestedURL); + bool isNavigationRequestAllowed (const String& location, const String& requestURL); + void configureMounts (); + }; +} +#endif diff --git a/src/ipc/navigator.kt b/src/ipc/navigator.kt new file mode 100644 index 0000000000..6e1a8e64e5 --- /dev/null +++ b/src/ipc/navigator.kt @@ -0,0 +1,22 @@ +// vim: set sw=2: +package socket.runtime.ipc + +open class Navigator (val bridge: Bridge) { + fun isNavigationRequestAllowed ( + currentURL: String, + requestedURL: String + ): Boolean { + return this.isNavigationRequestAllowed( + this.bridge.index, + currentURL, + requestedURL + ) + } + + @Throws(Exception::class) + external fun isNavigationRequestAllowed ( + index: Int, + currentURL: String, + requestedURL: String + ): Boolean +} diff --git a/src/ipc/preload.cc b/src/ipc/preload.cc new file mode 100644 index 0000000000..8a3052a565 --- /dev/null +++ b/src/ipc/preload.cc @@ -0,0 +1,747 @@ +#include "../core/core.hh" +#include "preload.hh" + +namespace SSC::IPC { + static constexpr auto DEFAULT_REFERRER_POLICY = "unsafe-url"; + static constexpr auto RUNTIME_PRELOAD_META_BEGIN_TAG = ( + R"HTML(<meta name="begin-runtime-preload">)HTML" + ); + + static constexpr auto RUNTIME_PRELOAD_META_END_TAG = ( + R"HTML(<meta name="end-runtime-preload">)HTML" + ); + + static constexpr auto RUNTIME_PRELOAD_JAVASCRIPT_BEGIN_TAG = ( + R"HTML(<script type="text/javascript">)HTML" + ); + + static constexpr auto RUNTIME_PRELOAD_JAVASCRIPT_END_TAG = ( + R"HTML(</script>)HTML" + ); + + static constexpr auto RUNTIME_PRELOAD_MODULE_BEGIN_TAG = ( + R"HTML(<script type="module">)HTML" + ); + + static constexpr auto RUNTIME_PRELOAD_MODULE_END_TAG = ( + R"HTML(</script>)HTML" + ); + + static constexpr auto RUNTIME_PRELOAD_IMPORTMAP_BEGIN_TAG = ( + R"HTML(<script type="importmap">)HTML" + ); + + static constexpr auto RUNTIME_PRELOAD_IMPORTMAP_END_TAG = ( + R"HTML(</script>)HTML" + ); + + const Preload Preload::compile (const Options& options) { + auto preload = Preload(options); + preload.compile(); + return preload; + } + + Preload::Preload (const Options& options) + : options(options) + { + this->configure(); + if (this->options.userConfig.size() == 0) { + this->options.userConfig = getUserConfig(); + } + } + + void Preload::configure () { + this->configure(this->options); + } + + void Preload::configure (const Options& options) { + this->options = options; + this->headers = options.headers; + this->metadata = options.metadata; + + if (options.features.useHTMLMarkup) { + if (!this->metadata.contains("referrer")) { + this->metadata["referrer"] = DEFAULT_REFERRER_POLICY; + } + + if (!this->headers.has("referrer-policy")) { + if (this->metadata["referrer"].size() > 0) { + this->headers.set("referrer-policy", this->metadata["referrer"]); + } + } + } + + if (Env::has("SOCKET_RUNTIME_VM_DEBUG")) { + this->options.env["SOCKET_RUNTIME_VM_DEBUG"] = Env::get("SOCKET_RUNTIME_VM_DEBUG"); + } + + if (Env::has("SOCKET_RUNTIME_NPM_DEBUG")) { + this->options.env["SOCKET_RUNTIME_NPM_DEBUG"] = Env::get("SOCKET_RUNTIME_NPM_DEBUG"); + } + + if (Env::has("SOCKET_RUNTIME_SHARED_WORKER_DEBUG")) { + this->options.env["SOCKET_RUNTIME_SHARED_WORKER_DEBUG"] = Env::get("SOCKET_RUNTIME_SHARED_WORKER_DEBUG"); + } + + if (Env::has("SOCKET_RUNTIME_SERVICE_WORKER_DEBUG")) { + this->options.env["SOCKET_RUNTIME_SERVICE_WORKER_DEBUG"] = Env::get("SOCKET_RUNTIME_SERVICE_WORKER_DEBUG"); + } + } + + Preload& Preload::append (const String& source) { + this->buffer.push_back(source); + return *this; + } + + const String& Preload::compile () { + Vector<String> buffers; + + auto args = JSON::Object { + JSON::Object::Entries { + {"argv", JSON::Array {}}, + {"client", JSON::Object {}}, + {"conduit", this->options.conduit}, + {"config", JSON::Object {}}, + {"debug", this->options.debug}, + {"headless", this->options.headless}, + {"env", JSON::Object {}}, + {"index", this->options.index} + } + }; + + for (const auto& value : this->options.argv) { + args["argv"].as<JSON::Array>().push(value); + } + + // 1. compile metadata if `options.features.useHTMLMarkup == true`, otherwise skip + if (this->options.features.useHTMLMarkup) { + for (const auto &entry : this->metadata) { + if (entry.second.size() == 0) { + continue; + } + + buffers.push_back(tmpl( + R"HTML(<meta name="{{name}}" content="{{content}}">)HTML", + Map { + {"name", trim(entry.first)}, + {"content", trim(decodeURIComponent(entry.second))} + } + )); + } + } + + // 2. compile headers if `options.features.useHTMLMarkup == true`, otherwise skip + if (this->options.features.useHTMLMarkup) { + for (const auto &entry : this->headers) { + buffers.push_back(tmpl( + R"HTML(<meta http-equiv="{{header}}" content="{{value}}">)HTML", + Map { + {"header", toHeaderCase(entry.name)}, + {"value", trim(decodeURIComponent(entry.value.str()))} + } + )); + } + } + + // 3. compile preload `<meta name="begin-runtime-preload">` "BEGIN" tag if `options.features.useHTMLMarkup == true`, otherwise skip + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_META_BEGIN_TAG); + } + + if (this->options.features.useGlobalArgs) { + // 4. compile preload `<script>` prefix if `options.features.useHTMLMarkup == true`, otherwise skip + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_BEGIN_TAG); + } + + // 5. compile `globalThis.__args` object + buffers.push_back(";(() => {"); + buffers.push_back(trim(tmpl( + R"JAVASCRIPT( + if (globalThis.__RUNTIME_INIT_NOW__) return + if (!globalThis.__args) { + Object.defineProperty(globalThis, '__args', { + configurable: false, + enumerable: false, + writable: false, + value: {} + }) + Object.defineProperties(globalThis.__args, { + argv: { + configurable: false, + enumerable: true, + writable: false, + value: {{argv}} + }, + client: { + configurable: false, + enumerable: true, + writable: false, + value: {} + }, + conduit: { + configurable: false, + enumerable: true, + writable: false, + value: {{conduit}} + }, + config: { + configurable: false, + enumerable: true, + writable: false, + value: {} + }, + debug: { + configurable: false, + enumerable: true, + writable: false, + value: {{debug}} + }, + env: { + configurable: false, + enumerable: true, + writable: false, + value: {} + }, + headless: { + configurable: false, + enumerable: true, + writable: false, + value: {{headless}} + }, + index: { + configurable: false, + enumerable: true, + writable: false, + value: {{index}} + }, + }) + } + )JAVASCRIPT", + Map { + {"argv", args["argv"].str()}, + {"conduit", args["conduit"].str()}, + {"debug", args["debug"].str()}, + {"headless", args["headless"].str()}, + {"index", args["index"].str()}, + } + ))); + + // 6. compile `globalThis.__args.client` values + buffers.push_back(trim(tmpl( + R"JAVASCRIPT( + if (!globalThis.__args.client?.id) { + Object.defineProperties(globalThis.__args.client, { + id: { + configurable: false, + enumerable: true, + writable: false, + value: globalThis.window && globalThis.top !== globalThis.window + ? '{{id}}' + : globalThis.window && globalThis.top + ? '{{clientId}}' + : null + }, + type: { + configurable: false, + enumerable: true, + writable: true, + value: globalThis.window ? 'window' : 'worker' + }, + parent: { + configurable: false, + enumerable: true, + writable: false, + value: globalThis.parent !== globalThis + ? globalThis.parent?.__args?.client ?? null + : null + }, + top: { + configurable: false, + enumerable: true, + get: () => globalThis.top + ? globalThis.top.__args?.client ?? null + : globalThis.__args.client + }, + frameType: { + configurable: false, + enumerable: true, + writable: true, + value: globalThis.window && globalThis.top !== globalThis.window + ? 'nested' + : globalThis.window && globalThis.top + ? 'top-level' + : 'none' + }, + }) + } + )JAVASCRIPT", + Map { + {"id", std::to_string(rand64())}, + {"clientId", std::to_string(this->options.client.id)} + } + ))); + + // 7. compile `globalThis.__args.config` values + buffers.push_back(R"JAVASCRIPT(const __RAW_CONFIG__ = {})JAVASCRIPT"); + + for (const auto& entry : this->options.userConfig) { + const auto key = trim(entry.first); + const auto value = trim(entry.second); + + // skip empty key/value and comments + if (key.size() == 0 || value.size() == 0 || key.rfind(";", 0) == 0 || key.rfind("#", 0) == 0) { + continue; + } + + buffers.push_back(tmpl( + R"JAVASCRIPT(__RAW_CONFIG__['{{key}}'] = '{{value}}')JAVASCRIPT", + Map {{"key", key}, {"value", encodeURIComponent(value)}} + )); + } + + buffers.push_back(R"JAVASCRIPT( + for (const key in __RAW_CONFIG__) { + let value = __RAW_CONFIG__[key] + + try { value = decodeURIComponent(value) } catch {} + + if (value === 'true') { + value = true + } else if (value === 'false') { + value = false + } else if (value === 'null') { + value = null + } else if (value === 'NaN') { + value = Number.NaN + } else if (value.startsWith('0x')) { + const parsed = parseInt(value.slice(2), 16) + if (!Number.isNaN(parsed)) { + value = parsed + } + } else { + const parsed = parseFloat(value) + if (!Number.isNaN(parsed)) { + value = parsed + } + } + + try { value = JSON.parse(value) } catch {} + + globalThis.__args.config[key] = value + if (key.startsWith('env_')) { + globalThis.__args.env[key.slice(4)] = value + } + } + )JAVASCRIPT"); + + // 8. compile `globalThis.__args.env` values + buffers.push_back(R"JAVASCRIPT(const __RAW_ENV__ = {})JAVASCRIPT"); + + for (const auto& entry : this->options.env) { + const auto key = trim(entry.first); + const auto value = trim(entry.second); + + // skip empty keys and valaues + if (key.size() == 0 || value.size() == 0) { + continue; + } + + buffers.push_back(tmpl( + R"JAVASCRIPT(__RAW_ENV__['{{key}}'] = '{{value}}')JAVASCRIPT", + Map {{"key", key}, {"value", encodeURIComponent(value)}} + )); + } + + buffers.push_back(R"JAVASCRIPT( + for (const key in __RAW_ENV__) { + let value = __RAW_ENV__[key] + try { value = decodeURIComponent(value) } catch {} + try { value = JSON.parse(value) } catch {} + globalThis.__args.env[key] = value + } + )JAVASCRIPT"); + + // 9. compile test script import if `options.features.useTestScript == true` + if (this->options.features.useTestScript && this->options.index == 0) { + String pathname; + for (const auto& value : this->options.argv) { + const auto start = value.find("--test="); + if (start != String::npos) { + auto end = value.find("'", start); + + if (end == String::npos) { + end = value.size(); + } + + const auto file = value.substr(start + 7, end - start - 7); + if (file.size() > 0) { + pathname = file; + break; + } + } + } + + if (pathname.size() == 0) { + buffers.push_back(R"JAVASCRIPT( + console.warn('Preload: A test script entry was requested, but was not given') + )JAVASCRIPT"); + } else { + buffers.push_back(tmpl( + R"JAVASCRIPT( + if (!globalThis.RUNTIME_TEST_FILENAME) { + Object.defineProperty(globalThis, 'RUNTIME_TEST_FILENAME', { + configurable: false, + enumerable: false, + writable: false, + value: String(new URL('{{pathname}}', globalThis.location.href) + }) + } + )JAVASCRIPT", + Map {{"pathname", pathname}} + )); + } + } + + // 10. compile listeners for `globalThis` + buffers.push_back(R"JAVASCRIPT( + if (globalThis.document && !globalThis.RUNTIME_APPLICATION_URL_EVENT_BACKLOG) { + Object.defineProperties(globalThis, { + RUNTIME_APPLICATION_URL_EVENT_BACKLOG: { + configurable: false, + enumerable: false, + writable: false, + value: [] + } + }) + + globalThis.document.addEventListener('readystatechange', async (e) => { + const ipc = await import('socket:ipc') + ipc.send('platform.event', { + value: 'readystatechange', + state: globalThis.document.readyState + }) + }) + + globalThis.addEventListener('applicationurl', (event) => { + if (globalThis.document.readyState !== 'complete') { + globalThis.RUNTIME_APPLICATION_URL_EVENT_BACKLOG.push(event) + } + }) + + globalThis.addEventListener('__runtime_init__', () => { + const backlog = globalThis.RUNTIME_APPLICATION_URL_EVENT_BACKLOG + if (Array.isArray(backlog)) { + for (const event of backlog) { + if (typeof ApplicationURLEvent === 'function') { + globalThis.dispatchEvent(new ApplicationURLEvent(event.type, event)) + } + } + + backlog.splice(0, backlog.length) + } + }, { once: true }) + + if ( + globalThis.__args.config.webview_watch === true && + globalThis.__args.config.webview_watch_reload !== false + ) { + globalThis.addEventListener('filedidchange', () => { + globalThis.location.reload() + }) + } + } + )JAVASCRIPT"); + + // 11. freeze `globalThis.__args` values + buffers.push_back(R"JAVASCRIPT( + try { Object.freeze(globalThis.__args.client) } catch {} + try { Object.freeze(globalThis.__args.config) } catch {} + try { Object.freeze(globalThis.__args.argv) } catch {} + try { Object.freeze(globalThis.__args.env) } catch {} + )JAVASCRIPT"); + buffers.push_back("})();"); + + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_END_TAG); + } + + // 12. compile preload `<script>` prefix if `options.features.useHTMLMarkup == true`, otherwise skip + if (this->options.features.useHTMLMarkup) { + if (this->options.features.useESM) { + buffers.push_back(RUNTIME_PRELOAD_MODULE_BEGIN_TAG); + } else { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_BEGIN_TAG); + } + } + + // 13. compile "internal init" import + if (this->options.features.useHTMLMarkup && this->options.features.useESM) { + buffers.push_back(tmpl( + R"JAVASCRIPT( + import 'socket:internal/init' + {{userScript}} + )JAVASCRIPT", + Map {{"userScript", this->options.userScript}} + )); + } else { + buffers.push_back(";(() => {"); + buffers.push_back(tmpl( + R"JAVASCRIPT( + async function userScriptCallback () { + {{userScript}} + } + + if (globalThis.document && globalThis.document.readyState !== 'complete') { + globalThis.document.addEventListener('readystatechange', () => { + if(/interactive|complete/.test(globalThis.document.readyState)) { + import('socket:internal/init') + .then(userScriptCallback) + .catch(console.error) + } + }) + } else { + import('socket:internal/init') + .then(userScriptCallback) + .catch(console.error) + } + )JAVASCRIPT", + Map {{"userScript", this->options.userScript}} + )); + buffers.push_back("})();"); + } + + // 14. compile preload `</script>` prefix if `options.features.useHTMLMarkup == true`, otherwise skip + if (this->options.features.useHTMLMarkup) { + if (this->options.features.useESM) { + buffers.push_back(RUNTIME_PRELOAD_MODULE_END_TAG); + } else { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_END_TAG); + } + } + + // 15. compile "global CommonJS" if `options.features.useGlobalCommonJS == true`, otherwise skip + if (this->options.features.useGlobalCommonJS) { + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_BEGIN_TAG); + } + + buffers.push_back(R"JAVASCRIPT( + if (globalThis.document && !globalThis.module) { + ;(async function GlobalCommonJSScope () { + const globals = await import('socket:internal/globals') + await globals.get('RuntimeReadyPromise') + + const href = encodeURIComponent(globalThis.location.href) + const source = `socket:module?ref=${href}` + + const { Module } = await import(source) + const path = await import('socket:path') + const require = Module.createRequire(globalThis.location.href) + const __filename = Module.main.filename + const __dirname = path.dirname(__filename) + + Object.defineProperties(globalThis, { + module: { + configurable: true, + enumerable: false, + writable: false, + value: Module.main.scope + }, + require: { + configurable: true, + enumerable: false, + writable: false, + value: require + }, + __dirname: { + configurable: true, + enumerable: false, + writable: false, + value: __dirname + }, + __filename: { + configurable: true, + enumerable: false, + writable: false, + value: __filename + } + }) + + // reload global CommonJS scope on 'popstate' events + globalThis.addEventListener('popstate', GlobalCommonJSScope) + })(); + } + )JAVASCRIPT"); + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_END_TAG); + } + } + + // 16. compile "global NodeJS" if `options.features.useGlobalNodeJS == true`, otherwise skip + if (this->options.features.useGlobalNodeJS) { + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_BEGIN_TAG); + } + buffers.push_back(R"JAVASCRIPT( + if (globalThis.document && !globalThis.process) { + ;(async function GlobalNodeJSScope () { + const process = await import('socket:process') + Object.defineProperties(globalThis, { + process: { + configurable: false, + enumerable: false, + writable: false, + value: process.default + }, + + global: { + configurable: false, + enumerable: false, + writable: false, + value: globalThis + } + }) + })(); + } + )JAVASCRIPT"); + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_END_TAG); + } + } + + // 17. compile `__RUNTIME_PRIMORDIAL_OVERRIDES__` -- assumes value is a valid JSON string + if (this->options.RUNTIME_PRIMORDIAL_OVERRIDES.size() > 0) { + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_BEGIN_TAG); + } + + buffers.push_back(tmpl( + R"JAVASCRIPT( + if (!globalThis.RUNTIME_PRIMORDIAL_OVERRIDES) { + Object.defineProperty(globalThis, '__RUNTIME_PRIMORDIAL_OVERRIDES__', { + configurable: false, + enumerable: false, + writable: false, + value: {{RUNTIME_PRIMORDIAL_OVERRIDES}} + }) + } + )JAVASCRIPT", + Map {{"RUNTIME_PRIMORDIAL_OVERRIDES", this->options.RUNTIME_PRIMORDIAL_OVERRIDES}} + )); + + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_JAVASCRIPT_END_TAG); + } + } + } + + // 18. compile preload `<meta name="end-runtime-preload">` "END" tag if `options.features.useHTMLMarkup == true`, otherwise skip + if (this->options.features.useHTMLMarkup) { + buffers.push_back(RUNTIME_PRELOAD_META_END_TAG); + } + + // 19. clear existing compiled state + this->compiled.clear(); + // 20. compile core buffers + for (const auto& buffer : buffers) { + this->compiled += buffer + "\n"; + } + // 21. user preload buffers + if (this->buffer.size() > 0) { + this->compiled += ";(() => {\n"; + this->compiled += join(this->buffer, '\n'); + this->compiled += "})();\n"; + } + if (this->options.features.useHTMLMarkup == false) { + // 22. compile 'sourceURL' source map value if `options.features.useHTMLMarkup == false` + this->compiled += "//# sourceURL=socket:<runtime>/preload.js"; + } + return this->compiled; + } + + const String& Preload::str () const { + return this->compiled; + } + + const String Preload::insertIntoHTML ( + const String& html, + const InsertIntoHTMLOptions& options + ) const { + if (html.size() == 0) { + return ""; + } + + auto protocolHandlerSchemes = options.protocolHandlerSchemes; + auto userConfig = this->options.userConfig; + auto preload = this->str(); + auto output = html; + if ( + html.find(RUNTIME_PRELOAD_IMPORTMAP_BEGIN_TAG) == String::npos && + userConfig.contains("webview_importmap") && + userConfig.at("webview_importmap").size() > 0 + ) { + auto resource = FileResource(Path(userConfig.at("webview_importmap"))); + + if (resource.exists()) { + const auto bytes = resource.read(); + + if (bytes != nullptr) { + preload = ( + tmpl( + R"HTML(<script type="importmap">{{importmap}}</script>)HTML", + Map {{"importmap", String(bytes, resource.size())}} + ) + preload + ); + } + } + } + + protocolHandlerSchemes.push_back("node:"); + protocolHandlerSchemes.push_back("npm:"); + + output = tmpl(output, Map { + {"protocol_handlers", join(protocolHandlerSchemes, " ")} + }); + + if (output.find("<meta name=\"runtime-preload-injection\" content=\"disabled\"") != String::npos) { + preload = ""; + } else if (output.find("<meta content=\"disabled\" name=\"runtime-preload-injection\"") != String::npos) { + preload = ""; + } + + const auto existingImportMapCursor = output.find(RUNTIME_PRELOAD_IMPORTMAP_BEGIN_TAG); + bool preloadWasInjected = false; + + if (existingImportMapCursor != String::npos) { + const auto closingScriptTag = output.find( + RUNTIME_PRELOAD_JAVASCRIPT_END_TAG, + existingImportMapCursor + ); + + if (closingScriptTag != String::npos) { + output = ( + output.substr(0, closingScriptTag + 9) + + preload + + output.substr(closingScriptTag + 9) + ); + + preloadWasInjected = true; + } + } + + if (!preloadWasInjected) { + if (output.find("<head>") != String::npos) { + output = replace(output, "<head>", String("<head>" + preload)); + } else if (output.find("<body>") != String::npos) { + output = replace(output, "<body>", String("<body>" + preload)); + } else if (output.find("<html>") != String::npos) { + output = replace(output, "<html>", String("<html>" + preload)); + } else { + output = preload + output; + } + } + + return output; + } +} diff --git a/src/ipc/preload.hh b/src/ipc/preload.hh new file mode 100644 index 0000000000..9d9af8f96b --- /dev/null +++ b/src/ipc/preload.hh @@ -0,0 +1,165 @@ +#ifndef SOCKET_RUNTIME_IPC_PRELOAD_H +#define SOCKET_RUNTIME_IPC_PRELOAD_H + +#include "../platform/platform.hh" +#include "../core/unique_client.hh" +#include "../core/options.hh" +#include "../core/headers.hh" +#include "../core/config.hh" + +namespace SSC::IPC { + /** + * `Preload` is a container for state to compile a "preload script" attached + * to an IPC bridge that is injected into HTML documents loaded into a WebView + */ + class Preload { + String compiled = ""; + public: + + /** + * `Options` is an input container for configuring `Preload` metadata, + * headers, environment variable values, preload compiler features, and more. + */ + struct Options : SSC::Options { + /** + * `Features` is a container for a variety of ways of configuring the + * compiler features for the compiled preload that is "injeced" into HTML + * documents. + */ + struct Features { + /** + * If `true`, the feature enables global CommonJS values such as `module`, + * `exports`, `require`, `__filename`, and `__dirname`. + */ + bool useGlobalCommonJS = true; + + /** + * If `true`, the feature enables global Node.js values like + * `process` and `global`. + */ + bool useGlobalNodeJS = true; + + /** + * If `true`, the feature enables the automatic import of a "test script" + * that is specified with the `--test <path_relative_to_resources>` command + * line argument. This is useful for running tests + */ + bool useTestScript = false; + + /** + * If `true`, the feature enables the compiled preload to use HTML markup + * such as `<script>` and `<meta>` tags for injecting JavaScript, metadata + * and HTTP headers. If this feature is `false`, then the preload compiler + * will not include metadata or HTTP headers in `<meta>` tags nor will the + * JavaScript compiled in the preload be "wrapped" by a `<script>` tag. + */ + bool useHTMLMarkup = true; + + /** + * If `true`, the feature enables the preload compiler to use ESM import + * syntax instead of "dynamic" import for importing the "internal init" + * module or running a "user script" + */ + bool useESM = true; + + /** + * If `true`, the feature enables the preload compiler to use include the + * a global `__args` object available on `globalThis`. + */ + bool useGlobalArgs = true; + }; + + bool headless = false; + bool debug = false; + + Features features; + UniqueClient client; + + int index = 0; + int conduit = 0; + + Headers headers = {}; // depends on 'features.useHTMLMarkup' + Map metadata; // depends on 'features.useHTMLMarkup' + Map env; + Map userConfig; + Vector<String> argv; + + String userScript = ""; + + // only use if you know what you are doing + String RUNTIME_PRIMORDIAL_OVERRIDES = ""; + }; + + struct InsertIntoHTMLOptions : public Options { + Vector<String> protocolHandlerSchemes; + }; + + /** + * Creates and compiles preload from given options. + */ + static const Preload compile (const Options& options); + + /** + * The options used to configure this preload. + */ + Options options; + + /** + * The current preload buffer, prior to compilation. + */ + Vector<String> buffer; + + /** + * A mapping of key-value HTTP headers to be injected as HTML `<meta>` + * tags into the preload output. + * This depends on the 'useHTMLMarkup' feature. + */ + Headers headers = {}; + + /** + * A mapping of key-value metadata to be injected as HTML `<meta>` tags + * into the preload output. + * This depends on the 'useHTMLMarkup' feature. + */ + Map metadata; + + Preload () = default; + Preload (const Options& options); + + /** + * Appends source to the preload, prior to compilation. + */ + Preload& append (const String& source); + + /** + * Configures the preload from default options. + */ + void configure (); + + /** + * Configures the preload from given options. + */ + void configure (const Options& options); + + /** + * Compiles the preload and returns a reference to the + * compiled string. + */ + const String& compile (); + + /** + * Get a reference to the currently compiled preload string + */ + const String& str () const; + + /** + * Inserts and returns compiled preload into an HTML string with + * insert options + */ + const String insertIntoHTML ( + const String& html, + const InsertIntoHTMLOptions& options + ) const; + }; +} +#endif diff --git a/src/ipc/result.cc b/src/ipc/result.cc new file mode 100644 index 0000000000..836f25081a --- /dev/null +++ b/src/ipc/result.cc @@ -0,0 +1,125 @@ +#include "result.hh" + +namespace SSC::IPC { + Result::Result (const Message::Seq& seq, const Message& message) { + this->id = rand64(); + this->seq = seq; + this->message = message; + this->source = message.name; + this->post.workerId = this->message.get("runtime-worker-id"); + } + + Result::Result ( + const Message::Seq& seq, + const Message& message, + JSON::Any value + ) : Result(seq, message, value, Post{}) + {} + + Result::Result ( + const Message::Seq& seq, + const Message& message, + JSON::Any value, + Post post + ) : Result(seq, message) { + this->post = post; + this->headers = Headers(post.headers); + + if (this->post.workerId.size() == 0) { + this->post.workerId = this->message.get("runtime-worker-id"); + } + + if (value.type != JSON::Type::Any) { + this->value = value; + } + } + + Result::Result (const JSON::Any value) { + this->id = rand64(); + this->value = value; + } + + Result::Result (const Err error): Result(error.message.seq, error.message) { + this->err = error.value; + this->source = error.message.name; + } + + Result::Result (const Data data): Result(data.message.seq, data.message) { + this->data = data.value; + this->post = data.post; + this->source = data.message.name; + this->headers = Headers(data.post.headers); + } + + JSON::Any Result::json () const { + if (!this->value.isNull()) { + if (this->value.isObject()) { + auto object = this->value.as<JSON::Object>(); + + if (object.has("data") || object.has("err")) { + object["source"] = this->source; + object["id"] = std::to_string(this->id); + } + + return object; + } + + return this->value; + } + + auto entries = JSON::Object::Entries { + {"source", this->source}, + {"id", std::to_string(this->id)} + }; + + if (!this->err.isNull()) { + entries["err"] = this->err; + if (this->err.isObject()) { + if (this->err.as<JSON::Object>().has("id")) { + entries["id"] = this->err.as<JSON::Object>().get("id"); + } + } + } else if (!this->data.isNull()) { + entries["data"] = this->data; + if (this->data.isObject()) { + if (this->data.as<JSON::Object>().has("id")) { + entries["id"] = this->data.as<JSON::Object>().get("id"); + } + } + } + + return JSON::Object(entries); + } + + String Result::str () const { + auto json = this->json(); + return json.str(); + } + + Result::Err::Err (const Message& message, JSON::Any value) { + this->seq = message.seq; + this->message = message; + this->value = value; + } + + Result::Err::Err (const Message& message, const char* error) + : Err(message, String(error)) + {} + + Result::Err::Err (const Message& message, const String& error) { + this->seq = message.seq; + this->message = message; + this->value = JSON::Object::Entries {{"message", error}}; + } + + Result::Data::Data (const Message& message, JSON::Any value) + : Data(message, value, Post{}) + {} + + Result::Data::Data (const Message& message, JSON::Any value, Post post) { + this->seq = message.seq; + this->message = message; + this->value = value; + this->post = post; + } +} diff --git a/src/ipc/result.hh b/src/ipc/result.hh new file mode 100644 index 0000000000..050f177b23 --- /dev/null +++ b/src/ipc/result.hh @@ -0,0 +1,55 @@ +#ifndef SOCKET_RUNTIME_IPC_RESULT_H +#define SOCKET_RUNTIME_IPC_RESULT_H + +#include "../core/core.hh" +#include "message.hh" + +namespace SSC::IPC { + class Result { + public: + class Err { + public: + Message message; + Message::Seq seq; + JSON::Any value; + Err () = default; + Err (const Message&, const char*); + Err (const Message&, const String&); + Err (const Message&, JSON::Any); + }; + + class Data { + public: + Message message; + Message::Seq seq; + JSON::Any value; + Post post; + + Data () = default; + Data (const Message&, JSON::Any); + Data (const Message&, JSON::Any, Post); + }; + + Message message; + Message::Seq seq = "-1"; + uint64_t id = 0; + String source = ""; + JSON::Any value = nullptr; + JSON::Any data = nullptr; + JSON::Any err = nullptr; + Headers headers; + Post post; + + Result () = default; + Result (const Result&) = default; + Result (const JSON::Any); + Result (const Err error); + Result (const Data data); + Result (const Message::Seq&, const Message&); + Result (const Message::Seq&, const Message&, JSON::Any); + Result (const Message::Seq&, const Message&, JSON::Any, Post); + String str () const; + JSON::Any json () const; + }; +} +#endif diff --git a/src/ipc/router.cc b/src/ipc/router.cc new file mode 100644 index 0000000000..e158fe352b --- /dev/null +++ b/src/ipc/router.cc @@ -0,0 +1,179 @@ +#include "bridge.hh" +#include "router.hh" +#include "../core/trace.hh" + +namespace SSC::IPC { + Router::Router (Bridge* bridge) + : bridge(bridge) + {} + + void Router::init () { + this->mapRoutes(); + this->preserveCurrentTable(); + } + + void Router::preserveCurrentTable () { + this->preserved = this->table; + } + + uint64_t Router::listen ( + const String& name, + const MessageCallback& callback + ) { + const auto key = toLowerCase(name); + + if (!this->listeners.contains(key)) { + this->listeners[key] = Vector<MessageCallbackListenerContext>(); + } + + auto& listeners = this->listeners.at(key); + const auto token = rand64(); + listeners.push_back(MessageCallbackListenerContext { token , callback }); + return token; + } + + bool Router::unlisten (const String& name, uint64_t token) { + const auto key = toLowerCase(name); + if (!this->listeners.contains(key)) { + return false; + } + + auto& listeners = this->listeners.at(key); + for (int i = 0; i < listeners.size(); ++i) { + const auto& listener = listeners[i]; + if (listener.token == token) { + listeners.erase(listeners.begin() + i); + return true; + } + } + + return false; + } + + void Router::map (const String& name, const MessageCallback& callback) { + return this->map(name, true, callback); + } + + void Router::map ( + const String& name, + bool async, + const MessageCallback& callback + ) { + if (callback != nullptr) { + const auto key = toLowerCase(name); + this->table.insert_or_assign(key, MessageCallbackContext { + async, + callback + }); + } + } + + void Router::unmap (const String& name) { + this->table.erase(toLowerCase(name)); + } + + bool Router::invoke ( + const String& uri, + SharedPointer<char[]> bytes, + size_t size + ) { + return this->invoke(uri, bytes, size, [this](auto result) { + this->bridge->dispatch([=, this] () { + this->bridge->send(result.seq, result.str(), result.post); + }); + }); + } + + bool Router::invoke (const String& uri, const ResultCallback& callback) { + return this->invoke(uri, nullptr, 0, callback); + } + + bool Router::invoke ( + const String& uri, + SharedPointer<char[]> bytes, + size_t size, + const ResultCallback& callback + ) { + if (this->bridge->core->isShuttingDown) { + return false; + } + + const auto message = Message(uri, true); + return this->invoke(message, bytes, size, callback); + } + + bool Router::invoke ( + const Message& message, + SharedPointer<char[]> bytes, + size_t size, + const ResultCallback& callback + ) { + if (this->bridge->core->isShuttingDown) { + return false; + } + + const auto name = toLowerCase(message.name); + MessageCallbackContext context; + + // lookup router function in the preserved table, + // then the public table, return if unable to determine a context + if (this->preserved.contains(name)) { + context = this->preserved.at(name); + } else if (this->table.contains(name)) { + context = this->table.at(name); + } else { + return false; + } + + if (context.callback == nullptr) { + return false; + } + + Message msg(message); + + if (bytes != nullptr && size > 0) { + msg.buffer.bytes = bytes; + msg.buffer.size = size; + } + + // named listeners + if (this->listeners.contains(name)) { + const auto listeners = this->listeners[name]; + for (const auto& listener : listeners) { + listener.callback(msg, this, [](const auto& _) {}); + } + } + + // wild card (*) listeners + if (this->listeners.contains("*")) { + const auto listeners = this->listeners["*"]; + for (const auto& listener : listeners) { + listener.callback(msg, this, [](const auto& _) {}); + } + } + + if (context.async) { + return this->bridge->dispatch([=, this]() mutable { + context.callback(msg, this, [callback, this](const auto result) mutable { + if (result.seq == "-1") { + this->bridge->send(result.seq, result.str(), result.post); + } else { + this->bridge->dispatch([result, callback, this]() { + callback(result); + }); + } + }); + }); + } + + context.callback(msg, this, [=, this](const auto result) mutable { + if (result.seq == "-1") { + this->bridge->send(result.seq, result.str(), result.post); + } else { + callback(result); + } + }); + + return true; + } +} diff --git a/src/ipc/router.hh b/src/ipc/router.hh new file mode 100644 index 0000000000..c3ba108d83 --- /dev/null +++ b/src/ipc/router.hh @@ -0,0 +1,70 @@ +#ifndef SOCKET_RUNTIME_IPC_ROUTER_H +#define SOCKET_RUNTIME_IPC_ROUTER_H + +#include "../core/core.hh" +#include "message.hh" +#include "result.hh" + +namespace SSC::IPC { + class Bridge; + class Router { + public: + using ReplyCallback = Function<void(const Result&)>; + using ResultCallback = Function<void(Result)>; + using MessageCallback = Function<void(const Message&, Router*, ReplyCallback)>; + + struct MessageCallbackContext { + bool async = true; + MessageCallback callback; + }; + + struct MessageCallbackListenerContext { + uint64_t token; + MessageCallback callback; + }; + + using Table = std::map<String, MessageCallbackContext>; + using Listeners = std::map<String, std::vector<MessageCallbackListenerContext>>; + + private: + Table preserved; + + public: + Listeners listeners; + Mutex mutex; + Table table; + + Bridge *bridge = nullptr; + + Router () = default; + Router (Bridge* bridge); + Router (const Router&) = delete; + Router (const Router&&) = delete; + Router (Router&&) = delete; + + void init (); + void mapRoutes (); + void preserveCurrentTable (); + uint64_t listen (const String& name, const MessageCallback& callback); + bool unlisten (const String& name, uint64_t token); + void map (const String& name, const MessageCallback& callback); + void map (const String& name, bool async, const MessageCallback& callback); + void unmap (const String& name); + bool invoke (const String& uri, const ResultCallback& callback); + bool invoke (const String& uri, SharedPointer<char[]> bytes, size_t size); + bool invoke ( + const String& uri, + SharedPointer<char[]> bytes, + size_t size, + const ResultCallback& callback + ); + + bool invoke ( + const Message& message, + SharedPointer<char[]> bytes, + size_t size, + const ResultCallback& callback + ); + }; +} +#endif diff --git a/src/ipc/routes.cc b/src/ipc/routes.cc new file mode 100644 index 0000000000..e557b90985 --- /dev/null +++ b/src/ipc/routes.cc @@ -0,0 +1,3859 @@ +#include "../app/app.hh" +#include "../cli/cli.hh" +#include "../core/json.hh" +#include "../core/resource.hh" +#include "../extension/extension.hh" +#include "../window/window.hh" +#include "ipc.hh" + +extern int LLAMA_BUILD_NUMBER; + +#define REQUIRE_AND_GET_MESSAGE_VALUE(var, name, parse, ...) \ + try { \ + var = parse(message.get(name, ##__VA_ARGS__)); \ + } catch (...) { \ + return reply(Result::Err { message, JSON::Object::Entries { \ + {"message", "Invalid '" name "' given in parameters"} \ + }}); \ + } + +#define RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) \ + [message, reply](auto seq, auto json, auto post) { \ + reply(Result { seq, message, json, post }); \ + } + +using namespace SSC; +using namespace SSC::IPC; + +static JSON::Any validateMessageParameters ( + const Message& message, + const Vector<String> names +) { + for (const auto& name : names) { + if (!message.has(name) || message.get(name).size() == 0) { + return JSON::Object::Entries { + {"message", "Expecting '" + name + "' in parameters"} + }; + } + } + + return nullptr; +} + +static void mapIPCRoutes (Router *router) { + auto userConfig = router->bridge->userConfig; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + auto SOCKET_RUNTIME_OS_LOG_BUNDLE = os_log_create(bundleIdentifier.c_str(), "socket.runtime"); + #endif + + /** + * AI + */ + router->map("ai.llm.create", [](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "path", "prompt"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + SSC::LLMOptions options; + options.path = message.get("path"); + options.prompt = message.get("prompt"); + options.antiprompt = message.get("antiprompt"); + + uint64_t modelId = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(modelId, "id", std::stoull); + + if (message.has("n_batch")) REQUIRE_AND_GET_MESSAGE_VALUE(options.n_batch, "n_batch", std::stoi); + if (message.has("n_ctx")) REQUIRE_AND_GET_MESSAGE_VALUE(options.n_ctx, "n_ctx", std::stoi); + if (message.has("n_gpu_layers")) REQUIRE_AND_GET_MESSAGE_VALUE(options.n_gpu_layers, "n_gpu_layers", std::stoi); + if (message.has("n_keep")) REQUIRE_AND_GET_MESSAGE_VALUE(options.n_keep, "n_keep", std::stoi); + if (message.has("n_threads")) REQUIRE_AND_GET_MESSAGE_VALUE(options.n_threads, "n_threads", std::stoi); + if (message.has("n_predict")) REQUIRE_AND_GET_MESSAGE_VALUE(options.n_predict, "n_predict", std::stoi); + if (message.has("grp_attn_n")) REQUIRE_AND_GET_MESSAGE_VALUE(options.grp_attn_n, "grp_attn_n", std::stoi); + if (message.has("grp_attn_w")) REQUIRE_AND_GET_MESSAGE_VALUE(options.grp_attn_w, "grp_attn_w", std::stoi); + if (message.has("max_tokens")) REQUIRE_AND_GET_MESSAGE_VALUE(options.max_tokens, "max_tokens", std::stoi); + if (message.has("seed")) REQUIRE_AND_GET_MESSAGE_VALUE(options.seed, "seed", std::stoi); + if (message.has("temp")) REQUIRE_AND_GET_MESSAGE_VALUE(options.temp, "temp", std::stof); + if (message.has("top_k")) REQUIRE_AND_GET_MESSAGE_VALUE(options.top_k, "top_k", std::stoi); + if (message.has("top_p")) REQUIRE_AND_GET_MESSAGE_VALUE(options.top_p, "top_p", std::stof); + if (message.has("min_p")) REQUIRE_AND_GET_MESSAGE_VALUE(options.min_p, "min_p", std::stof); + if (message.has("tfs_z")) REQUIRE_AND_GET_MESSAGE_VALUE(options.tfs_z, "tfs_z", std::stof); + if (message.has("conversation")) options.conversation = message.get("conversation") == "true"; + if (message.has("chatml")) options.chatml = message.get("chatml") == "true"; + if (message.has("instruct")) options.instruct = message.get("instruct") == "true"; + + router->bridge->core->ai.createLLM(message.seq, modelId, options, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("ai.llm.destroy", [](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + uint64_t modelId = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(modelId, "id", std::stoull); + router->bridge->core->ai.destroyLLM(message.seq, modelId, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("ai.llm.stop", [](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + uint64_t modelId = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(modelId, "id", std::stoull); + router->bridge->core->ai.stopLLM(message.seq, modelId, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("ai.llm.chat", [](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "message"}); + + uint64_t modelId = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(modelId, "id", std::stoull); + + auto value = message.get("message"); + router->bridge->core->ai.chatLLM(message.seq, modelId, value, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Attemps to exit the application + * @param value The exit code + */ + router->map("application.exit", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + const auto err = validateMessageParameters(message, {"value"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + int exitCode; + REQUIRE_AND_GET_MESSAGE_VALUE(exitCode, "value", std::stoi); + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (app->wasLaunchedFromCli) { + debug("__EXIT_SIGNAL__=%d", exitCode); + CLI::notify(); + } + #endif + + const auto window = app->windowManager.getWindow(0); + + if (window == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + window->exit(exitCode); + + reply(Result::Data { message, JSON::Object {} }); + }); + + /** + * Get the screen size available to the application + */ + router->map("application.getScreenSize", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + const auto window = app->windowManager.getWindow(0); + + if (window == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + const auto screenSize = window->getScreenSize(); + const JSON::Object json = JSON::Object::Entries { + { "width", screenSize.width }, + { "height", screenSize.height } + }; + + reply(Result::Data { message, json }); + }); + + /** + * Get all active application windows + * @param value - A list of window indexes to filter on + */ + router->map("application.getWindows", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + const auto window = app->windowManager.getWindow(0); + + if (window == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + const auto requested = split(message.value, ','); + Vector<int> indices; + + if (requested.size() == 0) { + for (const auto& window : app->windowManager.windows) { + if (window != nullptr) { + indices.push_back(window->index); + } + } + } else { + for (const auto& value : requested) { + try { + indices.push_back(std::stoi(value)); + } catch (...) { + return reply(Result::Err { message, "Invalid index given" }); + } + } + } + + const auto json = app->windowManager.json(indices); + reply(Result::Data { message, json }); + }); + + /** + * Set the application tray menu + * @param value - The DSL for the system tray menu + */ + router->map("application.setTrayMenu", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + const auto app = App::sharedApplication(); + const auto err = validateMessageParameters(message, {"value"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + app->dispatch([=]() { + const auto window = app->windowManager.getWindow(0); + + if (window == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + window->setTrayMenu(message.value); + reply(Result::Data { message, JSON::Object {} }); + }); + #else + reply(Result::Err { + message, + JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Application Tray Menu is not supported"} + } + }); + #endif + }); + + /** + * Set the application system menu + * @param value - The DSL for the system tray menu + */ + router->map("application.setSystemMenu", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + const auto app = App::sharedApplication(); + const auto err = validateMessageParameters(message, {"value"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + app->dispatch([=]() { + const auto window = app->windowManager.getWindow(0); + + if (window == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + window->setSystemMenu(message.value); + reply(Result::Data { message, JSON::Object {} }); + }); + #else + reply(Result::Err { + message, + JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Application System Menu is not supported"} + } + }); + #endif + }); + + /** + * Set the application system menu item enabled state + * @param value - The DSL for the system tray menu + * @param enabled - true or false + * @param indexMain + * @param indexSub + */ + router->map("application.setSystemMenuItemEnabled", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + const auto app = App::sharedApplication(); + const auto err = validateMessageParameters(message, {"value", "enabled", "indexMain", "indexSub"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + app->dispatch([=]() { + const auto window = app->windowManager.getWindow(0); + + if (window == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + const auto enabled = message.get("enabled") == "true"; + int indexMain; + int indexSub; + + REQUIRE_AND_GET_MESSAGE_VALUE(indexMain, "indexMain", std::stoi); + REQUIRE_AND_GET_MESSAGE_VALUE(indexSub, "indexSub", std::stoi); + + window->setSystemMenuItemEnabled(enabled, indexMain, indexSub); + + reply(Result::Data { message, JSON::Object {} }); + }); + #else + reply(Result::Err { + message, + JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Application System Menu is not supported"} + } + }); + #endif + }); + + /** + * Starts a bluetooth service + * @param serviceId + */ + router->map("bluetooth.start", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"serviceId"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (router->bridge->userConfig["permissions_allow_bluetooth"] == "false") { + auto err =JSON::Object::Entries { + {"message", "Bluetooth is not allowed"} + }; + + return reply(Result::Err { message, err }); + } + + router->bridge->bluetooth.startService( + message.seq, + message.get("serviceId"), + [reply, message](auto seq, auto json) { + reply(Result { seq, message, json }); + } + ); + }); + + /** + * Subscribes to a characteristic for a service. + * @param serviceId + * @param characteristicId + */ + router->map("bluetooth.subscribe", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, { + "characteristicId", + "serviceId" + }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (router->bridge->userConfig["permissions_allow_bluetooth"] == "false") { + auto err =JSON::Object::Entries { + {"message", "Bluetooth is not allowed"} + }; + + return reply(Result::Err { message, err }); + } + + router->bridge->bluetooth.subscribeCharacteristic( + message.seq, + message.get("serviceId"), + message.get("characteristicId"), + [reply, message](auto seq, auto json) { + reply(Result { seq, message, json }); + } + ); + }); + + /** + * Publishes data to a characteristic for a service. + * @param serviceId + * @param characteristicId + */ + router->map("bluetooth.publish", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, { + "characteristicId", + "serviceId" + }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (router->bridge->userConfig["permissions_allow_bluetooth"] == "false") { + auto err =JSON::Object::Entries { + {"message", "Bluetooth is not allowed"} + }; + + return reply(Result::Err { message, err }); + } + + auto bytes = message.buffer.bytes.get(); + auto size = message.buffer.size; + + if (bytes == nullptr) { + bytes = message.value.data(); + size = message.value.size(); + } + + router->bridge->bluetooth.publishCharacteristic( + message.seq, + bytes, + size, + message.get("serviceId"), + message.get("characteristicId"), + [reply, message](auto seq, auto json) { + reply(Result { seq, message, json }); + } + ); + }); + + /** + * Kills an already spawned child process. + * + * @param id + * @param signal + */ + router->map("child_process.kill", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_IOS + auto err = JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Operation is not supported on this platform"} + }; + + return reply(Result::Err { message, err }); + #else + auto err = validateMessageParameters(message, {"id", "signal"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + int signal; + REQUIRE_AND_GET_MESSAGE_VALUE(signal, "signal", std::stoi); + + router->bridge->core->childProcess.kill( + message.seq, + id, + signal, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + #endif + }); + + /** + * Spawns a child process + * + * @param id + * @param args (command, ...args) + */ + router->map("child_process.spawn", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_IOS + auto err = JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Operation is not supported on this platform"} + }; + + return reply(Result::Err { message, err }); + #else + auto err = validateMessageParameters(message, {"args", "id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto args = split(message.get("args"), 0x0001); + + if (args.size() == 0 || args.at(0).size() == 0) { + auto json = JSON::Object::Entries { + {"source", "child_process.spawn"}, + {"err", JSON::Object::Entries { + {"message", "Spawn requires at least one argument with a length greater than zero"}, + }} + }; + + return reply(Result { message.seq, message, json }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + SSC::Vector<SSC::String> env{}; + + if (message.has("env")) { + env = split(message.get("env"), 0x0001); + } + + const auto options = Core::ChildProcess::SpawnOptions { + .cwd = message.get("cwd", getcwd()), + .env = env, + .allowStdin = message.get("stdin") != "false", + .allowStdout = message.get("stdout") != "false", + .allowStderr = message.get("stderr") != "false" + }; + + router->bridge->core->childProcess.spawn( + message.seq, + id, + args, + options, + [message, reply](auto seq, auto json, auto post) { + reply(Result { seq, message, json, post }); + } + ); + #endif + }); + + router->map("child_process.exec", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_IOS + auto err = JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Operation is not supported on this platform"} + }; + + return reply(Result::Err { message, err }); + #else + auto err = validateMessageParameters(message, {"args", "id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto args = split(message.get("args"), 0x0001); + + if (args.size() == 0 || args.at(0).size() == 0) { + auto json = JSON::Object::Entries { + {"source", "child_process.exec"}, + {"err", JSON::Object::Entries { + {"message", "Spawn requires at least one argument with a length greater than zero"}, + }} + }; + + return reply(Result { message.seq, message, json }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + uint64_t timeout = 0; + int killSignal = 0; + + if (message.has("timeout")) { + REQUIRE_AND_GET_MESSAGE_VALUE(timeout, "timeout", std::stoull); + } + + if (message.has("killSignal")) { + REQUIRE_AND_GET_MESSAGE_VALUE(killSignal, "killSignal", std::stoi); + } + + SSC::Vector<SSC::String> env{}; + + if (message.has("env")) { + env = split(message.get("env"), 0x0001); + } + + const auto options = Core::ChildProcess::ExecOptions { + .cwd = message.get("cwd", getcwd()), + .env = env, + .allowStdout = message.get("stdout") != "false", + .allowStderr = message.get("stderr") != "false", + .timeout = timeout, + .killSignal = killSignal + }; + + router->bridge->core->childProcess.exec( + message.seq, + id, + args, + options, + [message, reply](auto seq, auto json, auto post) { + reply(Result { seq, message, json, post }); + } + ); + #endif + }); + + /** + * Writes to an already spawned child process. + * + * @param id + */ + router->map("child_process.write", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_IOS + auto err = JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Operation is not supported on this platform"} + }; + + return reply(Result::Err { message, err }); + #else + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->childProcess.write( + message.seq, + id, + message.buffer.bytes, + message.buffer.size, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + #endif + }); + + /** + * Query diagnostics information about the runtime core. + */ + router->map("diagnostics.query", [=](auto message, auto router, auto reply) { + router->bridge->core->diagnostics.query( + message.seq, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Look up an IP address by `hostname`. + * @param hostname Host name to lookup + * @param family IP address family to resolve [default = 0 (AF_UNSPEC)] + * @see getaddrinfo(3) + */ + router->map("dns.lookup", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"hostname"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int family = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(family, "family", std::stoi, "0"); + + router->bridge->core->dns.lookup( + message.seq, + Core::DNS::LookupOptions { message.get("hostname"), family }, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + router->map("extension.stats", [=](auto message, auto router, auto reply) { + auto extensions = Extension::all(); + auto name = message.get("name"); + + if (name.size() > 0) { + auto type = Extension::getExtensionType(name); + auto path = Extension::getExtensionPath(name); + auto json = JSON::Object::Entries { + {"source", "extension.stats"}, + {"data", JSON::Object::Entries { + {"abi", SOCKET_RUNTIME_EXTENSION_ABI_VERSION}, + {"name", name}, + {"type", type}, + #if SOCKET_RUNTIME_PLATFORM_ANDROID + {"path", FileResource::getResourcePath(path).string()} + #else + // `path` is absolute to the location of the resources + {"path", String("/") + std::filesystem::relative(path, getcwd()).string()} + #endif + }} + }; + + reply(Result { message.seq, message, json }); + } else { + int loaded = 0; + + for (const auto& tuple : extensions) { + if (tuple.second != nullptr) { + loaded++; + } + } + + auto json = JSON::Object::Entries { + {"source", "extension.stats"}, + {"data", JSON::Object::Entries { + {"abi", SOCKET_RUNTIME_EXTENSION_ABI_VERSION}, + {"loaded", loaded} + }} + }; + + reply(Result { message.seq, message, json }); + } + }); + + /** + * Query for type of extension ('shared', 'wasm32', 'unknown') + * @param name + */ + router->map("extension.type", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"name"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto name = message.get("name"); + auto type = Extension::getExtensionType(name); + auto json = SSC::JSON::Object::Entries { + {"source", "extension.type"}, + {"data", JSON::Object::Entries { + {"name", name}, + {"type", type} + }} + }; + + reply(Result { message.seq, message, json }); + }); + + /** + * Load a named native extension. + * @param name + * @param allow + */ + router->map("extension.load", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"name"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto name = message.get("name"); + + if (!Extension::load(name)) { + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + auto error = formatWindowsError(GetLastError(), "bridge"); + #else + auto err = dlerror(); + auto error = String(err ? err : "Unknown error"); + #endif + + std::cout << "Load extension error: " << error << std::endl; + + return reply(Result::Err { message, JSON::Object::Entries { + {"message", "Failed to load extension: '" + name + "': " + error} + }}); + } + + auto extension = Extension::get(name); + auto allowed = split(message.get("allow"), ','); + auto context = Extension::getContext(name); + auto ctx = context->memory.template alloc<Extension::Context>(context, router); + + for (const auto& value : allowed) { + auto policy = trim(value); + ctx->setPolicy(policy, true); + } + + Extension::setRouterContext(name, router, ctx); + + /// init context + if (!Extension::initialize(ctx, name, nullptr)) { + if (ctx->state == Extension::Context::State::Error) { + auto json = JSON::Object::Entries { + {"source", "extension.load"}, + {"extension", name}, + {"err", JSON::Object::Entries { + {"code", ctx->error.code}, + {"name", ctx->error.name}, + {"message", ctx->error.message}, + {"location", ctx->error.location}, + }} + }; + + reply(Result { message.seq, message, json }); + } else { + auto json = JSON::Object::Entries { + {"source", "extension.load"}, + {"extension", name}, + {"err", JSON::Object::Entries { + {"message", "Failed to initialize extension: '" + name + "'"}, + }} + }; + + reply(Result { message.seq, message, json }); + } + } else { + auto json = JSON::Object::Entries { + {"source", "extension.load"}, + {"data", JSON::Object::Entries { + {"abi", (uint64_t) extension->abi}, + {"name", extension->name}, + {"version", extension->version}, + {"description", extension->description} + }} + }; + + reply(Result { message.seq, message, json }); + } + }); + + /** + * Unload a named native extension. + * @param name + */ + router->map("extension.unload", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"name"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto name = message.get("name"); + + if (!Extension::isLoaded(name)) { + return reply(Result::Err { message, JSON::Object::Entries { + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + {"message", "Extension '" + name + "' is not loaded"} + #else + {"message", "Extension '" + name + "' is not loaded" + String(dlerror())} + #endif + }}); + } + + auto extension = Extension::get(name); + auto ctx = Extension::getRouterContext(name, router); + + if (Extension::unload(ctx, name, extension->contexts.size() == 1)) { + Extension::removeRouterContext(name, router); + auto json = JSON::Object::Entries { + {"source", "extension.unload"}, + {"extension", name}, + {"data", JSON::Object::Entries {}} + }; + return reply(Result { message.seq, message, json }); + } + + if (ctx->state == Extension::Context::State::Error) { + auto json = JSON::Object::Entries { + {"source", "extension.unload"}, + {"extension", name}, + {"err", JSON::Object::Entries { + {"code", ctx->error.code}, + {"name", ctx->error.name}, + {"message", ctx->error.message}, + {"location", ctx->error.location}, + }} + }; + + reply(Result { message.seq, message, json }); + } else { + auto json = JSON::Object::Entries { + {"source", "extension.unload"}, + {"extension", name}, + {"err", JSON::Object::Entries { + {"message", "Failed to unload extension: '" + name + "'"}, + }} + }; + + reply(Result { message.seq, message, json }); + } + }); + + /** + * Checks if current user can access file at `path` with `mode`. + * @param path + * @param mode + * @see access(2) + */ + router->map("fs.access", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path", "mode"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int mode = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); + + router->bridge->core->fs.access( + message.seq, + message.get("path"), + mode, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Returns a mapping of file system constants. + */ + router->map("fs.constants", [=](auto message, auto router, auto reply) { + router->bridge->core->fs.constants(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Changes `mode` of file at `path`. + * @param path + * @param mode + * @see chmod(2) + */ + router->map("fs.chmod", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path", "mode"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int mode = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); + + router->bridge->core->fs.chmod( + message.seq, + message.get("path"), + mode, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Changes uid and gid of file at `path`. + * @param path + * @param uid + * @param gid + * @see chown(2) + */ + router->map("fs.chown", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path", "uid", "gid"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int uid = 0; + int gid = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(uid, "uid", std::stoi); + REQUIRE_AND_GET_MESSAGE_VALUE(gid, "gid", std::stoi); + + router->bridge->core->fs.chown( + message.seq, + message.get("path"), + static_cast<uv_uid_t>(uid), + static_cast<uv_gid_t>(gid), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Changes uid and gid of symbolic link at `path`. + * @param path + * @param uid + * @param gid + * @see lchown(2) + */ + router->map("fs.lchown", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path", "uid", "gid"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int uid = 0; + int gid = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(uid, "uid", std::stoi); + REQUIRE_AND_GET_MESSAGE_VALUE(gid, "gid", std::stoi); + + router->bridge->core->fs.lchown( + message.seq, + message.get("path"), + static_cast<uv_uid_t>(uid), + static_cast<uv_gid_t>(gid), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Closes underlying file descriptor handle. + * @param id + * @see close(2) + */ + router->map("fs.close", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.close(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Closes underlying directory descriptor handle. + * @param id + * @see closedir(3) + */ + router->map("fs.closedir", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.closedir(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Closes an open file or directory descriptor handle. + * @param id + * @see close(2) + * @see closedir(3) + */ + router->map("fs.closeOpenDescriptor", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.closeOpenDescriptor( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Closes all open file and directory descriptors, optionally preserving + * explicitly retrained descriptors. + * @param preserveRetained (default: true) + * @see close(2) + * @see closedir(3) + */ + router->map("fs.closeOpenDescriptors", [=](auto message, auto router, auto reply) { + router->bridge->core->fs.closeOpenDescriptor( + message.seq, + message.get("preserveRetained") != "false", + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Copy file at path `src` to path `dest`. + * @param src + * @param dest + * @param flags + * @see copyfile(3) + */ + router->map("fs.copyFile", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"src", "dest", "flags"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int flags = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(flags, "flags", std::stoi); + + router->bridge->core->fs.copyFile( + message.seq, + message.get("src"), + message.get("dest"), + flags, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Creates a link at `dest` + * @param src + * @param dest + * @see link(2) + */ + router->map("fs.link", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"src", "dest"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.link( + message.seq, + message.get("src"), + message.get("dest"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Creates a symlink at `dest` + * @param src + * @param dest + * @param flags + * @see symlink(2) + */ + router->map("fs.symlink", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"src", "dest", "flags"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int flags = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(flags, "flags", std::stoi); + + router->bridge->core->fs.symlink( + message.seq, + message.get("src"), + message.get("dest"), + flags, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Computes stats for an open file descriptor. + * @param id + * @see stat(2) + * @see fstat(2) + */ + router->map("fs.fstat", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.fstat(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Synchronize a file's in-core state with storage device + * @param id + * @see fsync(2) + */ + router->map("fs.fsync", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.fsync( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Truncates opened file + * @param id + * @param offset + * @see ftruncate(2) + */ + router->map("fs.ftruncate", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "offset"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + int64_t offset; + REQUIRE_AND_GET_MESSAGE_VALUE(offset, "offset", std::stoll); + + router->bridge->core->fs.ftruncate( + message.seq, + id, + offset, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Returns all open file or directory descriptors. + */ + router->map("fs.getOpenDescriptors", [=](auto message, auto router, auto reply) { + router->bridge->core->fs.getOpenDescriptors( + message.seq, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Computes stats for a symbolic link at `path`. + * @param path + * @see stat(2) + * @see lstat(2) + */ + router->map("fs.lstat", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.lstat( + message.seq, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Creates a directory at `path` with an optional mode and an optional recursive flag. + * @param path + * @param mode + * @param recursive + * @see mkdir(2) + */ + router->map("fs.mkdir", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path", "mode"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int mode = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); + + router->bridge->core->fs.mkdir( + message.seq, + message.get("path"), + mode, + message.get("recursive") == "true", + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + + /** + * Opens a file descriptor at `path` for `id` with `flags` and `mode` + * @param id + * @param path + * @param flags + * @param mode + * @see open(2) + */ + router->map("fs.open", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, { + "id", + "path", + "flags", + "mode" + }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + int mode = 0; + int flags = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(mode, "mode", std::stoi); + REQUIRE_AND_GET_MESSAGE_VALUE(flags, "flags", std::stoi); + + router->bridge->core->fs.open( + message.seq, + id, + message.get("path"), + flags, + mode, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Opens a directory descriptor at `path` for `id` with `flags` and `mode` + * @param id + * @param path + * @see opendir(3) + */ + router->map("fs.opendir", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.opendir( + message.seq, + id, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Reads `size` bytes at `offset` from the underlying file descriptor. + * @param id + * @param size + * @param offset + * @see read(2) + */ + router->map("fs.read", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "size", "offset"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + int size = 0; + int offset = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(size, "size", std::stoi); + REQUIRE_AND_GET_MESSAGE_VALUE(offset, "offset", std::stoi); + + router->bridge->core->fs.read( + message.seq, + id, + size, + offset, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Reads next `entries` of from the underlying directory descriptor. + * @param id + * @param entries (default: 256) + */ + router->map("fs.readdir", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + int entries = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(entries, "entries", std::stoi); + + router->bridge->core->fs.readdir( + message.seq, + id, + entries, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Read value of a symbolic link at 'path' + * @param path + * @see readlink(2) + */ + router->map("fs.readlink", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.readlink( + message.seq, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Get the realpath at 'path' + * @param path + * @see realpath(2) + */ + router->map("fs.realpath", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.realpath( + message.seq, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Marks a file or directory descriptor as retained. + * @param id + */ + router->map("fs.retainOpenDescriptor", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.retainOpenDescriptor( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Renames file at path `src` to path `dest`. + * @param src + * @param dest + * @see rename(2) + */ + router->map("fs.rename", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"src", "dest"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.rename( + message.seq, + message.get("src"), + message.get("dest"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Removes file at `path`. + * @param path + * @see rmdir(2) + */ + router->map("fs.rmdir", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.rmdir( + message.seq, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Computes stats for a file at `path`. + * @param path + * @see stat(2) + */ + router->map("fs.stat", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.stat( + message.seq, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Stops a already started watcher + */ + router->map("fs.stopWatch", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.watch( + message.seq, + id, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Removes a file or empty directory at `path`. + * @param path + * @see unlink(2) + */ + router->map("fs.unlink", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + router->bridge->core->fs.unlink( + message.seq, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * TODO + */ + router->map("fs.watch", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "path"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->fs.watch( + message.seq, + id, + message.get("path"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Writes buffer at `message.buffer.bytes` of size `message.buffers.size` + * at `offset` for an opened file handle. + * @param id Handle ID for an open file descriptor + * @param offset The offset to start writing at + * @see write(2) + */ + router->map("fs.write", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "offset"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (message.buffer.bytes == nullptr || message.buffer.size == 0) { + auto err = JSON::Object::Entries {{ "message", "Missing buffer in message" }}; + return reply(Result::Err { message, err }); + } + + + uint64_t id; + int offset = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(offset, "offset", std::stoi); + + router->bridge->core->fs.write( + message.seq, + id, + message.buffer.bytes, + message.buffer.size, + offset, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + router->map("geolocation.getCurrentPosition", [=](auto message, auto router, auto reply) { + router->bridge->core->geolocation.getCurrentPosition( + message.seq, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + router->map("geolocation.watchPosition", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int id = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoi); + + router->bridge->core->geolocation.watchPosition( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + router->map("geolocation.clearWatch", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int id = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoi); + router->bridge->core->geolocation.clearWatch( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + + reply(Result { message.seq, message, JSON::Object{} }); + }); + + /** + * A private API for artifically setting the current cached CWD value. + * This is only useful on platforms that need to set this value from an + * external source, like Android or ChromeOS. + */ + router->map("internal.setcwd", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"value"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + setcwd(message.value); + reply(Result { message.seq, message, JSON::Object{} }); + }); + + /** + * A private API for starting the Runtime `Core::Conduit`, if it isn't running. + */ + router->map("internal.conduit.start", [=](auto message, auto router, auto reply) { + router->bridge->core->conduit.start([=]() { + if (router->bridge->core->conduit.isActive()) { + reply(Result::Data { + message, + JSON::Object::Entries { + {"isActive", true}, + {"port", router->bridge->core->conduit.port.load()} + } + }); + } else { + const auto err = JSON::Object::Entries {{ "message", "Failed to start Conduit"}}; + reply(Result::Err { message, err }); + } + }); + }); + + /** + * A private API for stopping the Runtime `Core::Conduit`, if it is running. + */ + router->map("internal.conduit.stop", [=](auto message, auto router, auto reply) { + router->bridge->core->conduit.stop(); + reply(Result { message.seq, message, JSON::Object{} }); + }); + + /** + * A private API for getting the status of the Runtime `Core::Conduit. + */ + router->map("internal.conduit.status", [=](auto message, auto router, auto reply) { + reply(Result::Data { + message, + JSON::Object::Entries { + {"isActive", router->bridge->core->conduit.isActive()}, + {"port", router->bridge->core->conduit.port.load()} + } + }); + }); + + /** + * Log `value to stdout` with platform dependent logger. + * @param value + */ + router->map("log", [=](auto message, auto router, auto reply) { + auto value = message.value.c_str(); + #if SOCKET_RUNTIME_PLATFORM_APPLE + NSLog(@"%s", value); + os_log_with_type(SOCKET_RUNTIME_OS_LOG_BUNDLE, OS_LOG_TYPE_INFO, "%{public}s", value); + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + __android_log_print(ANDROID_LOG_DEBUG, "", "%s", value); + #else + printf("%s\n", value); + #endif + }); + + router->map("mime.lookup", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, { "value" }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto resource = FileResource(message.value, FileResource::Options { + .cache = false, + .core = router->bridge->core.get() + }); + + reply(Result { message.seq, message, JSON::Object::Entries { + {"url", resource.url.str()}, + {"type", resource.mimeType()} + }}); + }); + + router->map("notification.show", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, { + "id", + "title" + }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + const auto options = Core::Notifications::ShowOptions { + message.get("id"), + message.get("title", "Notification"), + message.get("tag"), + message.get("lang"), + message.get("silent") == "true", + message.get("icon"), + message.get("image"), + message.get("body"), + replace( + message.get("channel", "default"), + "default", + router->bridge->userConfig["meta_bundle_identifier"] + ), + message.get("category"), + message.get("vibrate") + }; + + router->bridge->core->notifications.show(options, [=] (const auto result) { + if (result.error.size() > 0) { + const auto err = JSON::Object::Entries {{ "message", result.error }}; + return reply(Result::Err { message, err }); + } + + const auto data = JSON::Object::Entries {{"id", result.notification.identifier}}; + reply(Result::Data { message, data }); + }); + }); + + router->map("notification.close", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, { "id" }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + + const auto notification = Core::Notifications::Notification { + message.get("id"), + message.get("tag") + }; + + router->bridge->core->notifications.close(notification); + + reply(Result { message.seq, message, JSON::Object::Entries { + {"id", notification.identifier} + }}); + }); + + router->map("notification.list", [=](auto message, auto router, auto reply) { + router->bridge->core->notifications.list([=](const auto notifications) { + JSON::Array entries; + for (const auto& notification : notifications) { + entries.push(notification.json()); + } + + reply(Result::Data { message.seq, entries, }); + }); + }); + + /** + * Read or modify the `SEND_BUFFER` or `RECV_BUFFER` for a peer socket. + * @param id Handle ID for the buffer to read/modify + * @param size If given, the size to set in the buffer [default = 0] + * @param buffer The buffer to read/modify (SEND_BUFFER, RECV_BUFFER) [default = 0 (SEND_BUFFER)] + */ + router->map("os.bufferSize", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + int buffer = 0; + int size = 0; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(buffer, "buffer", std::stoi, "0"); + REQUIRE_AND_GET_MESSAGE_VALUE(size, "size", std::stoi, "0"); + + router->bridge->core->os.bufferSize( + message.seq, + id, + size, + buffer, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Returns a mapping of operating system constants. + */ + router->map("os.constants", [=](auto message, auto router, auto reply) { + router->bridge->core->os.constants(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Returns a mapping of network interfaces. + */ + router->map("os.networkInterfaces", [=](auto message, auto router, auto reply) { + router->bridge->core->os.networkInterfaces(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Returns an array of CPUs available to the process. + */ + router->map("os.cpus", [=](auto message, auto router, auto reply) { + router->bridge->core->os.cpus(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("os.rusage", [=](auto message, auto router, auto reply) { + router->bridge->core->os.rusage(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("os.uptime", [=](auto message, auto router, auto reply) { + router->bridge->core->os.uptime(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("os.uname", [=](auto message, auto router, auto reply) { + router->bridge->core->os.uname(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("os.hrtime", [=](auto message, auto router, auto reply) { + router->bridge->core->os.hrtime(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("os.availableMemory", [=](auto message, auto router, auto reply) { + router->bridge->core->os.availableMemory(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + router->map("os.paths", [=](auto message, auto router, auto reply) { + const auto json = FileResource::getWellKnownPaths().json(); + return reply(Result::Data { message, json }); + }); + + router->map("permissions.query", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"name"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + return router->bridge->core->permissions.query( + message.seq, + message.get("name"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + router->map("permissions.request", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"name"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + return router->bridge->core->permissions.request( + message.seq, + message.get("name"), + message.dump(), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Simply returns `pong`. + */ + router->map("ping", [=](auto message, auto router, auto reply) { + auto result = Result { message.seq, message }; + result.data = "pong"; + reply(result); + }); + + /** + * Handles platform events. + * @param value The event name [domcontentloaded] + * @param data Optional data associated with the platform event. + */ + router->map("platform.event", [=](auto message, auto router, auto reply) { + const auto err = validateMessageParameters(message, {"value"}); + const auto app = App::sharedApplication(); + const auto window = app->windowManager.getWindowForBridge(router->bridge); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + if (message.value == "readystatechange") { + const auto err = validateMessageParameters(message, {"state"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + const auto state = message.get("state"); + + if (state == "loading") { + window->readyState = Window::ReadyState::Loading; + } else if (state == "interactive") { + window->readyState = Window::ReadyState::Interactive; + } else if (state == "complete") { + window->readyState = Window::ReadyState::Complete; + } + + window->onReadyStateChange(window->readyState); + } + + const auto frameType = message.get("runtime-frame-type"); + const auto frameSource = message.get("runtime-frame-source"); + auto userConfig = router->bridge->userConfig; + + if (frameType == "top-level" && frameSource != "serviceworker") { + if (message.value == "load") { + const auto href = message.get("location.href"); + if (href.size() > 0) { + router->bridge->navigator.location.set(href); + router->bridge->navigator.location.workers.clear(); + auto tmp = href; + tmp = replace(tmp, "socket://", ""); + tmp = replace(tmp, "https://", ""); + tmp = replace(tmp, userConfig["meta_bundle_identifier"], ""); + const auto parsed = URL::Components::parse(tmp); + router->bridge->navigator.location.pathname = parsed.pathname; + router->bridge->navigator.location.query = parsed.query; + } + } + + if (router->bridge == router->bridge->navigator.serviceWorker.bridge) { + if (router->bridge->userConfig["webview_service_worker_mode"] == "hybrid") { + if (router->bridge->navigator.location.href.size() > 0 && message.value == "beforeruntimeinit") { + router->bridge->navigator.serviceWorker.reset(); + router->bridge->navigator.serviceWorker.isReady = false; + } else if (message.value == "runtimeinit") { + router->bridge->navigator.serviceWorker.isReady = true; + } + } + } + } + + if (message.value == "load" && frameType == "worker") { + const auto workerLocation = message.get("runtime-worker-location"); + const auto href = message.get("location.href"); + if (href.size() > 0 && workerLocation.size() > 0) { + router->bridge->navigator.location.workers[href] = workerLocation; + } + } + + router->bridge->core->platform.event( + message.seq, + message.value, + message.get("data"), + frameType, + frameSource, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Reveal a file in the native operating system file system explorer. + * @param value + */ + router->map("platform.revealFile", [=](auto message, auto router, auto reply) mutable { + auto err = validateMessageParameters(message, {"value"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + router->bridge->core->platform.revealFile( + message.seq, + message.get("value"), + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Requests a URL to be opened externally. + * @param value + */ + router->map("platform.openExternal", [=](auto message, auto router, auto reply) mutable { + const auto applicationProtocol = router->bridge->userConfig["meta_application_protocol"]; + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"value"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + if (applicationProtocol.size() > 0 && message.value.starts_with(applicationProtocol + ":")) { + SSC::JSON::Object json = SSC::JSON::Object::Entries { + { "url", message.value } + }; + + const auto window = app->windowManager.getWindowForBridge(router->bridge); + + if (window) { + window->handleApplicationURL(message.value); + } + + reply(Result { + message.seq, + message, + SSC::JSON::Object::Entries { + {"data", json} + } + }); + return; + } + + router->bridge->core->platform.openExternal( + message.seq, + message.value, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Return Socket Runtime primordials. + */ + router->map("platform.primordials", [=](auto message, auto router, auto reply) { + std::regex platform_pattern("^mac$", std::regex_constants::icase); + auto platformRes = std::regex_replace(platform.os, platform_pattern, "darwin"); + auto arch = std::regex_replace(platform.arch, std::regex("x86_64"), "x64"); + arch = std::regex_replace(arch, std::regex("x86"), "ia32"); + arch = std::regex_replace(arch, std::regex("arm(?!64).*"), "arm"); + auto json = JSON::Object::Entries { + {"source", "platform.primordials"}, + {"data", JSON::Object::Entries { + {"arch", arch}, + {"cwd", getcwd()}, + {"platform", platformRes}, + {"version", JSON::Object::Entries { + {"full", SSC::VERSION_FULL_STRING}, + {"short", SSC::VERSION_STRING}, + {"hash", SSC::VERSION_HASH_STRING}} + }, + {"uv", JSON::Object::Entries { + {"version", uv_version_string()} + }}, + {"llama", JSON::Object::Entries { + {"version", String("0.0.") + std::to_string(LLAMA_BUILD_NUMBER)} + }}, + {"host-operating-system", + #if SOCKET_RUNTIME_PLATFORM_APPLE + #if SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR + "iphonesimulator" + #elif SOCKET_RUNTIME_PLATFORM_IOS + "iphoneos" + #else + "macosx" + #endif + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + (router->bridge->isAndroidEmulator ? "android-emulator" : "android") + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + "win32" + #elif SOCKET_RUNTIME_PLATFORM_LINUX + "linux" + #elif SOCKET_RUNTIME_PLATFORM_UNIX + "unix" + #else + "unknown" + #endif + } + }} + }; + reply(Result { message.seq, message, json }); + }); + + /** + * Returns pending post data typically returned in the response of an + * `ipc://post` IPC call intercepted by an XHR request. + * @param id The id of the post data. + */ + router->map("post", false, [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + if (!router->bridge->core->hasPost(id)) { + return reply(Result::Err { message, JSON::Object::Entries { + {"id", std::to_string(id)}, + {"type", "NotFoundError"}, + {"message", "A 'Post' was not found for the given 'id' in parameters"} + }}); + } + + auto result = Result { message.seq, message }; + result.post = router->bridge->core->getPost(id); + if (result.post.headers.size() > 0) { + const auto lines = split(trim(result.post.headers), '\n'); + for (const auto& line : lines) { + const auto pair = split(trim(line), ':'); + const auto key = trim(pair[0]); + const auto value = trim(pair[1]); + result.headers.set(key, value); + } + } + reply(result); + router->bridge->core->removePost(id); + }); + + /** + * Registers a custom protocol handler scheme. Custom protocols MUST be handled in service workers. + * @param scheme + * @param data + */ + router->map("protocol.register", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"scheme"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + const auto scheme = message.get("scheme"); + const auto data = message.get("data"); + + if (data.size() > 0 && router->bridge->navigator.serviceWorker.protocols.hasHandler(scheme)) { + router->bridge->navigator.serviceWorker.protocols.setHandlerData(scheme, { data }); + } else { + router->bridge->navigator.serviceWorker.protocols.registerHandler(scheme, { data }); + } + + reply(Result { message.seq, message }); + }); + + /** + * Unregister a custom protocol handler scheme. + * @param scheme + */ + router->map("protocol.unregister", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"scheme"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + const auto scheme = message.get("scheme"); + + if (!router->bridge->navigator.serviceWorker.protocols.hasHandler(scheme)) { + return reply(Result::Err { message, JSON::Object::Entries { + {"message", "Protocol handler scheme is not registered."} + }}); + } + + router->bridge->navigator.serviceWorker.protocols.unregisterHandler(scheme); + + reply(Result { message.seq, message }); + }); + + /** + * Gets protocol handler data + * @param scheme + */ + router->map("protocol.getData", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"scheme"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + const auto scheme = message.get("scheme"); + + if (!router->bridge->navigator.serviceWorker.protocols.hasHandler(scheme)) { + return reply(Result::Err { message, JSON::Object::Entries { + {"message", "Protocol handler scheme is not registered."} + }}); + } + + const auto data = router->bridge->navigator.serviceWorker.protocols.getHandlerData(scheme); + + reply(Result { message.seq, message, JSON::Raw(data.json) }); + }); + + /** + * Sets protocol handler data + * @param scheme + * @param data + */ + router->map("protocol.setData", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"scheme", "data"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + const auto scheme = message.get("scheme"); + const auto data = message.get("data"); + + if (!router->bridge->navigator.serviceWorker.protocols.hasHandler(scheme)) { + return reply(Result::Err { message, JSON::Object::Entries { + {"message", "Protocol handler scheme is not registered."} + }}); + } + + router->bridge->navigator.serviceWorker.protocols.setHandlerData(scheme, { data }); + + reply(Result { message.seq, message }); + }); + + /** + * Prints incoming message value to stdout. + * @param value + */ + router->map("stdout", [=](auto message, auto router, auto reply) { + if (message.value.size() > 0) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + int seq = ++router->bridge->core->logSeq; + auto msg = String(std::to_string(seq) + "::::" + message.value.c_str()); + os_log_with_type(SOCKET_RUNTIME_OS_LOG_BUNDLE, OS_LOG_TYPE_INFO, "%{public}s", msg.c_str()); + + if (Env::get("SSC_LOG_SOCKET").size() > 0) { + Core::UDP::SendOptions options; + options.size = 2; + options.bytes = SharedPointer<char[]>(new char[3]{ '+', 'N', '\0' }); + options.address = "0.0.0.0"; + options.port = std::stoi(Env::get("SSC_LOG_SOCKET")); + options.ephemeral = true; + router->bridge->core->udp.send("-1", 0, options, [](auto seq, auto json, auto post) {}); + } + #endif + IO::write(message.value, false); + } else if (message.buffer.bytes != nullptr && message.buffer.size > 0) { + IO::write(String(message.buffer.bytes.get(), message.buffer.size), false); + } + + reply(Result { message.seq, message }); + }); + + /** + * Prints incoming message value to stderr. + * @param value + */ + router->map("stderr", [=](auto message, auto router, auto reply) { + if (message.get("debug") == "true") { + if (message.value.size() > 0) { + debug("%s", message.value.c_str()); + } + } else if (message.value.size() > 0) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + int seq = ++router->bridge->core->logSeq; + auto msg = String(std::to_string(seq) + "::::" + message.value.c_str()); + os_log_with_type(SOCKET_RUNTIME_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", msg.c_str()); + + if (Env::get("SSC_LOG_SOCKET").size() > 0) { + Core::UDP::SendOptions options; + options.size = 2; + options.bytes = SharedPointer<char[]>(new char[3]{ '+', 'N', '\0' }); + options.address = "0.0.0.0"; + options.port = std::stoi(Env::get("SSC_LOG_SOCKET")); + options.ephemeral = true; + router->bridge->core->udp.send("-1", 0, options, [](auto seq, auto json, auto post) {}); + } + #endif + IO::write(message.value, true); + } else if (message.buffer.bytes != nullptr && message.buffer.size > 0) { + IO::write(String(message.buffer.bytes.get(), message.buffer.size), true); + } + + reply(Result { message.seq, message }); + }); + + /** + * Registers a service worker script for a given scope. + * @param scriptURL + * @param scope + */ + router->map("serviceWorker.register", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"scriptURL", "scope"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + const auto options = ServiceWorkerContainer::RegistrationOptions { + .type = ServiceWorkerContainer::RegistrationOptions::Type::Module, + .scope = message.get("scope"), + .scriptURL = message.get("scriptURL") + }; + + const auto registration = router->bridge->navigator.serviceWorker.registerServiceWorker(options); + auto json = JSON::Object { + JSON::Object::Entries { + {"registration", registration.json()} + } + }; + + reply(Result::Data { message, json }); + }); + + /** + * Resets the service worker container state. + */ + router->map("serviceWorker.reset", [=](auto message, auto router, auto reply) { + router->bridge->navigator.serviceWorker.reset(); + reply(Result::Data { message, JSON::Object {}}); + }); + + /** + * Unregisters a service worker for given scoep. + * @param scope + */ + router->map("serviceWorker.unregister", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"scope"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + const auto scope = message.get("scope"); + router->bridge->navigator.serviceWorker.unregisterServiceWorker(scope); + + return reply(Result::Data { message, JSON::Object {} }); + }); + + /** + * Gets registration information for a service worker scope. + * @param scope + */ + router->map("serviceWorker.getRegistration", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"scope"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + const auto scope = message.get("scope"); + + for (const auto& entry : router->bridge->navigator.serviceWorker.registrations) { + const auto& registration = entry.second; + if (scope.starts_with(registration.options.scope)) { + auto json = JSON::Object { + JSON::Object::Entries { + {"registration", registration.json()}, + {"client", JSON::Object::Entries { + {"id", std::to_string(router->bridge->id)} + }} + } + }; + + return reply(Result::Data { message, json }); + } + } + + return reply(Result::Data { message, JSON::Object {} }); + }); + + /** + * Gets all service worker scope registrations. + */ + router->map("serviceWorker.getRegistrations", [=](auto message, auto router, auto reply) { + auto json = JSON::Array::Entries {}; + for (const auto& entry : router->bridge->navigator.serviceWorker.registrations) { + const auto& registration = entry.second; + json.push_back(registration.json()); + } + return reply(Result::Data { message, json }); + }); + + /** + * Informs container that a service worker will skip waiting. + * @param id + */ + router->map("serviceWorker.skipWaiting", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->navigator.serviceWorker.skipWaiting(id); + + reply(Result::Data { message, JSON::Object {}}); + }); + + /** + * Updates service worker controller state. + * @param id + * @param state + */ + router->map("serviceWorker.updateState", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "state"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + const auto workerURL = message.get("workerURL"); + const auto scriptURL = message.get("scriptURL"); + + if (workerURL.size() > 0 && scriptURL.size() > 0) { + router->bridge->navigator.location.workers[workerURL] = scriptURL; + } + + router->bridge->navigator.serviceWorker.updateState(id, message.get("state")); + reply(Result::Data { message, JSON::Object {}}); + }); + + /** + * Sets storage for a service worker. + * @param id + * @param key + * @param value + */ + router->map("serviceWorker.storage.set", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "key", "value"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + for (auto& entry : router->bridge->navigator.serviceWorker.registrations) { + if (entry.second.id == id) { + auto& registration = entry.second; + registration.storage.set(message.get("key"), message.get("value")); + return reply(Result::Data { message, JSON::Object {}}); + } + } + + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Not found"}, + {"type", "NotFoundError"} + } + }); + }); + + /** + * Gets a storage value for a service worker. + * @param id + * @param key + */ + router->map("serviceWorker.storage.get", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "key"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + for (auto& entry : router->bridge->navigator.serviceWorker.registrations) { + if (entry.second.id == id) { + auto& registration = entry.second; + return reply(Result::Data { + message, + JSON::Object::Entries { + {"value", registration.storage.get(message.get("key"))} + } + }); + } + } + + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Not found"}, + {"type", "NotFoundError"} + } + }); + }); + + /** + * Remoes a storage value for a service worker. + * @param id + * @param key + */ + router->map("serviceWorker.storage.remove", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "key"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + for (auto& entry : router->bridge->navigator.serviceWorker.registrations) { + if (entry.second.id == id) { + auto& registration = entry.second; + registration.storage.remove(message.get("key")); + return reply(Result::Data {message, JSON::Object {}}); + } + } + + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Not found"}, + {"type", "NotFoundError"} + } + }); + }); + + /** + * Clears all storage values for a service worker. + * @param id + */ + router->map("serviceWorker.storage.clear", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + for (auto& entry : router->bridge->navigator.serviceWorker.registrations) { + if (entry.second.id == id) { + auto& registration = entry.second; + registration.storage.clear(); + return reply(Result::Data { message, JSON::Object {} }); + } + } + + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Not found"}, + {"type", "NotFoundError"} + } + }); + }); + + /** + * Gets all storage values for a service worker. + * @param id + */ + router->map("serviceWorker.storage", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + for (auto& entry : router->bridge->navigator.serviceWorker.registrations) { + if (entry.second.id == id) { + auto& registration = entry.second; + return reply(Result::Data { message, registration.storage.json() }); + } + } + + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Not found"}, + {"type", "NotFoundError"} + } + }); + }); + + router->map("timers.setTimeout", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"timeout"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint32_t timeout; + REQUIRE_AND_GET_MESSAGE_VALUE(timeout, "timeout", std::stoul); + const auto wait = message.get("wait") == "true"; + const Core::Timers::ID id = router->bridge->core->timers.setTimeout(timeout, [=]() { + if (wait) { + reply(Result::Data { message, JSON::Object::Entries {{"id", std::to_string(id) }}}); + } + }); + + if (!wait) { + reply(Result::Data { message, JSON::Object::Entries {{"id", std::to_string(id) }}}); + } + }); + + router->map("timers.clearTimeout", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result { message.seq, message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + router->bridge->core->timers.clearTimeout(id); + + reply(Result::Data { message, JSON::Object::Entries {{"id", std::to_string(id) }}}); + }); + + /** + * Binds an UDP socket to a specified port, and optionally a host + * address (default: 0.0.0.0). + * @param id Handle ID of underlying socket + * @param port Port to bind the UDP socket to + * @param address The address to bind the UDP socket to (default: 0.0.0.0) + * @param reuseAddr Reuse underlying UDP socket address (default: false) + */ + router->map("udp.bind", [=](auto message, auto router, auto reply) { + Core::UDP::BindOptions options; + auto err = validateMessageParameters(message, {"id", "port"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(options.port, "port", std::stoi); + + options.reuseAddr = message.get("reuseAddr") == "true"; + options.address = message.get("address", "0.0.0.0"); + + router->bridge->core->udp.bind( + message.seq, + id, + options, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Close socket handle and underlying UDP socket. + * @param id Handle ID of underlying socket + */ + router->map("udp.close", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->udp.close(message.seq, id, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); + }); + + /** + * Connects an UDP socket to a specified port, and optionally a host + * address (default: 0.0.0.0). + * @param id Handle ID of underlying socket + * @param port Port to connect the UDP socket to + * @param address The address to connect the UDP socket to (default: 0.0.0.0) + */ + router->map("udp.connect", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "port"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + Core::UDP::ConnectOptions options; + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(options.port, "port", std::stoi); + + options.address = message.get("address", "0.0.0.0"); + + router->bridge->core->udp.connect( + message.seq, + id, + options, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Disconnects a connected socket handle and underlying UDP socket. + * @param id Handle ID of underlying socket + */ + router->map("udp.disconnect", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->udp.disconnect( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Returns connected peer socket address information. + * @param id Handle ID of underlying socket + */ + router->map("udp.getPeerName", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->udp.getPeerName( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Returns local socket address information. + * @param id Handle ID of underlying socket + */ + router->map("udp.getSockName", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->udp.getSockName( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Returns socket state information. + * @param id Handle ID of underlying socket + */ + router->map("udp.getState", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->udp.getState( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Initializes socket handle to start receiving data from the underlying + * socket and route through the IPC bridge to the WebView. + * @param id Handle ID of underlying socket + */ + router->map("udp.readStart", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->udp.readStart( + message.seq, + id, + [&, id, router, message, reply](auto seq, auto json, auto post) { + if (seq == "-1" && router->bridge->core->conduit.has(id)) { + auto data = json["data"]; + + CoreConduit::Options options = { + { "port", data["port"].str() }, + { "address", data["address"].template as<JSON::String>().data } + }; + + auto client = router->bridge->core->conduit.get(id); + client->emit(options, post.body, post.length); + return; + } + + reply(Result { seq, message, json, post }); + } + ); + }); + + /** + * Stops socket handle from receiving data from the underlying + * socket and routing through the IPC bridge to the WebView. + * @param id Handle ID of underlying socket + */ + router->map("udp.readStop", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + + router->bridge->core->udp.readStop( + message.seq, + id, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Broadcasts a datagram on the socket. For connectionless sockets, the + * destination port and address must be specified. Connected sockets, on the + * other hand, will use their associated remote endpoint, so the port and + * address arguments must not be set. + * @param id Handle ID of underlying socket + * @param port The port to send data to + * @param size The size of the bytes to send + * @param bytes A pointer to the bytes to send + * @param address The address to send to (default: 0.0.0.0) + * @param ephemeral Indicates that the socket handle, if created is ephemeral and should eventually be destroyed + */ + router->map("udp.send", [=](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"id", "port"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + Core::UDP::SendOptions options; + uint64_t id; + REQUIRE_AND_GET_MESSAGE_VALUE(id, "id", std::stoull); + REQUIRE_AND_GET_MESSAGE_VALUE(options.port, "port", std::stoi); + + options.size = message.buffer.size; + options.ephemeral = message.get("ephemeral") == "true"; + options.address = message.get("address", "0.0.0.0"); + options.bytes = message.buffer.bytes; + + router->bridge->core->udp.send( + message.seq, + id, + options, + RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply) + ); + }); + + /** + * Show the file system picker dialog + * @param allowMultiple + * @param allowFiles + * @param allowDirs + * @param type + * @param contentTypeSpecs + * @param defaultName + * @param defaultPath + * @param title + */ + router->map("window.showFileSystemPicker", [=](auto message, auto router, auto reply) { + const auto allowMultiple = message.get("allowMultiple") == "true"; + const auto allowFiles = message.get("allowFiles") == "true"; + const auto allowDirs = message.get("allowDirs") == "true"; + const auto isSave = message.get("type") == "save"; + + const auto contentTypeSpecs = message.get("contentTypeSpecs"); + const auto defaultName = message.get("defaultName"); + const auto defaultPath = message.get("defaultPath"); + const auto title = message.get("title", isSave ? "Save" : "Open"); + const auto app = App::sharedApplication(); + const auto window = app->windowManager.getWindowForBridge(router->bridge); + + app->dispatch([=]() { + Dialog* dialog = nullptr; + + if (window) { + dialog = &window->dialog; + } else { + dialog = new Dialog(); + } + + const auto options = Dialog::FileSystemPickerOptions { + .prefersDarkMode = message.get("prefersDarkMode") == "true", + .directories = allowDirs, + .multiple = allowMultiple, + .files = allowFiles, + .contentTypes = contentTypeSpecs, + .defaultName = defaultName, + .defaultPath = defaultPath, + .title = title + }; + + const auto callback = [=](Vector<String> results) { + JSON::Array paths; + + if (results.size() == 0) { + const auto err = JSON::Object::Entries {{"type", "AbortError"}}; + return reply(Result::Err { message, err }); + } + + for (const auto& result : results) { + paths.push(result); + } + + const auto data = JSON::Object::Entries { + {"paths", paths} + }; + + reply(Result::Data { message, data }); + }; + + if (isSave) { + if (!dialog->showSaveFilePicker(options, callback)) { + const auto err = JSON::Object::Entries {{"type", "AbortError"}}; + reply(Result::Err { message, err }); + } + } else { + const auto result = ( + allowFiles && !allowDirs + ? dialog->showOpenFilePicker(options, callback) + : dialog->showDirectoryPicker(options, callback) + ); + + if (!result) { + const auto err = JSON::Object::Entries {{"type", "AbortError"}}; + reply(Result::Err { message, err }); + } + } + + if (!window) { + delete dialog; + } + }); + }); + + /** + * Closes a target window + * @param targetWindowIndex + */ + router->map("window.close", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + + if (!window) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + reply(Result::Data { message, window->json() }); + + app->core->setTimeout(16, [=] () { + app->windowManager.destroyWindow(targetWindowIndex); + }); + }); + + /** + * Creates a new window + * @param url + * @param title + * @param shouldExitApplicationOnClose + * @param headless + * @param radius + * @param margin + * @param height + * @param width + * @param minWidth + * @param minHeight + * @param maxWidth + * @param maxHeight + * @param resizable + * @param frameless + * @param closable + * @param maximizable + * @param minimizable + * @param aspectRatio + * @param titlebarStyle + * @param windowControlOffsets + * @param backgroundColorLight + * @param backgroundColorDark + * @param utility + * @param userScript + * @param userConfig + * @param targetWindowIndex + */ + router->map("window.create", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + if ( + targetWindowIndex >= SOCKET_RUNTIME_MAX_WINDOWS && + message.get("headless") != "true" && + message.get("debug") != "true" + ) { + static const auto maxWindows = std::to_string(SOCKET_RUNTIME_MAX_WINDOWS); + return reply(Result::Err { + message, + "Cannot create widow with an index beyond " + maxWindows + }); + } + + app->dispatch([=]() { + if ( + app->windowManager.getWindow(targetWindowIndex) != nullptr && + app->windowManager.getWindowStatus(targetWindowIndex) != WindowManager::WindowStatus::WINDOW_NONE + ) { + return reply(Result::Err { + message, + "Window with index " + message.get("targetWindowIndex") + " already exists" + }); + } + + const auto window = app->windowManager.getWindow(0); + const auto screen = window->getScreenSize(); + auto options = Window::Options {}; + + options.shouldExitApplicationOnClose = message.get("shouldExitApplicationOnClose") == "true" ? true : false; + options.headless = app->userConfig["build_headless"] == "true"; + + if (message.get("headless") == "true") { + options.headless = true; + } else if (message.get("headless") == "false") { + options.headless = false; + } + + try { + if (message.has("radius")) { + options.radius = std::stof(message.get("radius")); + } + } catch (...) {} + + try { + if (message.has("margin")) { + options.margin = std::stof(message.get("margin")); + } + } catch (...) {} + + options.width = message.get("width").size() + ? window->getSizeInPixels(message.get("width"), screen.width) + : 0; + + options.height = message.get("height").size() + ? window->getSizeInPixels(message.get("height"), screen.height) + : 0; + + options.minWidth = message.get("minWidth").size() + ? window->getSizeInPixels(message.get("minWidth"), screen.width) + : 0; + + options.minHeight = message.get("minHeight").size() + ? window->getSizeInPixels(message.get("minHeight"), screen.height) + : 0; + + options.maxWidth = message.get("maxWidth").size() + ? window->getSizeInPixels(message.get("maxWidth"), screen.width) + : screen.width; + + options.maxHeight = message.get("maxHeight").size() + ? window->getSizeInPixels(message.get("maxHeight"), screen.height) + : screen.height; + + options.resizable = message.get("resizable") == "true" ? true : false; + options.frameless = message.get("frameless") == "true" ? true : false; + options.closable = message.get("closable") == "true" ? true : false; + options.maximizable = message.get("maximizable") == "true" ? true : false; + options.minimizable = message.get("minimizable") == "true" ? true : false; + options.aspectRatio = message.get("aspectRatio"); + options.titlebarStyle = message.get("titlebarStyle"); + options.windowControlOffsets = message.get("windowControlOffsets"); + options.backgroundColorLight = message.get("backgroundColorLight"); + options.backgroundColorDark = message.get("backgroundColorDark"); + options.utility = message.get("utility") == "true" ? true : false; + options.debug = message.get("debug") == "true" ? true : false; + options.userScript = message.get("userScript"); + options.index = targetWindowIndex; + options.RUNTIME_PRIMORDIAL_OVERRIDES = message.get("__runtime_primordial_overrides__"); + options.userConfig = INI::parse(message.get("config")); + + if (options.index >= SOCKET_RUNTIME_MAX_WINDOWS) { + options.features.useGlobalCommonJS = false; + } + + auto createdWindow = app->windowManager.createWindow(options); + + if (message.has("title")) { + createdWindow->setTitle(message.get("title")); + } + + if (message.has("url")) { + createdWindow->navigate(message.get("url")); + } + + if (!options.headless) { + createdWindow->show(); + } + + reply(Result::Data { message, createdWindow->json() }); + }); + }); + + /** + * Gets the background color of a target window window + * @param targetWindowIndex + */ + router->map("window.getBackgroundColor", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + app->dispatch([=]() { + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + reply(Result::Data { message, window->getBackgroundColor() }); + }); + }); + + /** + * Gets the title of a target window + * @param targetWindowIndex + */ + router->map("window.getTitle", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + app->dispatch([=]() { + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + reply(Result::Data { message, window->getTitle() }); + }); + }); + + /** + * Hides a target window + * @param targetWindowIndex + */ + router->map("window.hide", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + window->hide(); + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Maximize a target window + * @param targetWindowIndex + */ + router->map("window.maximize", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + window->maximize(); + #else + const auto screen = window->getScreenSize(); + window->setSize(screen.width, screen.height); + window->show(); + #endif + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Minimize a target window + * @param targetWindowIndex + */ + router->map("window.minimize", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + window->minimize(); + #else + window->hide(); + #endif + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Navigate a targetbnnb + * @param targetWindowIndex + */ + router->map("window.navigate", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex", "url"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + const auto requestedURL = message.get("url"); + const auto allowed = window->bridge.navigator.isNavigationRequestAllowed( + window->bridge.navigator.location.href, + requestedURL + ); + + if (!allowed) { + return reply(Result::Err { message, "Navigation to URL is not allowed" }); + } + + app->dispatch([=]() { + window->bridge.navigator.location.set(requestedURL); + window->bridge.navigate(requestedURL); + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Restore a target window + * @param targetWindowIndex + */ + router->map("window.restore", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + window->restore(); + #else + window->show(); + #endif + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Send an event to another window + * @param event + * @param value + * @param targetWindowIndex + */ + router->map("window.send", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + const auto event = message.get("event"); + const auto value = message.get("value"); + const auto targetWindowIndex = message.get("targetWindowIndex").size() >= 0 ? std::stoi(message.get("targetWindowIndex")) : -1; + + if (targetWindowIndex < 0) { + return reply(Result::Err { message, "Invalid target window index" }); + } + + const auto targetWindow = app->windowManager.getWindow(targetWindowIndex); + + if (!targetWindow) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + targetWindow->eval(getEmitToRenderProcessJavaScript(event, value)); + reply(Result { message.seq, message }); + }); + }); + + /** + * Sets the background color + * @param targetWindowIndex + * @param red + * @param green + * @param blue + * @param alpha + * + */ + router->map("window.setBackgroundColor", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + int red = 0; + int green = 0; + int blue = 0; + float alpha = 1; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + if (message.has("red")) { + REQUIRE_AND_GET_MESSAGE_VALUE(red, "red", std::stoi); + } + + if (message.has("green")) { + REQUIRE_AND_GET_MESSAGE_VALUE(green, "green", std::stoi); + } + + if (message.has("blue")) { + REQUIRE_AND_GET_MESSAGE_VALUE(blue, "blue", std::stoi); + } + + if (message.has("alpha")) { + REQUIRE_AND_GET_MESSAGE_VALUE(alpha, "alpha", std::stof); + } + + if (alpha > 1 || alpha < 0) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Invalid 'alpha' parameter given"}, + {"type", "RangeError"} + } + }); + } + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + window->setBackgroundColor(red, green, blue, alpha); + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Creates and displays a context menu at the current mouse position (desktop only) + * @param value + */ + router->map("window.setContextMenu", [=](auto message, auto router, auto reply) { + #if SOCKET_RUNTIME_PLATFORM_DESKTOP + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"index", "value"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + const auto window = app->windowManager.getWindow(message.index); + const auto windowStatus = app->windowManager.getWindowStatus(message.index); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + window->setContextMenu(message.seq, message.value); + reply(Result::Data { message, JSON::Object {} }); + }); + #else + reply(Result::Err { + message, + JSON::Object::Entries { + {"type", "NotSupportedError"}, + {"message", "Setting a window context menu is not supported"} + } + }); + #endif + }); + + /** + * Sets the position of a target window + * @param targetWindowIndex + * @param height + * @param width + */ + router->map("window.setPosition", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex", "x", "y"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + const auto screen = window->getScreenSize(); + const auto x = Window::getSizeInPixels(message.get("x"), screen.width); + const auto y = Window::getSizeInPixels(message.get("y"), screen.height); + + app->dispatch([=]() { + window->setPosition(x, y); + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Sets the size of a target window (desktop only) + * @param targetWindowIndex + * @param height + * @param width + */ + router->map("window.setSize", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex", "height", "width"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + const auto screen = window->getScreenSize(); + const auto width = window->getSizeInPixels(message.get("width"), screen.width); + const auto height = window->getSizeInPixels(message.get("height"), screen.height); + + app->dispatch([=]() { + window->setSize(width, height, 0); + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Sets the title of a target windo + * @param targetWindowIndex + * @param value + */ + router->map("window.setTitle", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex", "value"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + window->setTitle(message.value); + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Shows a target window + * @param targetWindowIndex + */ + router->map("window.show", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=]() { + window->show(); + reply(Result::Data { message, window->json() }); + }); + }); + + /** + * Shows the target window web inspector (desktop only) + * @param targetWindowIndex + */ + router->map("window.showInspector", [=](auto message, auto router, auto reply) { + const auto app = App::sharedApplication(); + auto err = validateMessageParameters(message, {"targetWindowIndex"}); + + if (app == nullptr) { + return reply(Result::Err { message, "Application is invalid state" }); + } + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + int targetWindowIndex; + + REQUIRE_AND_GET_MESSAGE_VALUE(targetWindowIndex, "targetWindowIndex", std::stoi); + + const auto window = app->windowManager.getWindow(targetWindowIndex); + const auto windowStatus = app->windowManager.getWindowStatus(targetWindowIndex); + + if (!window || windowStatus == WindowManager::WindowStatus::WINDOW_NONE) { + return reply(Result::Err { + message, + JSON::Object::Entries { + {"message", "Target window not found"}, + {"type", "NotFoundError"} + } + }); + } + + app->dispatch([=] () { + window->showInspector(); + reply(Result::Data { message, window->json() }); + }); + }); +} + +namespace SSC::IPC { + void Router::mapRoutes () { + mapIPCRoutes(this); + } +} diff --git a/src/ipc/scheme_handlers.cc b/src/ipc/scheme_handlers.cc new file mode 100644 index 0000000000..c885aeb96a --- /dev/null +++ b/src/ipc/scheme_handlers.cc @@ -0,0 +1,1623 @@ +#include "../app/app.hh" +#include "scheme_handlers.hh" +#include "ipc.hh" + +using namespace SSC; +using namespace SSC::IPC; + +#if SOCKET_RUNTIME_PLATFORM_APPLE +using Task = id<WKURLSchemeTask>; + +@class SSCWebView; +@interface SSCInternalWKURLSchemeHandler : NSObject<WKURLSchemeHandler> +@property (nonatomic) SSC::IPC::SchemeHandlers* handlers; + +- (void) webView: (SSCWebView*) webview + startURLSchemeTask: (id<WKURLSchemeTask>) task; + +- (void) webView: (SSCWebView*) webview + stopURLSchemeTask: (id<WKURLSchemeTask>) task; +@end + +@implementation SSCInternalWKURLSchemeHandler +{ + Mutex mutex; + std::unordered_map<Task, uint64_t> tasks; +} + +- (void) enqueueTask: (Task) task + withRequestID: (uint64_t) id +{ + Lock lock(mutex); + if (task != nullptr && !tasks.contains(task)) { + tasks.emplace(task, id); + } +} + +- (void) finalizeTask: (Task) task { + Lock lock(mutex); + if (task != nullptr && tasks.contains(task)) { + tasks.erase(task); + } +} + +- (bool) waitingForTask: (Task) task { + Lock lock(mutex); + return task != nullptr && tasks.contains(task); +} + +- (void) webView: (SSCWebView*) webview + stopURLSchemeTask: (Task) task +{ + if (tasks.contains(task)) { + const auto id = tasks[task]; + if (self.handlers->isRequestActive(id)) { + auto request = self.handlers->activeRequests[id]; + request->cancelled = true; + if (request->callbacks.cancel != nullptr) { + request->callbacks.cancel(); + } + } + } + + [self finalizeTask: task]; +} + +- (void) webView: (SSCWebView*) webview + startURLSchemeTask: (Task) task +{ + if (self.handlers == nullptr) { + static auto userConfig = SSC::getUserConfig(); + const auto bundleIdentifier = userConfig.contains("meta_bundle_identifier") + ? userConfig.at("meta_bundle_identifier") + : ""; + + [task didFailWithError: [NSError + errorWithDomain: @(bundleIdentifier.c_str()) + code: 1 + userInfo: @{NSLocalizedDescriptionKey: @("IPC::SchemeHandlers::Response: Request is in an invalid state")} + ]]; + return; + } + + auto request = IPC::SchemeHandlers::Request::Builder(self.handlers, task) + .setMethod(toUpperCase(task.request.HTTPMethod.UTF8String)) + // copies all headers + .setHeaders(task.request.allHTTPHeaderFields) + // copies request body + .setBody(task.request.HTTPBody) + .build(); + + [self enqueueTask: task withRequestID: request->id]; + const auto handled = self.handlers->handleRequest(request, [=](const auto& response) { + [self finalizeTask: task]; + }); + + if (!handled) { + auto response = IPC::SchemeHandlers::Response(request, 404); + response.finish(); + [self finalizeTask: task]; + } +} +@end +#elif SOCKET_RUNTIME_PLATFORM_LINUX +static const auto MAX_URI_SCHEME_REQUEST_BODY_BYTES = 4 * 1024 * 1024; +static void onURISchemeRequest (WebKitURISchemeRequest* schemeRequest, gpointer userData) { + static auto userConfig = SSC::getUserConfig(); + static auto app = App::sharedApplication(); + + if (!app) { + const auto quark = g_quark_from_string(userConfig["meta_bundle_identifier"].c_str()); + const auto error = g_error_new(quark, 1, "IPC::SchemeHandlers::Request: Missing WindowManager in request"); + webkit_uri_scheme_request_finish_error(schemeRequest, error); + return; + } + + auto webview = webkit_uri_scheme_request_get_web_view(schemeRequest); + auto window = app->windowManager.getWindowForWebView(webview); + + if (!window) { + const auto quark = g_quark_from_string(userConfig["meta_bundle_identifier"].c_str()); + const auto error = g_error_new(quark, 1, "IPC::SchemeHandlers::Request: Missing Window in request"); + webkit_uri_scheme_request_finish_error(schemeRequest, error); + return; + } + + auto bridge = &window->bridge; + auto request = IPC::SchemeHandlers::Request::Builder(&bridge->schemeHandlers, schemeRequest) + .setMethod(String(webkit_uri_scheme_request_get_http_method(schemeRequest))) + // copies all request soup headers + .setHeaders(webkit_uri_scheme_request_get_http_headers(schemeRequest)) + // reads and copies request stream body + .setBody(webkit_uri_scheme_request_get_http_body(schemeRequest)) + .build(); + + const auto handled = bridge->schemeHandlers.handleRequest(request, [=](const auto& response) mutable { + }); + + if (!handled) { + auto response = IPC::SchemeHandlers::Response(request, 404); + response.finish(); + } +} +#elif SOCKET_RUNTIME_PLATFORM_ANDROID +extern "C" { + jboolean ANDROID_EXTERNAL(ipc, SchemeHandlers, handleRequest) ( + JNIEnv* env, + jobject self, + jint index, + jobject requestObject + ) { + auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return false; + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + ANDROID_THROW(env, "Invalid window requested"); + return false; + } + + const auto method = Android::StringWrap(env, (jstring) CallClassMethodFromAndroidEnvironment( + env, + Object, + requestObject, + "getMethod", + "()Ljava/lang/String;" + )).str(); + + const auto headers = Android::StringWrap(env, (jstring) CallClassMethodFromAndroidEnvironment( + env, + Object, + requestObject, + "getHeaders", + "()Ljava/lang/String;" + )).str(); + + const auto requestBodyByteArray = (jbyteArray) CallClassMethodFromAndroidEnvironment( + env, + Object, + requestObject, + "getBody", + "()[B" + ); + + const auto requestBodySize = requestBodyByteArray != nullptr + ? env->GetArrayLength(requestBodyByteArray) + : 0; + + const auto bytes = requestBodySize > 0 + ? new char[requestBodySize]{0} + : nullptr; + + if (requestBodyByteArray) { + env->GetByteArrayRegion( + requestBodyByteArray, + 0, + requestBodySize, + (jbyte*) bytes + ); + } + + const auto requestObjectRef = env->NewGlobalRef(requestObject); + const auto request = IPC::SchemeHandlers::Request::Builder( + &window->bridge.schemeHandlers, + requestObjectRef + ) + .setMethod(method) + // copies all request soup headers + .setHeaders(headers) + // reads and copies request stream body + .setBody(requestBodySize, bytes) + .build(); + + const auto handled = window->bridge.schemeHandlers.handleRequest(request, [=](const auto& response) { + if (bytes) { + delete [] bytes; + } + + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + attachment.env->DeleteGlobalRef(requestObjectRef); + }); + + if (!handled) { + env->DeleteGlobalRef(requestObjectRef); + } + + return handled; + } + + jboolean ANDROID_EXTERNAL(ipc, SchemeHandlers, hasHandlerForScheme) ( + JNIEnv* env, + jobject self, + jint index, + jstring schemeString + ) { + auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return false; + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + ANDROID_THROW(env, "Invalid window requested"); + return false; + } + + const auto scheme = Android::StringWrap(env, schemeString).str(); + return window->bridge.schemeHandlers.hasHandlerForScheme(scheme); + } +} +#endif + +namespace SSC::IPC { + class SchemeHandlersInternals { + public: + SchemeHandlers* handlers = nullptr; + #if SOCKET_RUNTIME_PLATFORM_APPLE + SSCInternalWKURLSchemeHandler* schemeHandler = nullptr; + #endif + + SchemeHandlersInternals (SchemeHandlers* handlers) { + this->handlers = handlers; + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->schemeHandler = [SSCInternalWKURLSchemeHandler new]; + this->schemeHandler.handlers = handlers; + #endif + } + + ~SchemeHandlersInternals () { + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->schemeHandler != nullptr) { + this->schemeHandler.handlers = nullptr; + #if !__has_feature(objc_arc) + [this->schemeHandler release]; + #endif + this->schemeHandler = nullptr; + } + #endif + } + }; +} + +namespace SSC::IPC { +#if SOCKET_RUNTIME_PLATFORM_LINUX + static Set<String> globallyRegisteredSchemesForLinux; +#endif + + static const std::map<int, String> STATUS_CODES = { + {100, "Continue"}, + {101, "Switching Protocols"}, + {102, "Processing"}, + {103, "Early Hints"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {207, "Multi-Status"}, + {208, "Already Reported"}, + {226, "IM Used"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {305, "Use Proxy"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Payload Too Large"}, + {414, "URI Too Long"}, + {415, "Unsupported Media Type"}, + {416, "Range Not Satisfiable"}, + {417, "Expectation Failed"}, + {418, "I'm a Teapot"}, + {421, "Misdirected Request"}, + {422, "Unprocessable Entity"}, + {423, "Locked"}, + {424, "Failed Dependency"}, + {425, "Too Early"}, + {426, "Upgrade Required"}, + {428, "Precondition Required"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {451, "Unavailable For Legal Reasons"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + {506, "Variant Also Negotiates"}, + {507, "Insufficient Storage"}, + {508, "Loop Detected"}, + {509, "Bandwidth Limit Exceeded"}, + {510, "Not Extended"}, + {511, "Network Authentication Required"} + }; + + SchemeHandlers::SchemeHandlers (Bridge* bridge) + : bridge(bridge) + { + this->internals = new SchemeHandlersInternals(this); + } + + SchemeHandlers::~SchemeHandlers () { + for (auto& entry : this->activeRequests) { + entry.second->handlers = nullptr; + } + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->configuration.webview = nullptr; + #endif + + if (this->internals != nullptr) { + delete this->internals; + this->internals = nullptr; + } + } + + void SchemeHandlers::init () {} + + void SchemeHandlers::configure (const Configuration& configuration) { + static const auto devHost = SSC::getDevHost(); + this->configuration = configuration; + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (SSC::isDebugEnabled() && devHost.starts_with("http:")) { + [configuration.webview.processPool + performSelector: @selector(_registerURLSchemeAsSecure:) + withObject: @"http" + ]; + } + #endif + } + + bool SchemeHandlers::hasHandlerForScheme (const String& scheme) { + Lock lock(this->mutex); + return this->handlers.contains(scheme); + } + + SchemeHandlers::Handler SchemeHandlers::getHandlerForScheme (const String& scheme) { + Lock lock(this->mutex); + return this->handlers.contains(scheme) + ? this->handlers.at(scheme) + : SchemeHandlers::Handler {}; + } + + bool SchemeHandlers::registerSchemeHandler (const String& scheme, const Handler& handler) { + if (scheme.size() == 0 || this->hasHandlerForScheme(scheme)) { + return false; + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (this->configuration.webview == nullptr) { + return false; + } + + [this->configuration.webview + setURLSchemeHandler: this->internals->schemeHandler + forURLScheme: @(scheme.c_str()) + ]; + + #elif SOCKET_RUNTIME_PLATFORM_LINUX + // schemes are registered for the globally shared defaut context, + // despite the `SchemeHandlers` instance bound to a `Router` + // we'll select the correct `Router` in the callback which will give + // access to the `SchemeHandlers` that should handle the + // request and provide a response + if ( + std::find( + globallyRegisteredSchemesForLinux.begin(), + globallyRegisteredSchemesForLinux.end(), + scheme + ) == globallyRegisteredSchemesForLinux.end() + ) { + globallyRegisteredSchemesForLinux.insert(scheme); + auto context = webkit_web_context_get_default(); + auto security = webkit_web_context_get_security_manager(context); + webkit_web_context_register_uri_scheme( + context, + scheme.c_str(), + onURISchemeRequest, + nullptr, + nullptr + ); + } + #endif + + this->handlers.insert_or_assign(scheme, handler); + return true; + } + + bool SchemeHandlers::handleRequest ( + SharedPointer<Request> request, + const HandlerCallback callback + ) { + // request was not finalized, likely not from a `Request::Builder` + if (request == nullptr || !request->finalized) { + return false; + } + + if (this->isRequestActive(request->id)) { + return false; + } + + // respond with a 404 if somehow we are trying to respond to a + // request scheme we do not know about, we do not need to call + // `request.finalize()` as we'll just respond to the request right away + if (!this->hasHandlerForScheme(request->scheme)) { + auto response = IPC::SchemeHandlers::Response(request, 404); + // make sure the response was finished first + response.finish(); + + // notify finished, even for 404 + if (response.request->callbacks.finish != nullptr) { + response.request->callbacks.finish(); + } + + if (callback != nullptr) { + callback(response); + } + + return true; + } + + const auto handler = this->getHandlerForScheme(request->scheme); + const auto id = request->id; + + // fail if there is somehow not a handler for this request scheme + if (handler == nullptr) { + return false; + } + + do { + Lock lock(this->mutex); + this->activeRequests.emplace(request->id, request); + } while (0); + + if (request->error != nullptr) { + Lock lock(this->mutex); + auto response = IPC::SchemeHandlers::Response(request, 500); + response.fail(request->error); + this->activeRequests.erase(id); + return true; + } + + auto span = request->tracer.span("handler"); + + this->bridge->dispatch([=, this] () mutable { + if (request != nullptr && request->isActive() && !request->isCancelled()) { + handler(request, this->bridge, &request->callbacks, [=, this](auto& response) mutable { + // make sure the response was finished before + // calling the `callback` function below + response.finish(); + + // notify finished + if (response.request->callbacks.finish != nullptr) { + response.request->callbacks.finish(); + } + + if (callback != nullptr) { + callback(response); + } + + span->end(); + + do { + Lock lock(this->mutex); + this->activeRequests.erase(id); + } while (0); + }); + } + }); + + return true; + } + + bool SchemeHandlers::isRequestActive (uint64_t id) { + Lock lock(this->mutex); + return this->activeRequests.contains(id); + } + + bool SchemeHandlers::isRequestCancelled (uint64_t id) { + Lock lock(this->mutex); + return ( + id > 0 && + this->activeRequests.contains(id) && + this->activeRequests.at(id) != nullptr && + this->activeRequests.at(id)->cancelled + ); + } + + SchemeHandlers::Request::Builder::Builder ( + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + SchemeHandlers* handlers, + PlatformRequest platformRequest, + ICoreWebView2Environment* env + #else + SchemeHandlers* handlers, + PlatformRequest platformRequest + #endif + ) { + const auto userConfig = handlers->bridge->userConfig; + const auto bundleIdentifier = userConfig.contains("meta_bundle_identifier") + ? userConfig.at("meta_bundle_identifier") + : ""; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + this->absoluteURL = platformRequest.request.URL.absoluteString.UTF8String; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + this->absoluteURL = webkit_uri_scheme_request_get_uri(platformRequest); + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + LPWSTR requestURI; + platformRequest->get_Uri(&requestURI); + this->absoluteURL = convertWStringToString(requestURI); + if ( + this->absoluteURL.starts_with("socket://") && + !this->absoluteURL.starts_with("socket://" + bundleIdentifier) + ) { + this->absoluteURL = String("socket://") + this->absoluteURL.substr(8); + if (this->absoluteURL.ends_with("/")) { + this->absoluteURL = this->absoluteURL.substr(0, this->absoluteURL.size() - 1); + } + } + CoTaskMemFree(requestURI); + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + auto app = App::sharedApplication(); + auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + this->absoluteURL = Android::StringWrap(attachment.env, (jstring) CallClassMethodFromAndroidEnvironment( + attachment.env, + Object, + platformRequest, + "getUrl", + "()Ljava/lang/String;" + )).str(); + #endif + + const auto url = URL::Components::parse(this->absoluteURL); + + this->request = std::make_shared<Request>( + handlers, + platformRequest, + Request::Options { + .scheme = url.scheme + } + ); + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + this->request->env = env; + #endif + + this->request->client = handlers->bridge->client; + + // build request URL components from parsed URL components + this->request->originalURL = this->absoluteURL; + this->request->hostname = url.authority; + this->request->pathname = url.pathname; + this->request->query = url.query; + this->request->fragment = url.fragment; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setScheme (const String& scheme) { + this->request->scheme = scheme; + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setMethod (const String& method) { + this->request->method = method; + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setHostname (const String& hostname) { + this->request->hostname = hostname; + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setPathname (const String& pathname) { + this->request->pathname = pathname; + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setQuery (const String& query) { + this->request->query = query; + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setFragment (const String& fragment) { + this->request->fragment = fragment; + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setHeader ( + const String& name, + const Headers::Value& value + ) { + this->request->headers.set(name, value.string); + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setHeaders (const Headers& headers) { + for (const auto& entry : headers) { + this->request->headers.set(entry); + } + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setHeaders (const Map& headers) { + for (const auto& entry : headers) { + this->request->headers.set(entry.first, entry.second); + } + return *this; + } + +#if SOCKET_RUNTIME_PLATFORM_APPLE + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setHeaders ( + const NSDictionary<NSString*, NSString*>* headers + ) { + if (headers == nullptr) { + return *this; + } + + for (NSString* key in headers) { + const auto value = [headers objectForKey: key]; + if (value != nullptr) { + this->request->headers.set(key.UTF8String, value.UTF8String); + } + } + + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setBody (const NSData* data) { + if (data != nullptr && data.length > 0 && data.bytes != nullptr) { + return this->setBody(data.length, reinterpret_cast<const char*>(data.bytes)); + } + return *this; + } +#elif SOCKET_RUNTIME_PLATFORM_LINUX + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setHeaders ( + const SoupMessageHeaders* headers + ) { + if (headers) { + soup_message_headers_foreach( + const_cast<SoupMessageHeaders*>(headers), + [](const char* name, const char* value, gpointer userData) { + auto request = reinterpret_cast<SchemeHandlers::Request*>(userData); + request->headers.set(name, value); + }, + this->request.get() + ); + } + + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setBody ( + GInputStream* stream + ) { + if (stream == nullptr) { + return *this; + } + + if ( + this->request->method == "POST" || + this->request->method == "PUT" || + this->request->method == "PATCH" + ) { + GError* error = nullptr; + this->request->body.bytes = std::make_shared<char[]>(MAX_URI_SCHEME_REQUEST_BODY_BYTES); + const auto success = g_input_stream_read_all( + stream, + this->request->body.bytes.get(), + MAX_URI_SCHEME_REQUEST_BODY_BYTES, + &this->request->body.size, + nullptr, + &this->error + ); + } + return *this; + } +#endif + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setBody (const Body& body) { + if ( + this->request->method == "POST" || + this->request->method == "PUT" || + this->request->method == "PATCH" + ) { + this->request->body = body; + } + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setBody (size_t size, const char* bytes) { + if (this->request->method == "POST" || this->request->method == "PUT" || this->request->method == "PATCH") { + if (size > 0 && bytes != nullptr) { + this->request->body.size = size; + this->request->body.bytes = std::make_shared<char[]>(size); + memcpy( + this->request->body.bytes.get(), + bytes, + this->request->body.size + ); + } + } + return *this; + } + + SchemeHandlers::Request::Builder& SchemeHandlers::Request::Builder::setCallbacks (const RequestCallbacks& callbacks) { + this->request->callbacks = callbacks; + return *this; + } + + SharedPointer<SchemeHandlers::Request> SchemeHandlers::Request::Builder::build () { + this->request->error = this->error; + this->request->finalize(); + return this->request; + } + + SchemeHandlers::Request::Request ( + SchemeHandlers* handlers, + PlatformRequest platformRequest, + const Options& options + ) + : handlers(handlers), + bridge(handlers->bridge), + scheme(options.scheme), + method(options.method), + hostname(options.hostname), + pathname(options.pathname), + query(options.query), + fragment(options.fragment), + headers(options.headers), + tracer("IPC::SchemeHandlers::Request") + { + this->platformRequest = platformRequest; + } + + SchemeHandlers::Request::~Request () { + this->platformRequest = nullptr; + } + + static void copyRequest ( + SchemeHandlers::Request* destination, + const SchemeHandlers::Request& source + ) noexcept { + destination->id = source.id; + destination->scheme = source.scheme; + destination->method = source.method; + destination->hostname = source.hostname; + destination->pathname = source.pathname; + destination->query = source.query; + destination->fragment = source.fragment; + + destination->headers = source.headers; + destination->body = source.body; + + destination->client = source.client; + destination->callbacks = source.callbacks; + destination->originalURL = source.originalURL; + + if (destination->finalized) { + destination->origin = source.origin; + destination->params = source.params; + } + + destination->finalized = source.finalized.load(); + destination->cancelled = source.cancelled.load(); + + destination->bridge = source.bridge; + destination->tracer = source.tracer; + destination->handlers = source.handlers; + destination->platformRequest = source.platformRequest; + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + destination->env = source.env; + #endif + } + + SchemeHandlers::Request::Request (const Request& request) noexcept + : tracer("IPC::SchemeHandlers::Request") + { + copyRequest(this, request); + } + + SchemeHandlers::Request::Request (Request&& request) noexcept + : tracer("SchemeHandlers::Request") + { + copyRequest(this, request); + } + + SchemeHandlers::Request& SchemeHandlers::Request::operator= (const Request& request) noexcept { + copyRequest(this, request); + return *this; + } + + SchemeHandlers::Request& SchemeHandlers::Request::operator= (Request&& request) noexcept { + copyRequest(this, request); + return *this; + } + + bool SchemeHandlers::Request::hasHeader (const String& name) const { + if (this->headers.has(name)) { + return true; + } + + return false; + } + + const String SchemeHandlers::Request::getHeader (const String& name) const { + return this->headers.get(name).value.string; + } + + const String SchemeHandlers::Request::url () const { + return this->str(); + } + + const String SchemeHandlers::Request::str () const { + if (this->hostname.size() > 0) { + return trim( + this->scheme + + "://" + + this->hostname + + this->pathname + + (this->query.size() ? "?" + this->query : "") + + (this->fragment.size() ? "#" + this->fragment : "") + ); + } + + return trim( + this->scheme + + ":" + + this->pathname.substr(1) + + (this->query.size() ? "?" + this->query : "") + + (this->fragment.size() ? "#" + this->fragment : "") + ); + } + + bool SchemeHandlers::Request::finalize () { + if (this->finalized) { + return false; + } + + if (this->hasHeader("runtime-client-id")) { + try { + this->client.id = std::stoull(this->getHeader("runtime-client-id")); + } catch (...) {} + } + + for (const auto& entry : split(this->query, '&')) { + const auto parts = split(entry, '='); + if (parts.size() == 2) { + const auto key = decodeURIComponent(trim(parts[0])); + const auto value = decodeURIComponent(trim(parts[1])); + this->params.insert_or_assign(key, value); + } + } + + this->finalized = true; + this->origin = this->scheme + "://" + this->hostname; + return true; + } + + bool SchemeHandlers::Request::isActive () const { + auto app = App::sharedApplication(); + auto window = app->windowManager.getWindowForBridge(this->bridge); + + // only a scheme handler owned by this bridge and attached to a + // window should be considered "active" + // scheme handlers SHOULD only work windows that have a navigator + if (window != nullptr && this->handlers != nullptr) { + return this->handlers->isRequestActive(this->id); + } + + return false; + } + + bool SchemeHandlers::Request::isCancelled () const { + auto app = App::sharedApplication(); + auto window = app->windowManager.getWindowForBridge(this->bridge); + + if (window != nullptr && this->handlers != nullptr) { + return this->handlers->isRequestCancelled(this->id); + } + + return false; + } + + JSON::Object SchemeHandlers::Request::json () const { + return JSON::Object::Entries { + {"scheme", this->scheme}, + {"method", this->method}, + {"hostname", this->hostname}, + {"pathname", this->pathname}, + {"query", this->query}, + {"fragment", this->fragment}, + {"headers", this->headers.json()}, + {"client", JSON::Object::Entries { + {"id", this->client.id} + }} + }; + } + + SchemeHandlers::Response::Response ( + SharedPointer<Request> request, + int statusCode, + const Headers headers + ) : request(request), + handlers(request->handlers), + client(request->client), + id(request->id), + tracer("IPC::SchemeHandlers::Response") + { + const auto defaultHeaders = split( + this->request->bridge->userConfig.contains("webview_headers") + ? this->request->bridge->userConfig.at("webview_headers") + : "", + '\n' + ); + + if (SSC::isDebugEnabled()) { + this->setHeader("cache-control", "no-cache"); + } + + this->setHeader("connection", "keep-alive"); + this->setHeader("access-control-allow-origin", "*"); + this->setHeader("access-control-allow-headers", "*"); + this->setHeader("access-control-allow-methods", "*"); + this->setHeader("access-control-allow-credentials", "true"); + + if (request->method == "OPTIONS") { + this->setHeader("allow", "GET, POST, PATCH, PUT, DELETE, HEAD"); + } + + for (const auto& entry : defaultHeaders) { + const auto parts = split(trim(entry), ':'); + this->setHeader(parts[0], parts[1]); + } + } + + static void copyResponse ( + SchemeHandlers::Response* destination, + const SchemeHandlers::Response& source + ) noexcept { + destination->id = source.id; + destination->client = source.client; + destination->headers = source.headers; + destination->finished = source.finished.load(); + destination->handlers = source.handlers; + destination->statusCode = source.statusCode; + destination->buffers = source.buffers; + destination->platformResponse = source.platformResponse; + } + + SchemeHandlers::Response::Response (const Response& response) noexcept + : request(response.request), + tracer("IPC::SchemeHandlers::Response") + { + copyResponse(this, response); + } + + SchemeHandlers::Response::Response (Response&& response) noexcept + : request(response.request), + tracer("IPC::SchemeHandlers::Response") + { + copyResponse(this, response); + } + + SchemeHandlers::Response::~Response () {} + + SchemeHandlers::Response& SchemeHandlers::Response::operator= (const Response& response) noexcept { + copyResponse(this, response); + return *this; + } + + SchemeHandlers::Response& SchemeHandlers::Response::operator= (Response&& response) noexcept { + copyResponse(this, response); + return *this; + } + + bool SchemeHandlers::Response::writeHead (int statusCode, const Headers headers) { + // fail if already finished + if (this->finished) { + debug("IPC::SchemeHandlers::Response: Failed to write head. Already finished"); + return false; + } + + if ( + !this->handlers->isRequestActive(this->id) || + this->handlers->isRequestCancelled(this->id) + ) { + return false; + } + + // fail if head of response is already created + if (this->platformResponse != nullptr) { + debug("IPC::SchemeHandlers::Response: Failed to write head as it was already written"); + return false; + } + + if (this->request->platformRequest == nullptr) { + debug("IPC::SchemeHandlers::Response: Failed to write head. Request is in an invalid state"); + return false; + } + + if (statusCode >= 100 && statusCode < 600) { + this->statusCode = statusCode; + } + + for (const auto& header : headers) { + this->setHeader(header); + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE || SOCKET_RUNTIME_PLATFORM_LINUX || SOCKET_RUNTIME_PLATFORM_ANDROID + // webkit status codes cannot be in the range of 300 >= statusCode < 400 + if (this->statusCode >= 300 && this->statusCode < 400) { + this->statusCode = 200; + } + #endif + + #if SOCKET_RUNTIME_PLATFORM_APPLE + auto headerFields= [NSMutableDictionary dictionary]; + for (const auto& entry : this->headers) { + headerFields[@(entry.name.c_str())] = @(entry.value.c_str()); + } + + auto platformRequest = this->request->platformRequest; + if (platformRequest != nullptr && platformRequest.request != nullptr) { + const auto url = platformRequest.request.URL; + if (url != nullptr) { + @try { + this->platformResponse = [[NSHTTPURLResponse alloc] + initWithURL: platformRequest.request.URL + statusCode: this->statusCode + HTTPVersion: @"HTTP/1.1" + headerFields: headerFields + ]; + } @catch (::id) { + return false; + } + + [platformRequest didReceiveResponse: this->platformResponse]; + return true; + } + } + #elif SOCKET_RUNTIME_PLATFORM_LINUX + const auto contentLength = this->getHeader("content-length"); + gint64 size = -1; + + if (contentLength.size() > 0) { + try { + size = std::stol(contentLength); + } catch (...) {} + } + + if (this->platformResponseStream == nullptr) { + this->platformResponseStream = g_memory_input_stream_new(); + } + + this->platformResponse = webkit_uri_scheme_response_new(this->platformResponseStream, size); + + auto requestHeaders = soup_message_headers_new(SOUP_MESSAGE_HEADERS_RESPONSE); + for (const auto& entry : this->headers) { + soup_message_headers_append(requestHeaders, entry.name.c_str(), entry.value.c_str()); + } + + webkit_uri_scheme_response_set_http_headers(this->platformResponse, requestHeaders); + + if (this->hasHeader("content-type")) { + const auto contentType = this->getHeader("content-type"); + if (contentType.size() > 0) { + webkit_uri_scheme_response_set_content_type(this->platformResponse, contentType.c_str()); + } + } + + const auto statusText = String( + STATUS_CODES.contains(this->statusCode) + ? STATUS_CODES.at(this->statusCode) + : "" + ); + + webkit_uri_scheme_response_set_status( + this->platformResponse, + this->statusCode, + statusText.size() > 0 ? statusText.c_str() : nullptr + ); + + return true; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + const auto statusText = String( + STATUS_CODES.contains(this->statusCode) + ? STATUS_CODES.at(this->statusCode) + : "" + ); + this->platformResponseStream = SHCreateMemStream(nullptr, 0); + const auto result = this->request->env->CreateWebResourceResponse( + this->platformResponseStream, + this->statusCode, + convertStringToWString(statusText).c_str(), + convertStringToWString(this->headers.str()).c_str(), + &this->platformResponse + ); + + return result == S_OK; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + + this->platformResponse = attachment.env->NewGlobalRef( + CallClassMethodFromAndroidEnvironment( + attachment.env, + Object, + this->request->platformRequest, + "getResponse", + "()Lsocket/runtime/ipc/SchemeHandlers$Response;" + ) + ); + + for (const auto& header : this->headers) { + const auto name = attachment.env->NewStringUTF(toHeaderCase(header.name).c_str()); + const auto value = attachment.env->NewStringUTF(header.value.c_str()); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->platformResponse, + "setHeader", + "(Ljava/lang/String;Ljava/lang/String;)V", + name, + value + ); + + attachment.env->DeleteLocalRef(name); + attachment.env->DeleteLocalRef(value); + } + + const auto statusText = STATUS_CODES.contains(this->statusCode) + ? attachment.env->NewStringUTF(STATUS_CODES.at(this->statusCode).c_str()) + : attachment.env->NewStringUTF(""); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->platformResponse, + "setStatus", + "(ILjava/lang/String;)V", + this->statusCode, + statusText + ); + + attachment.env->DeleteLocalRef(statusText); + + return true; + #endif + return false; + } + + bool SchemeHandlers::Response::write ( + size_t size, + SharedPointer<char[]> bytes + ) { + if ( + !this->handlers->isRequestActive(this->id) || + this->handlers->isRequestCancelled(this->id) + ) { + debug("IPC::SchemeHandlers::Response: Write attemped for request that is no longer active or cancelled"); + return false; + } + + if (!this->hasHeader("content-type")) { + this->setHeader("content-type", "application/octet-stream"); + } + + if (!this->platformResponse) { + // set 'content-length' header if response was not created + this->setHeader("content-length", size); + if (!this->writeHead()) { + debug( + "IPC::SchemeHandlers::Response: Failed to write head for %s", + this->request->str().c_str() + ); + return false; + } + } + + if (size > 0 && bytes != nullptr) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + const auto data = [NSData dataWithBytes: bytes.get() length: size]; + @try { + [this->request->platformRequest didReceiveData: data]; + } @catch (::id) { + return false; + } + return true; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + g_memory_input_stream_add_data( + reinterpret_cast<GMemoryInputStream*>(this->platformResponseStream), + reinterpret_cast<const void*>(bytes.get()), + (gssize) size, + nullptr + ); + this->request->bridge->core->retainSharedPointerBuffer(std::move(bytes), 256); + return true; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + return S_OK == this->platformResponseStream->Write( + reinterpret_cast<const void*>(bytes.get()), + (ULONG) size, + nullptr + ); + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + const auto byteArray = attachment.env->NewByteArray(size); + + attachment.env->SetByteArrayRegion( + byteArray, + 0, + size, + (jbyte *) bytes.get() + ); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->platformResponse, + "write", + "([B)V", + byteArray + ); + + if (byteArray != nullptr) { + attachment.env->DeleteLocalRef(byteArray); + } + + return true; + #endif + } + + return false; + } + + bool SchemeHandlers::Response::write (const String& source) { + const auto size = source.size(); + + if (size == 0) { + return false; + } + + auto bytes = std::make_shared<char[]>(size); + memcpy(bytes.get(), source.c_str(), size); + return this->write(size, bytes); + } + + bool SchemeHandlers::Response::write (size_t size, const char* bytes) { + return size > 0 && bytes != nullptr && this->write(String(bytes, size)); + } + + bool SchemeHandlers::Response::write (const JSON::Any& json) { + this->setHeader("content-type", "application/json"); + return this->write(json.str()); + } + + bool SchemeHandlers::Response::write (const FileResource& resource) { + auto responseResource = FileResource(resource); + auto app = App::sharedApplication(); + + const auto contentLength = responseResource.size(); + const auto contentType = responseResource.mimeType(); + + if (contentType.size() > 0 && !this->hasHeader("content-type")) { + this->setHeader("content-type", contentType); + } + + if (contentLength > 0) { + this->setHeader("content-length", contentLength); + } + + if (contentLength > 0) { + this->writeHead(); + return this->write(contentLength, responseResource.read()); + } + + return false; + } + + bool SchemeHandlers::Response::write ( + const FileResource::ReadStream::Buffer& buffer + ) { + return this->write(buffer.size, buffer.bytes); + } + + bool SchemeHandlers::Response::send (const String& source) { + return this->write(source) && this->finish(); + } + + bool SchemeHandlers::Response::send (const JSON::Any& json) { + return this->write(json) && this->finish(); + } + + bool SchemeHandlers::Response::send (const FileResource& resource) { + return this->write(resource) && this->finish(); + } + + bool SchemeHandlers::Response::finish () { + // fail if already finished + if (this->finished) { + return false; + } + + if ( + !this->handlers->isRequestActive(this->id) || + this->handlers->isRequestCancelled(this->id) + ) { + return false; + } + + if (!this->platformResponse) { + if (!this->writeHead() || !this->platformResponse) { + return false; + } + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + @try { + [this->request->platformRequest didFinish]; + } @catch (::id) {} + #if !__has_feature(objc_arc) + [this->platformResponse release]; + #endif + this->platformResponse = nullptr; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + if (this->request->platformRequest) { + auto platformRequest = this->request->platformRequest; + auto platformResponse = this->platformResponse; + auto platformResponseStream = this->platformResponseStream; + + this->platformResponseStream = nullptr; + this->platformResponse = nullptr; + + webkit_uri_scheme_request_finish_with_response( + platformRequest, + platformResponse + ); + + g_object_unref(platformResponseStream); + } + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + this->platformResponseStream = nullptr; + // TODO(@jwerle): move more `WebResourceRequested` logic to here + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + if (this->platformResponse != nullptr) { + auto app = App::sharedApplication(); + auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + this->platformResponse, + "finish", + "()V" + ); + + this->platformResponse = nullptr; + attachment.env->DeleteGlobalRef(this->platformResponse); + } + #else + this->platformResponse = nullptr; + #endif + + this->finished = true; + return true; + } + + void SchemeHandlers::Response::setHeader (const String& name, const Headers::Value& value) { + const auto bridge = this->request->handlers->bridge; + if (toLowerCase(name) == "referer") { + if (bridge->navigator.location.workers.contains(value.string)) { + const auto workerLocation = bridge->navigator.location.workers[value.string]; + this->headers[name] = workerLocation; + return; + } else if (bridge->navigator.serviceWorker.bridge->navigator.location.workers.contains(value.string)) { + const auto workerLocation = bridge->navigator.serviceWorker.bridge->navigator.location.workers[value.string]; + this->headers[name] = workerLocation; + return; + } + } + + this->headers[name] = value.string; + } + + void SchemeHandlers::Response::setHeader (const Headers::Header& header) { + this->setHeader(header.name, header.value.string); + } + + void SchemeHandlers::Response::setHeader (const String& name, size_t value) { + this->setHeader(name, std::to_string(value)); + } + + void SchemeHandlers::Response::setHeader (const String& name, int64_t value) { + this->setHeader(name, std::to_string(value)); + } + +#if !SOCKET_RUNTIME_PLATFORM_LINUX && !SOCKET_RUNTIME_PLATFORM_ANDROID && !SOCKET_RUNTIME_PLATFORM_WINDOWS + void SchemeHandlers::Response::setHeader (const String& name, uint64_t value) { + this->setHeader(name, std::to_string(value)); + } +#endif + + void SchemeHandlers::Response::setHeaders (const Headers& headers) { + for (const auto& header : headers) { + this->setHeader(header); + } + } + + const String SchemeHandlers::Response::getHeader (const String& name) const { + return this->headers.get(name).value.string; + } + + bool SchemeHandlers::Response::hasHeader (const String& name) const { + return this->headers.has(name); + } + + bool SchemeHandlers::Response::fail (const Error* error) { + #if SOCKET_RUNTIME_PLATFORM_APPLE + if (error.localizedDescription != nullptr) { + return this->fail(error.localizedDescription.UTF8String); + } else if (error.localizedFailureReason != nullptr) { + return this->fail(error.localizedFailureReason.UTF8String); + } + #elif SOCKET_RUNTIME_PLATFORM_LINUX + if (error != nullptr && error->message != nullptr) { + return this->fail(error->message); + } + #endif + + return this->fail("Request failed for an unknown reason"); + } + + bool SchemeHandlers::Response::fail (const String& reason) { + const auto bundleIdentifier = this->request->bridge->userConfig.contains("meta_bundle_identifier") + ? this->request->bridge->userConfig.at("meta_bundle_identifier") + : ""; + + if ( + this->finished || + !this->handlers->isRequestActive(this->id) || + this->handlers->isRequestCancelled(this->id) + ) { + return false; + } + + #if SOCKET_RUNTIME_PLATFORM_APPLE + const auto error = [NSError + errorWithDomain: @(bundleIdentifier.c_str()) + code: 1 + userInfo: @{NSLocalizedDescriptionKey: @(reason.c_str())} + ]; + + @try { + [this->request->platformRequest didFailWithError: error]; + } @catch (::id e) { + // ignore possible 'NSInternalInconsistencyException' + return false; + } + + // notify fail callback + if (this->request->callbacks.fail != nullptr) { + this->request->callbacks.fail(error); + } + #elif SOCKET_RUNTIME_PLATFORM_LINUX + const auto quark = g_quark_from_string(bundleIdentifier.c_str()); + if (!quark) { + return false; + } + + const auto error = g_error_new(quark, 1, "%s", reason.c_str()); + + if (error == nullptr) { + return false; + } + + if (this->request && WEBKIT_IS_URI_SCHEME_REQUEST(this->request->platformRequest)) { + webkit_uri_scheme_request_finish_error(this->request->platformRequest, error); + } else { + return false; + } + + // notify fail callback + if (this->request->callbacks.fail != nullptr) { + this->request->callbacks.fail(error); + } + #else + // XXX(@jwerle): there doesn't appear to be a way to notify a failure for all platforms + this->finished = true; + return false; + #endif + + this->finished = true; + return true; + } + + bool SchemeHandlers::Response::redirect (const String& location, int statusCode) { + static constexpr auto redirectSourceTemplate = R"S( + <meta http-equiv="refresh" content="0; url='{{url}}'" /> + )S"; + + // if head was already written, then we cannot perform a redirect + if (this->platformResponse) { + return false; + } + + if (location.starts_with("/")) { + this->setHeader("location", this->request->origin + location); + } else if (location.starts_with(".")) { + this->setHeader("location", this->request->origin + location.substr(1)); + } else { + this->setHeader("location", location); + } + + if (!this->writeHead(statusCode)) { + return false; + } + + if (this->request->method != "HEAD" && this->request->method != "OPTIONS") { + const auto content = tmpl( + redirectSourceTemplate, + {{"url", location}} + ); + + if (!this->write(content)) { + return false; + } + } + + return this->finish(); + } + + const String SchemeHandlers::Response::Event::str () const noexcept { + if (this->name.size() > 0 && this->data.size() > 0) { + return ( + String("event: ") + this->name + "\n" + + String("data: ") + this->data + "\n" + "\n" + ); + } + + if (this->name.size() > 0) { + return String("event: ") + this->name + "\n\n"; + } + + if (this->data.size() > 0) { + return String("data: ") + this->data + "\n\n"; + } + + return ""; + } + + size_t SchemeHandlers::Response::Event::count () const noexcept { + if (this->name.size() > 0 && this->data.size() > 0) { + return 2; + } else if (this->name.size() > 0 || this->data.size() > 0) { + return 1; + } else { + return 0; + } + } +} diff --git a/src/ipc/scheme_handlers.hh b/src/ipc/scheme_handlers.hh new file mode 100644 index 0000000000..f1de33e67a --- /dev/null +++ b/src/ipc/scheme_handlers.hh @@ -0,0 +1,289 @@ +#ifndef SOCKET_RUNTIME_IPC_SCHEME_HANDLERS_H +#define SOCKET_RUNTIME_IPC_SCHEME_HANDLERS_H + +#include "../core/core.hh" +#include "../core/trace.hh" +#include "../core/webview.hh" + +#include "client.hh" + +namespace SSC::IPC { + class Bridge; + class SchemeHandlers; +} + +namespace SSC::IPC { + /** + * An opaque containere for platform internals + */ + class SchemeHandlersInternals; + + /** + * A container for registering scheme handlers attached to an `IPC::Bridge` + * that can handle WebView requests for runtime and custom user defined + * protocol schemes. + */ + class SchemeHandlers { + private: + SchemeHandlersInternals* internals = nullptr; + + public: + #if SOCKET_RUNTIME_PLATFORM_APPLE + using Error = NSError; + #elif SOCKET_RUNTIME_PLATFORM_LINUX + using Error = GError; + #else + using Error = char*; + #endif + + struct Body { + size_t size = 0; + SharedPointer<char[]> bytes = nullptr; + }; + + struct RequestCallbacks { + Function<void()> cancel = nullptr; + Function<void()> finish = nullptr; + Function<void(Error*)> fail = nullptr; + }; + + #if SOCKET_RUNTIME_PLATFORM_APPLE + using PlatformRequest = id<WKURLSchemeTask>; + using PlatformResponse = NSHTTPURLResponse*; + #elif SOCKET_RUNTIME_PLATFORM_LINUX && !SOCKET_RUNTIME_DESKTOP_EXTENSION + using PlatformRequest = WebKitURISchemeRequest*; + using PlatformResponse = WebKitURISchemeResponse*; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + using PlatformRequest = ICoreWebView2WebResourceRequest*; + using PlatformResponse = ICoreWebView2WebResourceResponse*; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + using PlatformRequest = jobject; + using PlatformResponse = jobject; + #else + using PlatformRequest = void*; + using PlatformResponse = void*; + #endif + + struct Request { + struct Options { + String scheme = ""; + String method = "GET"; + String hostname = ""; + String pathname = "/"; + String query = ""; + String fragment = ""; + Headers headers; + Function<bool(const Request*)> isCancelled; + }; + + struct Builder { + String absoluteURL; + Error* error = nullptr; + SharedPointer<Request> request = nullptr; + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + Builder ( + SchemeHandlers* handlers, + PlatformRequest platformRequest, + ICoreWebView2Environment* env + ); + #else + Builder ( + SchemeHandlers* handlers, + PlatformRequest platformRequest + ); + #endif + + Builder& setScheme (const String& scheme); + Builder& setMethod (const String& method); + Builder& setHostname (const String& hostname); + Builder& setPathname (const String& pathname); + Builder& setQuery (const String& query); + Builder& setFragment (const String& fragment); + Builder& setHeader (const String& name, const Headers::Value& value); + Builder& setHeaders (const Headers& headers); + Builder& setHeaders (const Map& headers); + + #if SOCKET_RUNTIME_PLATFORM_APPLE + Builder& setHeaders (const NSDictionary<NSString*, NSString*>* headers); + Builder& setBody (const NSData* data); + #elif SOCKET_RUNTIME_PLATFORM_LINUX + Builder& setHeaders (const SoupMessageHeaders* headers); + Builder& setBody (GInputStream* stream); + #endif + + Builder& setBody (const Body& body); + Builder& setBody (size_t size, const char* bytes); + Builder& setCallbacks (const RequestCallbacks& callbacks); + SharedPointer<Request> build (); + }; + + Client::ID id = rand64(); + String scheme = ""; + String method = "GET"; + String hostname = ""; + String pathname = "/"; + String query = ""; + String fragment = ""; + Headers headers; + + String origin = ""; + Map params; + Body body; + Client client; + String originalURL; + RequestCallbacks callbacks; + + Tracer tracer; + + Atomic<bool> finalized = false; + Atomic<bool> cancelled = false; + + Error* error = nullptr; + Bridge* bridge = nullptr; + SchemeHandlers* handlers = nullptr; + PlatformRequest platformRequest; + + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + ICoreWebView2Environment* env = nullptr; + #endif + + Request () = delete; + Request ( + SchemeHandlers* handlers, + PlatformRequest platformRequest, + const Options& options + ); + + Request (const Request&) noexcept; + Request (Request&&) noexcept; + ~Request (); + Request& operator= (const Request&) noexcept; + Request& operator= (Request&&) noexcept; + + const String getHeader (const String& name) const; + bool hasHeader (const String& name) const; + const String str () const; + const String url () const; + bool finalize (); + JSON::Object json () const; + bool isActive () const; + bool isCancelled () const; + }; + + struct Response { + struct Event { + String name = ""; + String data = ""; + const String str () const noexcept; + size_t count () const noexcept; + }; + + SharedPointer<Request> request; + uint64_t id = rand64(); + int statusCode = 200; + Headers headers; + Client client; + + Atomic<bool> finished = false; + + Vector<SharedPointer<char[]>> buffers; + + Tracer tracer; + + SchemeHandlers* handlers = nullptr; + PlatformResponse platformResponse = nullptr; + + #if SOCKET_RUNTIME_PLATFORM_LINUX + GInputStream* platformResponseStream = nullptr; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS + IStream* platformResponseStream = nullptr; + #endif + + Response ( + SharedPointer<Request> request, + int statusCode = 200, + const Headers headers = {} + ); + + Response (const Response&) noexcept; + Response (Response&&) noexcept; + ~Response (); + Response& operator= (const Response&) noexcept; + Response& operator= (Response&&) noexcept; + + bool write (size_t size, SharedPointer<char[]>); + bool write (const String& source); + bool write (const JSON::Any& json); + bool write (const FileResource& resource); + bool write (const FileResource::ReadStream::Buffer& buffer); + bool write (size_t size, const char* bytes); + bool send (const String& source); + bool send (const JSON::Any& json); + bool send (const FileResource& resource); + bool writeHead (int statusCode = 0, const Headers headers = {}); + bool finish (); + void setHeader (const String& name, const Headers::Value& value); + void setHeader (const String& name, size_t value); + void setHeader (const String& name, int64_t value); + #if !SOCKET_RUNTIME_PLATFORM_LINUX && !SOCKET_RUNTIME_PLATFORM_ANDROID && !SOCKET_RUNTIME_PLATFORM_WINDOWS + void setHeader (const String& name, uint64_t value); + #endif + void setHeader (const Headers::Header& header); + void setHeaders (const Headers& headers); + void setHeaders (const Map& headers); + #if SOCKET_RUNTIME_PLATFORM_APPLE + void setHeaders (const NSDictionary<NSString*, NSString*>* headers); + #endif + const String getHeader (const String& name) const; + bool hasHeader (const String& name) const; + bool fail (const String& reason); + bool fail (const Error* error); + bool redirect (const String& location, int statusCode = 302); + }; + + using HandlerCallback = Function<void(Response&)>; + using Handler = Function<void( + const SharedPointer<Request>, + const Bridge*, + RequestCallbacks* callbacks, + HandlerCallback + )>; + + using HandlerMap = std::map<String, Handler>; + using RequestMap = std::map<uint64_t, SharedPointer<Request>>; + + struct Configuration { + #if SOCKET_RUNTIME_PLATFORM_WINDOWS + WebViewSettings webview; + #else + WebViewSettings* webview; + #endif + }; + + Configuration configuration; + HandlerMap handlers; + + Mutex mutex; + Bridge* bridge = nullptr; + RequestMap activeRequests; + + SchemeHandlers (Bridge* bridge); + ~SchemeHandlers (); + SchemeHandlers (const SchemeHandlers&) = delete; + SchemeHandlers (SchemeHandlers&&) = delete; + + SchemeHandlers& operator= (const SchemeHandlers&) = delete; + SchemeHandlers& operator= (SchemeHandlers&&) = delete; + + void init (); + void configure (const Configuration& configuration); + bool hasHandlerForScheme (const String& scheme); + bool registerSchemeHandler (const String& scheme, const Handler& handler); + bool handleRequest (SharedPointer<Request> request, const HandlerCallback calllback = nullptr); + bool isRequestActive (uint64_t id); + bool isRequestCancelled (uint64_t id); + Handler getHandlerForScheme (const String& scheme); + }; +} +#endif diff --git a/src/ipc/scheme_handlers.kt b/src/ipc/scheme_handlers.kt new file mode 100644 index 0000000000..e4362a9d3e --- /dev/null +++ b/src/ipc/scheme_handlers.kt @@ -0,0 +1,190 @@ +// vim: set sw=2: +package socket.runtime.ipc + +import java.io.PipedInputStream +import java.io.PipedOutputStream +import java.lang.Thread +import java.util.concurrent.Semaphore + +import kotlin.concurrent.thread + +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse + +import socket.runtime.app.App +import socket.runtime.app.AppActivity +import socket.runtime.core.console +import socket.runtime.ipc.Message + +open class SchemeHandlers (val bridge: Bridge) { + open class Request (val bridge: Bridge, val request: WebResourceRequest) { + val response = Response(this) + val body: ByteArray? by lazy { + try { + val seq = this.getHeader("runtime-xhr-seq") + ?: this.request.url.getQueryParameter("seq") + + if (seq != null && this.bridge.buffers.contains(seq)) { + val buffer = this.bridge.buffers[seq] + if (request.method == "POST" || request.method == "PUT" || request.method == "PATCH") { + this.bridge.buffers.remove(seq) + } + buffer + } else { + null + } + } catch (_: Exception) { + null + } + } + + fun getScheme (): String { + val url = this.request.url + if ( + (url.scheme == "https" || url.scheme == "http") && + url.host == "__BUNDLE_IDENTIFIER__" + ) { + return "socket" + } + + return this.request.url.scheme ?: "" + } + + fun getMethod (): String { + return this.request.method ?: "" + } + + fun getHostname (): String { + return this.request.url.host ?: "" + } + + fun getPathname (): String { + return this.request.url.path ?: "" + } + + fun getQuery (): String { + return this.request.url.query ?: "" + } + + fun getHeaders (): String { + var headers = "" + for (entry in request.requestHeaders) { + headers += "${entry.key}: ${entry.value}\n" + } + return headers + } + + fun getHeader (name: String): String? { + return request.requestHeaders.get(name) + } + + fun getUrl (): String { + return this.request.url.toString().replace("https:", "socket:") + } + + fun getWebResourceResponse (): WebResourceResponse? { + return this.response.response + } + + fun waitForFinishedResponse () { + this.response.waitForFinish() + } + } + + open class Response (val request: Request) { + val stream = PipedOutputStream() + var mimeType = "application/octet-stream" + val response = WebResourceResponse( + mimeType, + null, + PipedInputStream(this.stream) + ) + + val headers = mutableMapOf<String, String>() + val buffers = mutableListOf<ByteArray>() + val semaphore = Semaphore(0) + + var pendingWrites = 0 + var finished = false + + fun setStatus (statusCode: Int, statusText: String) { + val headers = this.headers + val mimeType = this.mimeType + + this.response.apply { + setStatusCodeAndReasonPhrase(statusCode, statusText) + setResponseHeaders(headers) + setMimeType(mimeType) + } + } + + fun setHeader (name: String, value: String) { + if (name.lowercase() == "content-type") { + this.mimeType = value + this.response.setMimeType(value) + } else if (name.lowercase() != "content-length") { + this.headers.remove(name) + + if (this.response.responseHeaders != null) { + this.response.responseHeaders.remove(name) + } + + this.headers += mapOf(name to value) + this.response.responseHeaders = this.headers + } + } + + fun write (bytes: ByteArray) { + this.buffers += bytes + } + + fun write (string: String) { + this.write(string.toByteArray()) + } + + fun finish () { + if (!this.finished) { + this.finished = true + this.semaphore.release() + } + } + + fun waitForFinish () { + this.semaphore.acquireUninterruptibly() + this.semaphore.release() + if (this.finished) { + val stream = this.stream + val buffers = this.buffers + thread { + for (bytes in buffers) { + stream.write(bytes) + } + + stream.flush() + stream.close() + } + } + } + } + + fun handleRequest (webResourceRequest: WebResourceRequest): WebResourceResponse? { + val request = Request(this.bridge, webResourceRequest) + + if (this.handleRequest(this.bridge.index, request)) { + request.waitForFinishedResponse() + return request.getWebResourceResponse() + } + + return null + } + + fun hasHandlerForScheme (scheme: String): Boolean { + return this.hasHandlerForScheme(this.bridge.index, scheme) + } + + @Throws(Exception::class) + external fun handleRequest (index: Int, request: Request): Boolean + + @Throws(Exception::class) + external fun hasHandlerForScheme (index: Int, scheme: String): Boolean +} diff --git a/src/platform/android.hh b/src/platform/android.hh new file mode 100644 index 0000000000..a56f10b69c --- /dev/null +++ b/src/platform/android.hh @@ -0,0 +1,34 @@ +#ifndef SOCKET_RUNTIME_PLATFORM_ANDROID_H +#define SOCKET_RUNTIME_PLATFORM_ANDROID_H + +#include "android/content_resolver.hh" +#include "android/environment.hh" +#include "android/looper.hh" +#include "android/mime.hh" +#include "android/string_wrap.hh" +#include "android/types.hh" + +/** + * A macro to help define an Android external package class method + * implementation as a JNI binding. + */ +#define ANDROID_EXTERNAL(packageName, className, methodName) \ + JNIEXPORT JNICALL \ + Java_socket_runtime_##packageName##_##className##_##methodName + +/** + * Throw an exception with formatted message in the Android environment. + * @param {JNIEnv*} env + * @param {const char*} message + * @param {...} + */ +#define ANDROID_THROW(env, message, ...) ({ \ + char buffer[BUFSIZ] = {0}; \ + sprintf(buffer, message, ##__VA_ARGS__); \ + env->ThrowNew( \ + GetExceptionClassFromAndroidEnvironment(env), \ + buffer \ + ); \ + (void) 0; \ +}) +#endif diff --git a/src/platform/android/content_resolver.cc b/src/platform/android/content_resolver.cc new file mode 100644 index 0000000000..3306234942 --- /dev/null +++ b/src/platform/android/content_resolver.cc @@ -0,0 +1,374 @@ +#include "../../core/resource.hh" +#include "../../core/url.hh" + +#include "content_resolver.hh" + +namespace SSC::Android { + bool ContentResolver::isDocumentURI (const String& uri) { + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + return CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + platform, + "isDocumentURI", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF(uri.c_str()) + ); + } + + bool ContentResolver::isContentURI (const String& uri) { + const auto url = URL(uri); + return url.scheme == "content" || url.scheme == "android.resource"; + } + + bool ContentResolver::isExternalStorageDocumentURI (const String& uri) { + const auto url = URL(uri); + return url.hostname == "com.android.externalstorage.documents"; + } + + bool ContentResolver::isDownloadsDocumentURI (const String& uri) { + const auto url = URL(uri); + return url.hostname == "com.android.providers.downloads.documents"; + } + + bool ContentResolver::isMediaDocumentURI (const String& uri) { + const auto url = URL(uri); + return url.hostname == "com.android.providers.media.documents"; + } + + bool ContentResolver::isPhotosURI (const String& uri) { + const auto url = URL(uri); + return url.hostname == "com.android.providers.media.documents"; + } + + String ContentResolver::getContentMimeType (const String& uri) { + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + const auto contentMimeTypeString = (jstring) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + platform, + "getContentMimeType", + "(Ljava/lang/String;)Ljava/lang/String;", + attachment.env->NewStringUTF(uri.c_str()) + ); + + return StringWrap(attachment.env, contentMimeTypeString).str(); + } + + String ContentResolver::getPathnameFromURI (const String& uri) { + const auto url = URL(uri); + + if (url.scheme == "file") { + return url.pathname; + } + + if (this->isDocumentURI(uri)) { + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + const auto documentIDString = (jstring) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + platform, + "getDocumentID", + "(Ljava/lang/String;)Ljava/lang/String;", + attachment.env->NewStringUTF(uri.c_str()) + ); + + const auto documentID = StringWrap(attachment.env, documentIDString).str(); + + if (this->isExternalStorageDocumentURI(uri)) { + const auto externalStorage = FileResource::getExternalAndroidStorageDirectory(); + const auto parts = split(documentID, ":"); + const auto type = parts[0]; + + if (type == "primary" && parts.size() > 1) { + return externalStorage.string() + "/" + parts[1]; + } + } + + if (this->isDownloadsDocumentURI(uri)) { + const auto contentURIString = (jstring) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + platform, + "getContentURI", + "(Ljava/lang/String;J)Ljava/lang/String;", + attachment.env->NewStringUTF("content://downloads/public_downloads"), + std::stol(documentID) + ); + + const auto contentURI = StringWrap(attachment.env, contentURIString).str(); + + return this->getPathnameFromContentURIDataColumn(contentURI); + } + + if (this->isMediaDocumentURI(uri)) { + const auto parts = split(documentID, ":"); + const auto type = parts[0]; + + if (parts.size() > 1) { + const auto id = parts[1]; + const auto contentURI = this->getExternalContentURIForType(type); + + if (contentURI.size() > 0) { + return this->getPathnameFromContentURIDataColumn(contentURI, id); + } + } + } + } + + if (url.scheme == "content") { + if (this->isPhotosURI(uri)) { + const auto parts = split(url.pathname, '/'); + if (parts.size() > 0) { + return parts[parts.size() - 1]; + } + } + + return this->getPathnameFromContentURIDataColumn(uri); + } + + + return ""; + } + + String ContentResolver::getPathnameFromContentURIDataColumn ( + const String& uri, + const String& id + ) { + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + const auto result = (jstring) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + platform, + "getPathnameFromContentURIDataColumn", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + attachment.env->NewStringUTF(uri.c_str()), + attachment.env->NewStringUTF(id.c_str()) + ); + + return StringWrap(attachment.env, result).str(); + } + + String ContentResolver::getExternalContentURIForType (const String& type) { + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + const auto result = (jstring) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + platform, + "getExternalContentURIForType", + "(Ljava/lang/String;)Ljava/lang/String;", + attachment.env->NewStringUTF(type.c_str()) + ); + + return StringWrap(attachment.env, result).str(); + } + + Vector<String> ContentResolver::getPathnameEntriesFromContentURI ( + const String& uri + ) { + Vector<String> entries; + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + const auto results = (jobjectArray) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + platform, + "getPathnameEntriesFromContentURI", + "(Ljava/lang/String;)[Ljava/lang/String;", + attachment.env->NewStringUTF(uri.c_str()) + ); + + const auto length = attachment.env->GetArrayLength(results); + + for (int i = 0; i < length; ++i) { + const auto result = (jstring) attachment.env->GetObjectArrayElement(results, i); + const auto pathname = attachment.env->GetStringUTFChars(result, nullptr); + if (pathname != nullptr) { + entries.push_back(pathname); + attachment.env->ReleaseStringUTFChars(result, pathname); + } + } + + return entries; + } + + bool ContentResolver::hasAccess (const String& uri) { + if (!uri.starts_with("content:") & !uri.starts_with("android.resource:")) { + return false; + } + + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + return CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + platform, + "hasContentResolverAccess", + "(Ljava/lang/String;)Z", + attachment.env->NewStringUTF(uri.c_str()) + ); + } + + ContentResolver::FileDescriptor ContentResolver::openFileDescriptor ( + const String& uri, + off_t* offset, + off_t* length + ) { + if (!uri.starts_with("content:") & !uri.starts_with("android.resource:")) { + return nullptr; + } + + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto platform = (jobject) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->activity, + "getAppPlatform", + "()Lsocket/runtime/app/AppPlatform;" + ); + + const auto fileDescriptor = CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + platform, + "openContentResolverFileDescriptor", + "(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;", + attachment.env->NewStringUTF(uri.c_str()) + ); + + if (fileDescriptor != nullptr) { + if (offset != nullptr) { + *offset = CallClassMethodFromAndroidEnvironment( + attachment.env, + Long, + fileDescriptor, + "getStartOffset", + "()J" + ); + } + + if (length != nullptr) { + *length = CallClassMethodFromAndroidEnvironment( + attachment.env, + Long, + fileDescriptor, + "getLength", + "()J" + ); + } + + return attachment.env->NewGlobalRef(fileDescriptor); + } + + return nullptr; + } + + bool ContentResolver::closeFileDescriptor (ContentResolver::FileDescriptor fileDescriptor) { + if (fileDescriptor == nullptr) { + return false; + } + + const auto attachment = JNIEnvironmentAttachment(this->jvm); + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + fileDescriptor, + "close", + "()V" + ); + + attachment.env->DeleteGlobalRef(fileDescriptor); + + return true; + } + + size_t ContentResolver::getFileDescriptorLength (FileDescriptor fileDescriptor) { + if (fileDescriptor == nullptr) { + return -EINVAL; + } + + const auto attachment = JNIEnvironmentAttachment(this->jvm); + return CallClassMethodFromAndroidEnvironment( + attachment.env, + Long, + fileDescriptor, + "getLength", + "()J" + ); + } + + size_t ContentResolver::getFileDescriptorOffset (FileDescriptor fileDescriptor) { + if (fileDescriptor == nullptr) { + return -EINVAL; + } + + const auto attachment = JNIEnvironmentAttachment(this->jvm); + return CallClassMethodFromAndroidEnvironment( + attachment.env, + Long, + fileDescriptor, + "getOffset", + "()J" + ); + } + + int ContentResolver::getFileDescriptorFD (FileDescriptor fileDescriptor) { + if (fileDescriptor == nullptr) { + return -EINVAL; + } + + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto parcel = CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + fileDescriptor, + "getParcelFileDescriptor", + "()Landroid/os/ParcelFileDescriptor;" + ); + + return CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + parcel, + "getFd", + "()I" + ); + } +} diff --git a/src/platform/android/content_resolver.hh b/src/platform/android/content_resolver.hh new file mode 100644 index 0000000000..2e4dd8357e --- /dev/null +++ b/src/platform/android/content_resolver.hh @@ -0,0 +1,46 @@ +#ifndef SOCKET_RUNTIME_PLATFORM_ANDROID_CONTENT_RESOLVER_H +#define SOCKET_RUNTIME_PLATFORM_ANDROID_CONTENT_RESOLVER_H + +#include "environment.hh" +#include "types.hh" + +namespace SSC::Android { + class ContentResolver { + public: + using FileDescriptor = jobject; + + Activity activity; + JVMEnvironment jvm; + ContentResolver () = default; + + bool isDocumentURI (const String& uri); + bool isContentURI (const String& uri); + bool isExternalStorageDocumentURI (const String& uri); + bool isDownloadsDocumentURI (const String& uri); + bool isMediaDocumentURI (const String& uri); + bool isPhotosURI (const String& uri); + + String getContentMimeType (const String& url); + String getExternalContentURIForType (const String& type); + String getPathnameFromURI (const String& uri); + String getPathnameFromContentURIDataColumn ( + const String& uri, + const String& id = nullptr + ); + + Vector<String> getPathnameEntriesFromContentURI (const String& uri); + + bool hasAccess (const String& uri); + FileDescriptor openFileDescriptor ( + const String& uri, + off_t* offset = nullptr, + off_t* length = nullptr + ); + + bool closeFileDescriptor (FileDescriptor fileDescriptor); + size_t getFileDescriptorLength (FileDescriptor fileDescriptor); + size_t getFileDescriptorOffset (FileDescriptor fileDescriptor); + int getFileDescriptorFD (FileDescriptor fileDescriptor); + }; +} +#endif diff --git a/src/platform/android/environment.cc b/src/platform/android/environment.cc new file mode 100644 index 0000000000..5e9078ed20 --- /dev/null +++ b/src/platform/android/environment.cc @@ -0,0 +1,102 @@ +#include "environment.hh" +#include "../../core/debug.hh" + +namespace SSC::Android { + JVMEnvironment::JVMEnvironment (JNIEnv* env) { + this->jniVersion = env->GetVersion(); + env->GetJavaVM(&jvm); + } + + int JVMEnvironment::version () const { + return this->jniVersion; + } + + JavaVM* JVMEnvironment::get () const { + return this->jvm; + } + + JNIEnvironmentAttachment::JNIEnvironmentAttachment (JavaVM *jvm, int version) { + this->attach(jvm, version); + } + + JNIEnvironmentAttachment::JNIEnvironmentAttachment (const JVMEnvironment& jvm) + : JNIEnvironmentAttachment(jvm.get(), jvm.version()) + {} + + JNIEnvironmentAttachment::JNIEnvironmentAttachment (JNIEnvironmentAttachment&& attachment) { + this->jvm = attachment.jvm; + this->env = attachment.env; + this->status = attachment.status; + this->version = attachment.version; + this->attached = attachment.attached.load(); + + attachment.jvm = nullptr; + attachment.env = nullptr; + attachment.status = 0; + attachment.version = 0; + attachment.attached = false; + } + + JNIEnvironmentAttachment::~JNIEnvironmentAttachment () { + this->detach(); + } + + JNIEnvironmentAttachment& + JNIEnvironmentAttachment::operator= (JNIEnvironmentAttachment&& attachment) { + this->jvm = attachment.jvm; + this->env = attachment.env; + this->status = attachment.status; + this->version = attachment.version; + this->attached = attachment.attached.load(); + + attachment.jvm = nullptr; + attachment.env = nullptr; + attachment.status = 0; + attachment.version = 0; + attachment.attached = false; + return *this; + } + + void JNIEnvironmentAttachment::attach (JavaVM *jvm, int version) { + this->jvm = jvm; + this->version = version; + + if (jvm != nullptr) { + this->status = this->jvm->GetEnv((void **) &this->env, this->version); + + if (this->status == JNI_EDETACHED) { + this->attached = this->jvm->AttachCurrentThread(&this->env, 0); + } + + // debug("env: %p", this->env); + } + } + + void JNIEnvironmentAttachment::detach () { + const auto jvm = this->jvm; + const auto attached = this->attached.load(); + + if (this->hasException()) { + this->printException(); + } + + this->env = nullptr; + this->jvm = nullptr; + this->status = 0; + this->attached = false; + + if (attached && jvm != nullptr) { + jvm->DetachCurrentThread(); + } + } + + bool JNIEnvironmentAttachment::hasException () const { + return this->env != nullptr && this->env->ExceptionCheck(); + } + + void JNIEnvironmentAttachment::printException () const { + if (this->env != nullptr) { + this->env->ExceptionDescribe(); + } + } +} diff --git a/src/platform/android/environment.hh b/src/platform/android/environment.hh new file mode 100644 index 0000000000..045ef17d12 --- /dev/null +++ b/src/platform/android/environment.hh @@ -0,0 +1,148 @@ +#ifndef SOCKET_RUNTIME_PLATFORM_ANDROID_ENVIRONMENT_H +#define SOCKET_RUNTIME_PLATFORM_ANDROID_ENVIRONMENT_H + +#include "../types.hh" +#include "native.hh" + +/** + * Gets class for object for `self` from `env`. + * @param {JNIEnv*} env + * @param {jobject} self + */ +#define GetObjectClassFromAndroidEnvironment(env, self) ({ \ + env->GetObjectClass(self); \ +}) + +/** + * Get field on object `self` from `env`. + * @param {JNIEnv*} env + * @param {jobject} self + * @param {typeof any} Type + * @param {const char*} field + * @param {const char*} sig + */ +#define GetObjectClassFieldFromAndroidEnvironment( \ + env, \ + self, \ + Type, \ + field, \ + sig \ +) ({ \ + auto Class = GetObjectClassFromAndroidEnvironment(env, self); \ + auto id = env->GetFieldID(Class, field, sig); \ + env->Get##Type##Field(self, id); \ +}) + +/** + * Gets the JNI `Exception` class from environment. + * @param {JNIEnv*} env + * @param {jobject} self + */ +#define GetExceptionClassFromAndroidEnvironment(env) ({ \ + env->FindClass("java/lang/Exception"); \ +}) + +/** + * @param {JNIEnv*} env + * @param {jobject} object + * @param {const char*} method + * @param {const char* sig + * @param {...any} ... + */ +#define CallObjectClassMethodFromAndroidEnvironment( \ + env, \ + object, \ + method, \ + sig, \ + ... \ +) ({ \ + auto Class = env->GetObjectClass(object); \ + auto ID = env->GetMethodID(Class, method, sig); \ + env->CallObjectMethod(object, ID, ##__VA_ARGS__); \ +}) + +/** + * @param {JNIEnv*} env + * @param {Object|Boolean|Int|Float|Void} Type + * @param {jobject} object + * @param {const char*} method + * @param {const char* sig + * @param {...any} ... + */ +#define CallClassMethodFromAndroidEnvironment( \ + env, \ + Type, \ + object, \ + method, \ + sig, \ + ... \ +) ({ \ + auto Class = env->GetObjectClass(object); \ + auto ID = env->GetMethodID(Class, method, sig); \ + env->Call##Type##Method(object, ID, ##__VA_ARGS__); \ +}) +/** + * @param {JNIEnv*} env + * @param {jobject} object + * @param {const char*} method + * @param {const char* sig + * @param {...any} ... + */ +#define CallVoidClassMethodFromAndroidEnvironment( \ + env, \ + object, \ + method, \ + sig, \ + ... \ +) ({ \ + auto Class = env->GetObjectClass(object); \ + auto ID = env->GetMethodID(Class, method, sig); \ + env->CallVoidMethod(object, ID, ##__VA_ARGS__); \ +}) + +namespace SSC::Android { + /** + * A `JVMEnvironment` constainer holders a poitner the current `JavaVM` + * instance for the current `JNIEnv`. + */ + struct JVMEnvironment { + JavaVM* jvm = nullptr; + JNIEnv* env = nullptr; + int jniVersion = 0; + + JVMEnvironment () = default; + JVMEnvironment (JNIEnv* env); + JVMEnvironment (JavaVM* jvm); + int version () const; + JavaVM* get () const; + }; + + /** + * A `JNIEnvironmentAttachment` container can attach to the current thread + * for the `JavaVM` instance. This is useful to run code on the same thread + * as the Android application. + */ + struct JNIEnvironmentAttachment { + JavaVM *jvm = nullptr; + JNIEnv *env = nullptr; + int status = 0; + int version = 0; + Atomic<bool> attached = false; + + JNIEnvironmentAttachment () = default; + JNIEnvironmentAttachment (const JVMEnvironment& jvm); + JNIEnvironmentAttachment (JavaVM *jvm, int version); + JNIEnvironmentAttachment (const JNIEnvironmentAttachment&) = delete; + JNIEnvironmentAttachment (JNIEnvironmentAttachment&&); + ~JNIEnvironmentAttachment (); + JNIEnvironmentAttachment& operator= (const JNIEnvironmentAttachment&) = delete; + JNIEnvironmentAttachment& operator= (JNIEnvironmentAttachment&&); + + void attach (JavaVM *jvm, int version); + void detach (); + + bool hasException () const; + void printException () const; + }; +} +#endif diff --git a/src/platform/android/looper.cc b/src/platform/android/looper.cc new file mode 100644 index 0000000000..18429e216c --- /dev/null +++ b/src/platform/android/looper.cc @@ -0,0 +1,86 @@ +#include "looper.hh" +#include "../../core/debug.hh" + +namespace SSC::Android { + Looper::Looper (JVMEnvironment jvm) + : jvm(jvm) + {} + + void Looper::dispatch (const DispatchCallback& callback) { + while (!this->isReady) { + msleep(2); + } + + const auto attachment = Android::JNIEnvironmentAttachment(this->jvm); + const auto context = new DispatchCallbackContext(callback, this); + write(this->fds[1], &context, sizeof(DispatchCallbackContext*)); + } + + bool Looper::acquire (const LoopCallback& callback) { + if (pipe(this->fds) != 0) { + return false; + } + + const auto attachment = Android::JNIEnvironmentAttachment(this->jvm); + this->looper = ALooper_forThread(); + + if (this->looper == nullptr) { + return false; + } + + ALooper_acquire(this->looper); + + const auto context = std::make_shared<LoopCallbackContext>(callback, this); + const auto result = ALooper_addFd( + this->looper, + this->fds[0], + ALOOPER_POLL_CALLBACK, + ALOOPER_EVENT_INPUT, + [](int fd, int events, void* data) -> int { + const auto context = reinterpret_cast<LoopCallbackContext*>(data); + const auto size = sizeof(DispatchCallbackContext*); + auto buffer = new char[size]{0}; + read(fd, reinterpret_cast<void*>(buffer), size); + + auto dispatch = *((DispatchCallbackContext**) buffer); + if (dispatch != nullptr) { + if (dispatch->callback != nullptr) { + dispatch->callback(); + } + } + + delete dispatch; + delete [] buffer; + + if (context != nullptr) { + if (context->callback != nullptr) { + context->callback(); + } + } + return 1; + }, + context.get() + ); + + if (result == -1) { + return false; + } + + this->context = context; + this->isReady = true; + return true; + } + + void Looper::release () { + if (this->looper) { + ALooper_removeFd(this->looper, this->fds[0]); + ALooper_release(this->looper); + close(this->fds[0]); + close(this->fds[1]); + } + } + + bool Looper::isAcquired () const { + return this->looper != nullptr && this->context != nullptr; + } +} diff --git a/src/platform/android/looper.hh b/src/platform/android/looper.hh new file mode 100644 index 0000000000..a934427298 --- /dev/null +++ b/src/platform/android/looper.hh @@ -0,0 +1,61 @@ +#ifndef SOCKET_RUNTIME_PLATFORM_ANDROID_LOOPER_H +#define SOCKET_RUNTIME_PLATFORM_ANDROID_LOOPER_H + +#include "../types.hh" +#include "environment.hh" +#include "native.hh" + +namespace SSC::Android { + struct Looper { + using LoopCallback = Function<void()>; + using DispatchCallback = Function<void()>; + + struct LoopCallbackContext { + const Looper::LoopCallback callback; + Looper* looper = nullptr; + LoopCallbackContext ( + const Looper::LoopCallback& callback, + Looper* looper + ) : callback(callback), + looper(looper) + {} + }; + + struct DispatchCallbackContext { + const Looper::DispatchCallback callback; + Looper* looper = nullptr; + DispatchCallbackContext ( + const Looper::DispatchCallback& callback, + Looper* looper + ) : callback(callback), + looper(looper) + {} + + DispatchCallbackContext () = delete; + DispatchCallbackContext (const DispatchCallbackContext&) = delete; + DispatchCallbackContext (DispatchCallbackContext&&) = delete; + DispatchCallbackContext operator= (const DispatchCallbackContext&) = delete; + DispatchCallbackContext operator= (DispatchCallbackContext&&) = delete; + virtual ~DispatchCallbackContext() {} + }; + + ALooper* looper; + Atomic<bool> isReady = false; + int fds[2]; + JVMEnvironment jvm; + SharedPointer<LoopCallbackContext> context; + + Looper (JVMEnvironment jvm); + Looper () = delete; + Looper (const Looper&) = delete; + Looper (Looper&&) = delete; + Looper operator= (const Looper&) = delete; + Looper operator= (Looper&&) = delete; + + bool acquire (const LoopCallback& callback = nullptr); + void release (); + void dispatch (const DispatchCallback& callback); + bool isAcquired () const; + }; +} +#endif diff --git a/src/platform/android/mime.cc b/src/platform/android/mime.cc new file mode 100644 index 0000000000..cd627074ef --- /dev/null +++ b/src/platform/android/mime.cc @@ -0,0 +1,31 @@ +#include "mime.hh" +#include "string_wrap.hh" + +namespace SSC::Android { + static MimeTypeMap sharedMimeTypeMap; + + void initializeMimeTypeMap (MimeTypeMapRef ref, JVMEnvironment jvm) { + sharedMimeTypeMap.ref = ref; + sharedMimeTypeMap.jvm = jvm; + } + + const MimeTypeMap* MimeTypeMap::sharedMimeTypeMap () { + return &Android::sharedMimeTypeMap; + } + + String MimeTypeMap::getMimeTypeFromExtension (const String& extension) const { + const auto attachment = JNIEnvironmentAttachment(this->jvm); + const auto extensionString = attachment.env->NewStringUTF(extension.c_str()); + const auto mimeTypeString = (jstring) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + this->ref, + "getMimeTypeFromExtension", + "(Ljava/lang/String;)Ljava/lang/String;", + extensionString + ); + + const auto mimeType = StringWrap(attachment.env, mimeTypeString).str(); + attachment.env->DeleteLocalRef(extensionString); + return mimeType; + } +} diff --git a/src/platform/android/mime.hh b/src/platform/android/mime.hh new file mode 100644 index 0000000000..c7cbe2a3d1 --- /dev/null +++ b/src/platform/android/mime.hh @@ -0,0 +1,20 @@ +#ifndef SOCKET_RUNTIME_PLATFORM_ANDROID_MIME_H +#define SOCKET_RUNTIME_PLATFORM_ANDROID_MIME_H + +#include "../types.hh" +#include "environment.hh" + +namespace SSC::Android { + using MimeTypeMapRef = jobject; + + struct MimeTypeMap { + MimeTypeMapRef ref; + JVMEnvironment jvm; + static const MimeTypeMap* sharedMimeTypeMap (); + String getMimeTypeFromExtension (const String& extension) const; + }; + + void initializeMimeTypeMap (MimeTypeMapRef ref, JVMEnvironment jvm); +} + +#endif diff --git a/src/platform/android/native.hh b/src/platform/android/native.hh new file mode 100644 index 0000000000..86382d9b6f --- /dev/null +++ b/src/platform/android/native.hh @@ -0,0 +1,12 @@ +#ifndef SOCKET_RUNTIME_PLATFORM_ANDROID_NATIVE_H +#define SOCKET_RUNTIME_PLATFORM_ANDROID_NATIVE_H +#if defined(__ANDROID__) +// Java Native Interface +// @see https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html +#include <jni.h> +#include <android/asset_manager.h> +#include <android/asset_manager_jni.h> +#include <android/log.h> +#include <android/looper.h> +#endif +#endif diff --git a/src/android/string_wrap.cc b/src/platform/android/string_wrap.cc similarity index 66% rename from src/android/string_wrap.cc rename to src/platform/android/string_wrap.cc index ea00afc6f8..d9070d0a99 100644 --- a/src/android/string_wrap.cc +++ b/src/platform/android/string_wrap.cc @@ -1,10 +1,6 @@ -#include "internal.hh" +#include "string_wrap.hh" -// clang-format off -// -#pragma StringWrap - -namespace SSC::android { +namespace SSC::Android { StringWrap::StringWrap (JNIEnv *env) { this->env = env; this->ref = nullptr; @@ -13,13 +9,19 @@ namespace SSC::android { this->needsRelease = false; } - StringWrap::StringWrap (const StringWrap ©) + StringWrap::StringWrap (const StringWrap& copy) : env(copy.env), ref(copy.ref), length(copy.length), string(copy.string), needsRelease(false) - {} + {} + + StringWrap::StringWrap (StringWrap&& copy) + : StringWrap(copy) + { + copy.needsRelease = false; + } StringWrap::StringWrap (JNIEnv *env, jstring ref) : StringWrap(env) { if (ref) { @@ -27,7 +29,11 @@ namespace SSC::android { } } - StringWrap::StringWrap (JNIEnv *env, SSC::String string) : StringWrap(env) { + StringWrap::StringWrap (JNIEnv *env, jobject ref) + : StringWrap(env, (jstring) ref) + {} + + StringWrap::StringWrap (JNIEnv *env, String string) : StringWrap(env) { if (string.size() > 0) { this->set(string); } @@ -43,7 +49,20 @@ namespace SSC::android { this->release(); } - void StringWrap::set (SSC::String string) { + const StringWrap& StringWrap::operator= (const StringWrap& string) { + *this = string; + this->needsRelease = false; + return *this; + } + + StringWrap& StringWrap::operator= (StringWrap&& string) { + *this = string; + this->needsRelease = true; + string.needsRelease = false; + return *this; + } + + void StringWrap::set (String string) { this->set(string.c_str()); } @@ -70,12 +89,12 @@ namespace SSC::android { this->needsRelease = false; } - const char * StringWrap::c_str () { + const char* StringWrap::c_str () { return this->string; } - const SSC::String StringWrap::str () { - SSC::String value; + const String StringWrap::str () { + String value; if (this->string) { value.assign(this->string); diff --git a/src/platform/android/string_wrap.hh b/src/platform/android/string_wrap.hh new file mode 100644 index 0000000000..dcff37dc24 --- /dev/null +++ b/src/platform/android/string_wrap.hh @@ -0,0 +1,42 @@ +#ifndef SOCKET_RUNTIME_ANDROID_STRING_WRAP_H +#define SOCKET_RUNTIME_ANDROID_STRING_WRAP_H + +#include "../types.hh" +#include "native.hh" + +namespace SSC::Android { + /** + * A container for a JNI string (jstring). + */ + class StringWrap { + JNIEnv *env = nullptr; + jstring ref = nullptr; + const char *string = nullptr; + size_t length = 0; + jboolean needsRelease = false; + + public: + StringWrap (JNIEnv *env); + StringWrap (const StringWrap& copy); + StringWrap (StringWrap&& copy); + StringWrap (JNIEnv *env, jstring ref); + StringWrap (JNIEnv *env, jobject ref); + StringWrap (JNIEnv *env, String string); + StringWrap (JNIEnv *env, const char *string); + ~StringWrap (); + + void set (String string); + void set (const char *string); + void set (jstring ref); + void release (); + + const String str (); + const jstring j_str (); + const char * c_str (); + const size_t size (); + + const StringWrap& operator= (const StringWrap& string); + StringWrap& operator= (StringWrap&& string); + }; +} +#endif diff --git a/src/platform/android/types.hh b/src/platform/android/types.hh new file mode 100644 index 0000000000..e24e0bb10a --- /dev/null +++ b/src/platform/android/types.hh @@ -0,0 +1,46 @@ +#ifndef SOCKET_RUNTIME_PLATFORM_ANDROID_TYPES_H +#define SOCKET_RUNTIME_PLATFORM_ANDROID_TYPES_H + +#include "../types.hh" +#include "native.hh" + +namespace SSC::Android { + /** + * An Android AssetManager NDK type. + */ + using AssetManager = ::AAssetManager; + + /** + * An Android AssetManager Asset NDK type. + */ + using Asset = ::AAsset; + + /** + * An Android AssetManager AssetDirectory NDK type. + */ + using AssetDirectory = ::AAssetDir; + + /** + * An opaque `Activity` instance. + */ + using Activity = ::jobject; + + /** + * An opaque `Application` instance. + */ + using Application = ::jobject; + + /** + * A container that holds Android OS build information. + */ + struct BuildInformation { + String brand; + String device; + String fingerprint; + String hardware; + String model; + String manufacturer; + String product; + }; +} +#endif diff --git a/src/core/platform.cc b/src/platform/platform.cc similarity index 69% rename from src/core/platform.cc rename to src/platform/platform.cc index b9c774f245..b29135c759 100644 --- a/src/core/platform.cc +++ b/src/platform/platform.cc @@ -1,5 +1,8 @@ #include "platform.hh" +#define IMAX_BITS(m) ((m)/((m) % 255+1) / 255 % 255 * 8 + 7-86 / ((m) % 255+12)) +#define RAND_MAX_WIDTH IMAX_BITS(RAND_MAX) + namespace SSC { extern const RuntimePlatform platform = { #if defined(__x86_64__) || defined(_M_X64) @@ -39,17 +42,17 @@ namespace SSC { #undef linux #ifdef __ANDROID__ .os = "android", + .android = true, + .linux = true, #else .os = "linux", - #endif - - .android = true, .linux = true, + #endif #if defined(__unix__) || defined(unix) || defined(__unix) - .unix = true + .unix = true #else - .unix = false + .unix = false #endif #endif @@ -69,4 +72,26 @@ namespace SSC { .unix = true #endif }; + + uint64_t rand64 () { + static const auto maxWidth = RAND_MAX_WIDTH; + static bool init = false; + + if (!init) { + init = true; + srand(time(0)); + } + + uint64_t r = 0; + for (int i = 0; i < 64; i += maxWidth) { + r <<= maxWidth; + r ^= (unsigned) rand(); + } + return r; + } + + void msleep (uint64_t ms) { + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); + } } diff --git a/src/core/platform.hh b/src/platform/platform.hh similarity index 70% rename from src/core/platform.hh rename to src/platform/platform.hh index 34d0112e72..f09e395ac5 100644 --- a/src/core/platform.hh +++ b/src/platform/platform.hh @@ -1,5 +1,9 @@ -#ifndef SSC_CORE_PLATFORM_H -#define SSC_CORE_PLATFORM_H +#ifndef SOCKET_RUNTIME_PLATFORM_PLATFORM_H +#define SOCKET_RUNTIME_PLATFORM_PLATFORM_H + +#if SOCKET_RUNTIME_PLATFORM_WANTS_MINGW +#include <_mingw.h> +#endif // All Platforms #include <errno.h> @@ -25,6 +29,7 @@ #include <netinet/in.h> #include <sys/un.h> #include <UIKit/UIKit.h> +#include <objc/runtime.h> #else #include <Cocoa/Cocoa.h> #include <Carbon/Carbon.h> @@ -38,17 +43,16 @@ #include <JavaScriptCore/JavaScript.h> #include <gtk/gtk.h> #include <gdk/gdkkeysyms.h> -#include <webkit2/webkit2.h> + #if SOCKET_RUNTIME_DESKTOP_EXTENSION + #include <webkit2/webkit-web-extension.h> + #else + #include <webkit2/webkit2.h> + #endif #endif // `__linux__` // Android (Linux) #if defined(__ANDROID__) -// Java Native Interface -// @see https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html -#include <jni.h> -#include <android/asset_manager.h> -#include <android/asset_manager_jni.h> -#include <android/log.h> +#include "android/native.hh" #endif // `__ANDROID__` // Windows @@ -60,36 +64,46 @@ #undef _WINSOCKAPI_ #define _WINSOCKAPI_ -#include <WinSock2.h> +#include <winsock2.h> #include <windows.h> #include <dwmapi.h> +#include <fileapi.h> #include <io.h> -#include <tchar.h> -#include <wingdi.h> - +#include <objidl.h> #include <signal.h> -#include <shlobj_core.h> +#include <shellapi.h> +#include <shlobj.h> +#include <shlwapi.h> #include <shobjidl.h> +#include <tchar.h> +#include <urlmon.h> +#include <uxtheme.h> +#include <wingdi.h> +#include <wrl.h> #if !defined(SOCKET_RUNTIME_EXTENSION) #include <WebView2.h> #include <WebView2EnvironmentOptions.h> #endif -#include <shellapi.h> - #pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "dbghelp.lib") +#pragma comment(lib, "Dwmapi.lib") +#pragma comment(lib, "Gdi32.lib") +#pragma comment(lib, "iphlpapi.lib") +#pragma comment(lib, "libuv.lib") +#pragma comment(lib, "llama.lib") +#pragma comment(lib, "psapi.lib") #pragma comment(lib, "shell32.lib") -#pragma comment(lib, "version.lib") +#pragma comment(lib, "Shlwapi.lib") +#pragma comment(lib, "urlmon.lib") #pragma comment(lib, "user32.lib") +#pragma comment(lib, "userenv.lib") +#pragma comment(lib, "UxTheme.lib") +#pragma comment(lib, "version.lib") #pragma comment(lib, "WebView2LoaderStatic.lib") #pragma comment(lib, "Ws2_32.lib") -#pragma comment(lib, "iphlpapi.lib") -#pragma comment(lib, "psapi.lib") -#pragma comment(lib, "userenv.lib") -#pragma comment(lib, "libuv.lib") -#pragma comment(lib, "dbghelp.lib") #define isatty _isatty #define fileno _fileno @@ -104,15 +118,26 @@ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> +#include <dlfcn.h> +#else +#endif + +#if SOCKET_RUNTIME_CROSS_COMPILED_HOST +#include <windows.foundation.h> #endif -#include "config.hh" +#include <socket/platform.h> +#include "string.hh" #include "types.hh" +#if SOCKET_RUNTIME_PLATFORM_ANDROID +#include "android.hh" +#endif + namespace SSC { struct RuntimePlatform { - const String arch = ""; - const String os = ""; + const String arch; + const String os; bool mac = false; bool ios = false; bool win = false; @@ -122,6 +147,7 @@ namespace SSC { }; extern const RuntimePlatform platform; + void msleep (uint64_t ms); + uint64_t rand64 (); } - #endif diff --git a/src/core/string.cc b/src/platform/string.cc similarity index 68% rename from src/core/string.cc rename to src/platform/string.cc index 4cd553e340..266f6c2689 100644 --- a/src/core/string.cc +++ b/src/platform/string.cc @@ -1,7 +1,5 @@ +#include "platform.hh" #include "string.hh" -#include "debug.hh" - -#include <regex> #if defined(min) #undef min @@ -17,9 +15,9 @@ namespace SSC { } String tmpl (const String& source, const Map& variables) { - String output = source; + auto output = source; - for (const auto tuple : variables) { + for (const auto& tuple : variables) { auto key = String("[{]+(" + tuple.first + ")[}]+"); auto value = tuple.second; output = std::regex_replace(output, std::regex(key), value); @@ -35,7 +33,7 @@ namespace SSC { while (current.size() > 0 && position < source.size()) { position = current.find(needle); - if (position == std::string::npos) { + if (position == String::npos) { result.push_back(current); break; } @@ -93,6 +91,41 @@ namespace SSC { return source; } + String toLowerCase (const String& source) { + String output = source; + std::transform( + output.begin(), + output.end(), + output.begin(), + [](auto ch) { return std::tolower(ch); } + ); + return output; + } + + String toUpperCase (const String& source) { + String output = source; + std::transform( + output.begin(), + output.end(), + output.begin(), + [](auto ch) { return std::toupper(ch); } + ); + return output; + } + + String toProperCase (const String& source) { + String output = ""; + if (source.size() > 0) { + output += toupper(source[0]); + } + + if (source.size() > 1) { + output += source.substr(1); + } + + return output; + } + WString convertStringToWString (const String& source) { WString result(source.length(), L' '); std::copy(source.begin(), source.end(), result.begin()); @@ -131,6 +164,14 @@ namespace SSC { return join(vector, String(1, separator)); } + const String join (const Set<String>& set, const String& separator) { + return join(Vector<String>(set.begin(), set.end()), separator); + } + + const String join (const Set<String>& set, const char separator) { + return join(set, String(1, separator)); + } + Vector<String> parseStringList (const String& string, const Vector<char>& separators) { auto list = Vector<String>(); for (const auto& separator : separators) { @@ -151,4 +192,32 @@ namespace SSC { Vector<String> parseStringList (const String& string) { return parseStringList(string, { ' ', ',' }); } + +#if SOCKET_RUNTIME_PLATFORM_WINDOWS + String formatWindowsError (DWORD error, const String& source) { + StringStream message; + LPVOID errorMessage; + + // format + FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR) &errorMessage, + 0, + nullptr + ); + + // create output string + message + << "Error " << error + << " in " << source + << ": " << (LPTSTR) errorMessage; + + LocalFree(errorMessage); + + return message.str(); + } +#endif } diff --git a/src/core/string.hh b/src/platform/string.hh similarity index 75% rename from src/core/string.hh rename to src/platform/string.hh index 40f26fb960..578d61e58c 100644 --- a/src/core/string.hh +++ b/src/platform/string.hh @@ -1,8 +1,8 @@ -#ifndef SSC_CORE_STRING_HH -#define SSC_CORE_STRING_HH +#ifndef SOCKET_RUNTIME_PLATFORM_STRING_H +#define SOCKET_RUNTIME_PLATFORM_STRING_H -#include "types.hh" #include <regex> +#include "types.hh" /** * Converts a literal expression to an inline string: @@ -17,6 +17,9 @@ namespace SSC { String replace (const String& source, const std::regex& regex, const String& value); String tmpl (const String& source, const Map& variables); String trim (String source); + String toLowerCase (const String& source); + String toUpperCase (const String& source); + String toProperCase (const String& source); // conversion WString convertStringToWString (const String& source); @@ -30,9 +33,15 @@ namespace SSC { const Vector<String> split (const String& source, const String& needle); const String join (const Vector<String>& vector, const String& separator); const String join (const Vector<String>& vector, const char separator); + const String join (const Set<String>& set, const String& separator); + const String join (const Set<String>& set, const char separator); Vector<String> parseStringList (const String& string, const Vector<char>& separators); Vector<String> parseStringList (const String& string, const char separator); Vector<String> parseStringList (const String& string); + +#if SOCKET_RUNTIME_PLATFORM_WINDOWS + String formatWindowsError (DWORD error, const String& source); +#endif } #endif diff --git a/src/core/types.hh b/src/platform/types.hh similarity index 52% rename from src/core/types.hh rename to src/platform/types.hh index 7f22581a3d..def72808bd 100644 --- a/src/core/types.hh +++ b/src/platform/types.hh @@ -1,16 +1,21 @@ -#ifndef SSC_CORE_TYPES_H -#define SSC_CORE_TYPES_H +#ifndef SOCKET_RUNTIME_PLATFORM_TYPES_H +#define SOCKET_RUNTIME_PLATFORM_TYPES_H #include <array> #include <atomic> #include <filesystem> +#include <fstream> #include <functional> +#include <future> #include <map> #include <mutex> +#include <ostream> #include <queue> +#include <set> #include <sstream> #include <string> #include <thread> +#include <type_traits> #include <vector> #if defined(__APPLE__) @@ -21,14 +26,17 @@ namespace SSC { namespace fs = std::filesystem; using AtomicBool = std::atomic<bool>; + using AtomicInt = std::atomic<int>; using String = std::string; using StringStream = std::stringstream; using WString = std::wstring; using WStringStream = std::wstringstream; - using Map = std::map<String, String>; + using InputFileStream = std::ifstream; + using OutputFileStream = std::ofstream; using Mutex = std::recursive_mutex; - using Path = fs::path; using Lock = std::lock_guard<Mutex>; + using Map = std::map<String, String>; + using Path = fs::path; using Thread = std::thread; using Exception = std::exception; @@ -36,9 +44,15 @@ namespace SSC { template <typename T, int k> using Array = std::array<T, k>; template <typename T> using Queue = std::queue<T>; template <typename T> using Vector = std::vector<T>; + template <typename T> using Function = std::function<T>; + template <typename X, typename Y> using Tuple = std::tuple<X, Y>; + template <typename T = String> using Set = std::set<T>; + template <typename T> using SharedPointer = std::shared_ptr<T>; + template <typename T> using UniquePointer = std::unique_ptr<T>; + template <typename T> using Promise = std::promise<T>; + template <typename T> using InputStreamBufferIterator = std::istreambuf_iterator<T>; - using ExitCallback = std::function<void(int code)>; - using MessageCallback = std::function<void(const String)>; + using ExitCallback = Function<void(int code)>; + using MessageCallback = Function<void(const String)>; } - #endif diff --git a/src/process/process.hh b/src/process/process.hh deleted file mode 100644 index 21527a9607..0000000000 --- a/src/process/process.hh +++ /dev/null @@ -1,230 +0,0 @@ -#ifndef SSC_PROCESS_PROCESS_H -#define SSC_PROCESS_PROCESS_H - -#include <iostream> - -#include "../core/core.hh" - -#ifndef WIFEXITED -#define WIFEXITED(w) ((w) & 0x7f) -#endif - -#ifndef WEXITSTATUS -#define WEXITSTATUS(w) (((w) & 0xff00) >> 8) -#endif - -namespace SSC { - struct ExecOutput { - SSC::String output; - int exitCode = 0; - }; - - // Additional parameters to Process constructors. - struct ProcessConfig { - // Buffer size for reading stdout and stderr. Default is 131072 (128 kB). - std::size_t buffer_size = 131072; - // Set to true to inherit file descriptors from parent process. Default is false. - // On Windows: has no effect unless read_stdout==nullptr, read_stderr==nullptr and open_stdin==false. - bool inherit_file_descriptors = false; - - // On Windows only: controls how the process is started, mimics STARTUPINFO's wShowWindow. - // See: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/ns-processthreadsapi-startupinfoa - // and https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-showwindow - enum class ShowWindow { - hide = 0, - show_normal = 1, - show_minimized = 2, - maximize = 3, - show_maximized = 3, - show_no_activate = 4, - show = 5, - minimize = 6, - show_min_no_active = 7, - show_na = 8, - restore = 9, - show_default = 10, - force_minimize = 11 - }; - // On Windows only: controls how the window is shown. - ShowWindow show_window{ShowWindow::show_default}; - }; - - inline ExecOutput exec (SSC::String command) { - command = command + " 2>&1"; - - ExecOutput eo; - FILE* pipe; - size_t count; - int exitCode = 0; - const int bufsize = 128; - std::array<char, 128> buffer; - - #ifdef _WIN32 - // - // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/popen-wpopen?view=msvc-160 - // _popen works fine in a console application... ok fine that's all we need it for... thanks. - // - pipe = _popen((const char*) command.c_str(), "rt"); - #else - pipe = popen((const char*) command.c_str(), "r"); - #endif - - if (pipe == NULL) { - std::cout << "error: unable to open the command" << std::endl; - exit(1); - } - - do { - if ((count = fread(buffer.data(), 1, bufsize, pipe)) > 0) { - eo.output.insert(eo.output.end(), std::begin(buffer), std::next(std::begin(buffer), count)); - } - } while (count > 0); - - #ifdef _WIN32 - exitCode = _pclose(pipe); - #else - exitCode = pclose(pipe); - #endif - - if (!WIFEXITED(exitCode) || exitCode != 0) { - auto status = WEXITSTATUS(exitCode); - if (status && exitCode) { - exitCode = status; - } - } - - eo.exitCode = exitCode; - - return eo; - } - - // Platform independent class for creating processes. - // Note on Windows: it seems not possible to specify which pipes to redirect. - // Thus, at the moment, if read_stdout==nullptr, read_stderr==nullptr and open_stdin==false, - // the stdout, stderr and stdin are sent to the parent process instead. - class Process { - public: - static constexpr auto PROCESS_WAIT_TIMEOUT = 256; - - #ifdef _WIN32 - typedef unsigned long id_type; // Process id type - typedef void *fd_type; // File descriptor type - #else - typedef pid_t id_type; - typedef int fd_type; - typedef SSC::String string_type; - #endif - - SSC::String command; - SSC::String argv; - SSC::String path; - std::atomic<bool> closed = true; - std::atomic<int> status = -1; - id_type id = 0; - - private: - - class Data { - public: - Data() noexcept; - id_type id; - #ifdef _WIN32 - void *handle{nullptr}; - #endif - int exit_status{-1}; - }; - - public: - Process( - const SSC::String &command, - const SSC::String &argv, - const SSC::String &path = SSC::String(""), - MessageCallback read_stdout = nullptr, - MessageCallback read_stderr = nullptr, - MessageCallback on_exit = nullptr, - bool open_stdin = true, - const ProcessConfig &config = {}) noexcept; - -#ifndef _WIN32 - // Starts a process with the environment of the calling process. - // Supported on Unix-like systems only. - Process( - const std::function<int()> &function, - MessageCallback read_stdout = nullptr, - MessageCallback read_stderr = nullptr, - MessageCallback on_exit = nullptr, - bool open_stdin = true, - const ProcessConfig &config = {}) noexcept; -#endif - - ~Process() noexcept { - close_fds(); - }; - - // Get the process id of the started process. - id_type getPID() const noexcept { - return data.id; - } - - // Write to stdin. - bool write(const char *bytes, size_t n); - // Write to stdin. Convenience function using write(const char *, size_t). - bool write(const SSC::String &s) { - return write(s.c_str(), s.size()); - } - // Close stdin. If the process takes parameters from stdin, use this to - // notify that all parameters have been sent. - void close_stdin() noexcept; - id_type open() noexcept { - if (this->command.size() == 0) return 0; - auto pid = open(this->command + this->argv, this->path); - read(); - return pid; - } - - // Kill a given process id. Use kill(bool force) instead if possible. - // force=true is only supported on Unix-like systems. - void kill (id_type id) noexcept; - void kill () noexcept { - this->kill(this->getPID()); - } - - int wait () { - do { - msleep(Process::PROCESS_WAIT_TIMEOUT); - } while (this->closed == false); - - return this->status; - } - - private: - Data data; - std::mutex close_mutex; - MessageCallback read_stdout; - MessageCallback read_stderr; - MessageCallback on_exit; -#ifndef _WIN32 - std::thread stdout_stderr_thread; -#else - std::thread stdout_thread, stderr_thread; -#endif - bool open_stdin; - std::mutex stdin_mutex; - std::mutex stdout_mutex; - std::mutex stderr_mutex; - - ProcessConfig config; - - std::unique_ptr<fd_type> stdout_fd, stderr_fd, stdin_fd; - - id_type open(const SSC::String &command, const SSC::String &path) noexcept; -#ifndef _WIN32 - id_type open(const std::function<int()> &function) noexcept; -#endif - void read() noexcept; - void close_fds() noexcept; - }; - -} // namespace SSC - -#endif // SSC_HPP_ diff --git a/src/process/unix.cc b/src/process/unix.cc deleted file mode 100644 index a82bf5636d..0000000000 --- a/src/process/unix.cc +++ /dev/null @@ -1,387 +0,0 @@ -#include <algorithm> -#include <bitset> -#include <cstdlib> -#include <cstring> -#include <fcntl.h> -#include <iostream> -#include <poll.h> -#include <set> -#include <signal.h> -#include <sstream> -#include <stdexcept> -#include <sys/wait.h> -#include <unistd.h> - -#include "process.hh" - -namespace SSC { - -static StringStream initial; - -Process::Data::Data() noexcept : id(-1) {} -Process::Process( - const String &command, - const String &argv, - const String &path, - MessageCallback read_stdout, - MessageCallback read_stderr, - MessageCallback on_exit, - bool open_stdin, - const ProcessConfig &config -) noexcept : - open_stdin(true), - read_stdout(std::move(read_stdout)), - read_stderr(std::move(read_stderr)), - on_exit(std::move(on_exit)) -{ - this->command = command; - this->argv = argv; - this->path = path; -} - -Process::Process( - const std::function<int()> &function, - MessageCallback read_stdout, - MessageCallback read_stderr, - MessageCallback on_exit, - bool open_stdin, const ProcessConfig &config -) noexcept: - read_stdout(std::move(read_stdout)), - read_stderr(std::move(read_stderr)), - on_exit(std::move(on_exit)), - open_stdin(open_stdin), - config(config) -{ - open(function); - read(); -} - -Process::id_type Process::open(const std::function<int()> &function) noexcept { - if (open_stdin) { - stdin_fd = std::unique_ptr<fd_type>(new fd_type); - } - - if (read_stdout) { - stdout_fd = std::unique_ptr<fd_type>(new fd_type); - } - - if (read_stderr) { - stderr_fd = std::unique_ptr<fd_type>(new fd_type); - } - - int stdin_p[2]; - int stdout_p[2]; - int stderr_p[2]; - - if (stdin_fd && pipe(stdin_p) != 0) { - return -1; - } - - if (stdout_fd && pipe(stdout_p) != 0) { - if (stdin_fd) { - close(stdin_p[0]); - close(stdin_p[1]); - } - return -1; - } - - if (stderr_fd && pipe(stderr_p) != 0) { - if (stdin_fd) { - close(stdin_p[0]); - close(stdin_p[1]); - } - - if (stdout_fd) { - close(stdout_p[0]); - close(stdout_p[1]); - } - - return -1; - } - - id_type pid = fork(); - - if (pid < 0) { - if (stdin_fd) { - close(stdin_p[0]); - close(stdin_p[1]); - } - - if (stdout_fd) { - close(stdout_p[0]); - close(stdout_p[1]); - } - - if (stderr_fd) { - close(stderr_p[0]); - close(stderr_p[1]); - } - - return pid; - } - - closed = false; - id = pid; - - if (pid > 0) { - auto thread = std::thread([this] { - int code = 0; - waitpid(this->id, &code, 0); - - this->status = WEXITSTATUS(code); - this->closed = true; - - if (this->on_exit != nullptr) { - this->on_exit(std::to_string(status)); - } - }); - - thread.detach(); - } else if (pid == 0) { - if (stdin_fd) { - dup2(stdin_p[0], 0); - } - - if (stdout_fd) { - dup2(stdout_p[1], 1); - } - - if (stderr_fd) { - dup2(stderr_p[1], 2); - } - - if (stdin_fd) { - close(stdin_p[0]); - close(stdin_p[1]); - } - - if (stdout_fd) { - close(stdout_p[0]); - close(stdout_p[1]); - } - - if (stderr_fd) { - close(stderr_p[0]); - close(stderr_p[1]); - } - - setpgid(0, 0); - - if (function) { - function(); - } - - _exit(EXIT_FAILURE); - } - - if (stdin_fd) { - close(stdin_p[0]); - } - - if (stdout_fd) { - close(stdout_p[1]); - } - - if (stderr_fd) { - close(stderr_p[1]); - } - - if (stdin_fd) { - *stdin_fd = stdin_p[1]; - } - - if (stdout_fd) { - *stdout_fd = stdout_p[0]; - } - - if (stderr_fd) { - *stderr_fd = stderr_p[0]; - } - - data.id = pid; - return pid; -} - -Process::id_type Process::open(const SSC::String &command, const SSC::String &path) noexcept { - return open([&command, &path] { - auto command_c_str = command.c_str(); - SSC::String cd_path_and_command; - - if (!path.empty()) { - auto path_escaped = path; - size_t pos = 0; - - // Based on https://www.reddit.com/r/cpp/comments/3vpjqg/a_new_platform_independent_process_library_for_c11/cxsxyb7 - while((pos = path_escaped.find('\'', pos)) != SSC::String::npos) { - path_escaped.replace(pos, 1, "'\\''"); - pos += 4; - } - - cd_path_and_command = "cd '" + path_escaped + "' && " + command; // To avoid resolving symbolic links - command_c_str = cd_path_and_command.c_str(); - } - - return execl("/bin/sh", "/bin/sh", "-c", command_c_str, nullptr); - }); -} - -void Process::read() noexcept { - if (data.id <= 0 || (!stdout_fd && !stderr_fd)) { - return; - } - - stdout_stderr_thread = std::thread([this] { - std::vector<pollfd> pollfds; - std::bitset<2> fd_is_stdout; - - if (stdout_fd) { - fd_is_stdout.set(pollfds.size()); - pollfds.emplace_back(); - pollfds.back().fd = fcntl(*stdout_fd, F_SETFL, fcntl(*stdout_fd, F_GETFL) | O_NONBLOCK) == 0 ? *stdout_fd : -1; - pollfds.back().events = POLLIN; - } - - if (stderr_fd) { - pollfds.emplace_back(); - pollfds.back().fd = fcntl(*stderr_fd, F_SETFL, fcntl(*stderr_fd, F_GETFL) | O_NONBLOCK) == 0 ? *stderr_fd : -1; - pollfds.back().events = POLLIN; - } - - auto buffer = std::unique_ptr<char[]>(new char[config.buffer_size]); - bool any_open = !pollfds.empty(); - SSC::StringStream ss; - - while (any_open && (poll(pollfds.data(), static_cast<nfds_t>(pollfds.size()), -1) > 0 || errno == EINTR)) { - any_open = false; - - for (size_t i = 0; i < pollfds.size(); ++i) { - if (!(pollfds[i].fd >= 0)) continue; - - if (pollfds[i].revents & POLLIN) { - memset(buffer.get(), 0, config.buffer_size); - const ssize_t n = ::read(pollfds[i].fd, buffer.get(), config.buffer_size); - - if (n > 0) { - if (fd_is_stdout[i]) { - std::lock_guard<std::mutex> lock(stdout_mutex); - auto b = SSC::String(buffer.get()); - auto parts = splitc(b, '\n'); - - if (parts.size() > 1) { - for (int i = 0; i < parts.size() - 1; i++) { - ss << parts[i]; - - SSC::String s(ss.str()); - read_stdout(s); - - ss.str(SSC::String()); - ss.clear(); - ss.copyfmt(initial); - } - ss << parts[parts.size() - 1]; - } else { - ss << b; - } - } else { - std::lock_guard<std::mutex> lock(stderr_mutex); - read_stderr(SSC::String(buffer.get())); - } - } else if (n < 0 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { - pollfds[i].fd = -1; - continue; - } - } - - if (pollfds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) { - pollfds[i].fd = -1; - continue; - } - - any_open = true; - } - } - }); -} - -void Process::close_fds() noexcept { - if (stdout_stderr_thread.joinable()) { - stdout_stderr_thread.join(); - } - - if (stdin_fd) { - close_stdin(); - } - - if (stdout_fd) { - if (data.id > 0) { - close(*stdout_fd); - } - - stdout_fd.reset(); - } - - if (stderr_fd) { - if (data.id > 0) { - close(*stderr_fd); - } - - stderr_fd.reset(); - } -} - -bool Process::write(const char *bytes, size_t n) { - std::lock_guard<std::mutex> lock(stdin_mutex); - - if (stdin_fd) { - SSC::String b(bytes); - - while (true && (b.size() > 0)) { - int bytesWritten = ::write(*stdin_fd, b.c_str(), b.size()); - - if (bytesWritten >= b.size()) { - break; - } - - b = b.substr(bytesWritten, b.size()); - } - - int bytesWritten = ::write(*stdin_fd, "\n", 1); - } - - return false; -} - -void Process::close_stdin() noexcept { - std::lock_guard<std::mutex> lock(stdin_mutex); - - if (stdin_fd) { - if (data.id > 0) { - close(*stdin_fd); - } - - stdin_fd.reset(); - } -} - -void Process::kill(id_type id) noexcept { - if (id <= 0) { - return; - } - - this->closed = true; - auto r = ::kill(-id, SIGINT); - - if (r != 0) { - r = ::kill(-id, SIGTERM); - - if (r != 0) { - r = ::kill(-id, SIGKILL); - - if (r != 0) { - // @TODO: print warning - } - } - } -} - -} // namespace SSC diff --git a/src/serviceworker/container.cc b/src/serviceworker/container.cc new file mode 100644 index 0000000000..7efd6e9271 --- /dev/null +++ b/src/serviceworker/container.cc @@ -0,0 +1,735 @@ +#include "../app/app.hh" +#include "../ipc/ipc.hh" +#include "../core/debug.hh" + +#include "container.hh" + +namespace SSC { + static ServiceWorkerContainer* sharedServiceWorkerContainer = nullptr; + + static const String normalizeScope (const String& scope) { + auto normalized = trim(scope); + + if (normalized.size() == 0) { + return "/"; + } + + if (normalized.ends_with("/") && normalized.size() > 1) { + normalized = normalized.substr(0, normalized.size() - 1); + } + + return normalized; + } + + const String ServiceWorkerContainer::FetchRequest::str () const { + auto string = this->scheme + "://" + this->hostname + this->pathname; + + if (this->query.size() > 0) { + string += String("?") + this->query; + } + + return string; + } + + ServiceWorkerContainer::Registration::Registration ( + const ID id, + const String& scriptURL, + const State state, + const RegistrationOptions& options + ) : id(id), + scriptURL(scriptURL), + state(state), + options(options) + {} + + ServiceWorkerContainer::Registration::Registration (const Registration& registration) { + this->id = registration.id; + this->state = registration.state.load(); + this->options = registration.options; + this->scriptURL = registration.scriptURL; + } + + ServiceWorkerContainer::Registration::Registration (Registration&& registration) { + this->id = registration.id; + this->state = registration.state.load(); + this->options = registration.options; + this->scriptURL = registration.scriptURL; + + registration.id = 0; + registration.state = State::None; + registration.options = RegistrationOptions {}; + registration.scriptURL = ""; + } + + ServiceWorkerContainer::Registration& ServiceWorkerContainer::Registration::operator= (const Registration& registration) { + this->id = registration.id; + this->state = registration.state.load(); + this->options = registration.options; + this->scriptURL = registration.scriptURL; + return *this; + } + + ServiceWorkerContainer::Registration& ServiceWorkerContainer::Registration::operator= (Registration&& registration) { + this->id = registration.id; + this->state = registration.state.load(); + this->options = registration.options; + this->scriptURL = registration.scriptURL; + + registration.id = 0; + registration.state = State::None; + registration.options = RegistrationOptions {}; + registration.scriptURL = ""; + return *this; + } + + const JSON::Object ServiceWorkerContainer::Registration::json () const { + return JSON::Object::Entries { + {"id", std::to_string(this->id)}, + {"scriptURL", this->scriptURL}, + {"scope", this->options.scope}, + {"state", this->getStateString()} + }; + } + + bool ServiceWorkerContainer::Registration::isActive () const { + return ( + this->state == Registration::State::Activating || + this->state == Registration::State::Activated + ); + } + + bool ServiceWorkerContainer::Registration::isWaiting () const { + return this->state == Registration::State::Installed; + } + + bool ServiceWorkerContainer::Registration::isInstalling () const { + return this->state == Registration::State::Installing; + } + + const String ServiceWorkerContainer::Registration::getStateString () const { + String stateString = "none"; + + if (this->state == Registration::State::Error) { + stateString = "error"; + } else if (this->state == Registration::State::Registering) { + stateString = "registering"; + } else if (this->state == Registration::State::Registered) { + stateString = "registered"; + } else if (this->state == Registration::State::Installing) { + stateString = "installing"; + } else if (this->state == Registration::State::Installed) { + stateString = "installed"; + } else if (this->state == Registration::State::Activating) { + stateString = "activating"; + } else if (this->state == Registration::State::Activated) { + stateString = "activated"; + } + + return stateString; + }; + + const JSON::Object ServiceWorkerContainer::Registration::Storage::json () const { + return JSON::Object { this->data }; + } + + void ServiceWorkerContainer::Registration::Storage::set (const String& key, const String& value) { + this->data.insert_or_assign(key, value); + } + + const String ServiceWorkerContainer::Registration::Storage::get (const String& key) const { + if (this->data.contains(key)) { + return this->data.at(key); + } + + return key; + } + + void ServiceWorkerContainer::Registration::Storage::remove (const String& key) { + if (this->data.contains(key)) { + this->data.erase(key); + } + } + + void ServiceWorkerContainer::Registration::Storage::clear () { + this->data.clear(); + } + + ServiceWorkerContainer* ServiceWorkerContainer::sharedContainer () { + return SSC::sharedServiceWorkerContainer; + } + + ServiceWorkerContainer::ServiceWorkerContainer (SharedPointer<Core> core) + : core(core), + protocols(this) + { + if (SSC::sharedServiceWorkerContainer == nullptr) { + SSC::sharedServiceWorkerContainer = this; + } + } + + ServiceWorkerContainer::~ServiceWorkerContainer () { + if (this->bridge != nullptr) { + this->bridge->emit("serviceWorker.destroy", JSON::Object {}); + } + + this->bridge = nullptr; + this->registrations.clear(); + this->fetchRequests.clear(); + this->fetchCallbacks.clear(); + } + + void ServiceWorkerContainer::reset () { + Lock lock(this->mutex); + + for (auto& entry : this->registrations) { + entry.second.state = Registration::State::Registered; + this->registerServiceWorker(entry.second.options); + } + } + + void ServiceWorkerContainer::init (IPC::Bridge* bridge) { + Lock lock(this->mutex); + + this->reset(); + this->bridge = bridge; + this->isReady = true; + + if (bridge->userConfig["webview_service-workers"].size() > 0) { + const auto scripts = split(bridge->userConfig["webview_service-workers"], " "); + for (const auto& value : scripts) { + auto parts = split(value, "/"); + parts = Vector<String>(parts.begin(), parts.end() - 1); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + auto scriptURL = String("https://"); + #else + auto scriptURL = String("socket://"); + #endif + scriptURL += bridge->userConfig["meta_bundle_identifier"]; + + if (!value.starts_with("/")) { + scriptURL += "/"; + } + + scriptURL += value; + + const auto scope = normalizeScope(join(parts, "/")); + const auto id = rand64(); + this->registrations.insert_or_assign(scope, Registration { + id, + scriptURL, + Registration::State::Registered, + RegistrationOptions { + RegistrationOptions::Type::Module, + scope, + scriptURL, + "*", + id + } + }); + } + } + + for (const auto& entry : bridge->userConfig) { + const auto& key = entry.first; + const auto& value = entry.second; + + if (key.starts_with("webview_service-workers_")) { + const auto id = rand64(); + const auto scope = normalizeScope(replace(key, "webview_service-workers_", "")); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + auto scriptURL = String("https://"); + #else + auto scriptURL = String("socket://"); + #endif + scriptURL += bridge->userConfig["meta_bundle_identifier"]; + + if (!value.starts_with("/")) { + scriptURL += "/"; + } + + scriptURL += trim(value); + + this->registrations.insert_or_assign(scope, Registration { + id, + scriptURL, + Registration::State::Registered, + RegistrationOptions { + RegistrationOptions::Type::Module, + scope, + scriptURL, + "*", + id + } + }); + } + } + + for (const auto& entry : this->bridge->userConfig) { + const auto& key = entry.first; + if (key.starts_with("webview_protocol-handlers_")) { + const auto scheme = replace(replace(trim(key), "webview_protocol-handlers_", ""), ":", "");; + auto value = entry.second; + if (value.starts_with(".") || value.starts_with("/")) { + if (value.starts_with(".")) { + value = value.substr(1, value.size()); + } + + const auto id = rand64(); + auto parts = split(value, "/"); + parts = Vector<String>(parts.begin(), parts.end() - 1); + auto scope = normalizeScope(join(parts, "/")); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + auto scriptURL = String("https://"); + #else + auto scriptURL = String("socket://"); + #endif + + scriptURL += bridge->userConfig["meta_bundle_identifier"]; + + if (!value.starts_with("/")) { + scriptURL += "/"; + } + + scriptURL += trim(value); + + this->registrations.insert_or_assign(scope, Registration { + id, + scriptURL, + Registration::State::Registered, + RegistrationOptions { + RegistrationOptions::Type::Module, + scope, + scriptURL, + scheme, + id + } + }); + } + } + } + + this->bridge->router.map("serviceWorker.fetch.request.body", [this](auto message, auto router, auto reply) mutable { + FetchRequest request; + ID id = 0; + + try { + id = std::stoull(message.get("id")); + } catch (...) { + return reply(IPC::Result::Err { message, JSON::Object::Entries { + {"message", "Invalid 'id' given in parameters"} + }}); + } + + do { + Lock lock(this->mutex); + + if (!this->fetchRequests.contains(id)) { + return reply(IPC::Result::Err { message, JSON::Object::Entries { + {"type", "NotFoundError"}, + {"message", "Callback 'id' given in parameters does not have a 'FetchRequest'"} + }}); + } + + request = this->fetchRequests.at(id); + } while (0); + + const auto post = Post { + 0, + 0, + request.body.bytes, + request.body.size + }; + + reply(IPC::Result { message.seq, message, JSON::Object {}, post }); + }); + + this->bridge->router.map("serviceWorker.fetch.response", [this](auto message, auto router, auto reply) mutable { + FetchCallback callback; + FetchRequest request; + ID clientId = 0; + ID id = 0; + + int statusCode = 200; + + try { + id = std::stoull(message.get("id")); + } catch (...) { + return reply(IPC::Result::Err { message, JSON::Object::Entries { + {"message", "Invalid 'id' given in parameters"} + }}); + } + + try { + clientId = std::stoull(message.get("clientId")); + } catch (...) { + return reply(IPC::Result::Err { message, JSON::Object::Entries { + {"message", "Invalid 'clientId' given in parameters"} + }}); + } + + do { + Lock lock(this->mutex); + if (!this->fetchCallbacks.contains(id)) { + return reply(IPC::Result::Err { message, JSON::Object::Entries { + {"type", "NotFoundError"}, + {"message", "Callback 'id' given in parameters does not have a 'FetchCallback'"} + }}); + } + + if (!this->fetchRequests.contains(id)) { + return reply(IPC::Result::Err { message, JSON::Object::Entries { + {"type", "NotFoundError"}, + {"message", "Callback 'id' given in parameters does not have a 'FetchRequest'"} + }}); + } + + callback = this->fetchCallbacks.at(id); + request = this->fetchRequests.at(id); + } while (0); + + try { + statusCode = std::stoi(message.get("statusCode")); + } catch (...) { + return reply(IPC::Result::Err { message, JSON::Object::Entries { + {"message", "Invalid 'statusCode' given in parameters"} + }}); + } + + const auto headers = Headers(message.get("headers")); + auto response = FetchResponse { + id, + statusCode, + headers, + { message.buffer.size, message.buffer.bytes }, + { clientId } + }; + + // XXX(@jwerle): we handle this in the android runtime + const auto extname = Path(request.pathname).extension().string(); + auto html = (message.buffer.bytes != nullptr && message.buffer.size > 0) + ? String(response.body.bytes.get(), response.body.size) + : String(""); + + if ( + html.size() > 0 && + message.get("runtime-preload-injection") != "disabled" && + ( + message.get("runtime-preload-injection") == "always" || + (extname.ends_with("html") || headers.get("content-type").value.string == "text/html") || + (html.find("<!doctype html") != String::npos || html.find("<!DOCTYPE HTML") != String::npos) || + (html.find("<html") != String::npos || html.find("<HTML") != String::npos) || + (html.find("<body") != String::npos || html.find("<BODY") != String::npos) || + (html.find("<head") != String::npos || html.find("<HEAD") != String::npos) || + (html.find("<script") != String::npos || html.find("<SCRIPT") != String::npos) + ) + ) { + auto preloadOptions = request.client.preload.options; + preloadOptions.metadata["runtime-frame-source"] = "serviceworker"; + + auto preload = IPC::Preload::compile(preloadOptions); + auto begin = String("<meta name=\"begin-runtime-preload\">"); + auto end = String("<meta name=\"end-runtime-preload\">"); + auto x = html.find(begin); + auto y = html.find(end); + + if (x != String::npos && y != String::npos) { + html.erase(x, (y - x) + end.size()); + } + + html = preload.insertIntoHTML(html, { + .protocolHandlerSchemes = this->protocols.getSchemes() + }); + + response.body.bytes = std::make_shared<char[]>(html.size()); + response.body.size = html.size(); + + memcpy(response.body.bytes.get(), html.c_str(), html.size()); + } + + callback(response); + reply(IPC::Result { message.seq, message }); + + Lock lock(this->mutex); + this->fetchCallbacks.erase(id); + if (this->fetchRequests.contains(id)) { + this->fetchRequests.erase(id); + } + }); + } + + const ServiceWorkerContainer::Registration& ServiceWorkerContainer::registerServiceWorker ( + const RegistrationOptions& options + ) { + Lock lock(this->mutex); + auto scope = options.scope; + auto scriptURL = options.scriptURL; + + if (scope.size() == 0) { + auto tmp = options.scriptURL; + tmp = replace(tmp, "https://", ""); + tmp = replace(tmp, "socket://", ""); + tmp = replace(tmp, this->bridge->userConfig["meta_bundle_identifier"], ""); + + auto parts = split(tmp, "/"); + parts = Vector<String>(parts.begin(), parts.end() - 1); + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + scriptURL = String("https://"); + #else + scriptURL = String("socket://"); + #endif + + scriptURL += bridge->userConfig["meta_bundle_identifier"]; + scriptURL += tmp; + + scope = join(parts, "/"); + } + + scope = normalizeScope(scope); + + if (this->registrations.contains(scope)) { + const auto& registration = this->registrations.at(scope); + + if (this->bridge != nullptr) { + this->bridge->emit("serviceWorker.register", registration.json().str()); + } + + return registration; + } + + const auto id = options.id > 0 ? options.id : rand64(); + this->registrations.insert_or_assign(scope, Registration { + id, + options.scriptURL, + Registration::State::Registered, + RegistrationOptions { + options.type, + scope, + options.scriptURL, + options.scheme, + id + } + }); + + const auto& registration = this->registrations.at(scope); + + if (this->bridge != nullptr) { + App::sharedApplication()->core->setImmediate([&, this]() { + this->bridge->emit("serviceWorker.register", registration.json().str()); + }); + } + + return registration; + } + + bool ServiceWorkerContainer::unregisterServiceWorker (String scopeOrScriptURL) { + Lock lock(this->mutex); + + const auto& scope = normalizeScope(scopeOrScriptURL); + const auto& scriptURL = scopeOrScriptURL; + + if (this->registrations.contains(scope)) { + const auto& registration = this->registrations.at(scope); + if (this->bridge != nullptr) { + return this->bridge->emit("serviceWorker.unregister", registration.json().str()); + } + + this->registrations.erase(scope); + return true; + } + + for (const auto& entry : this->registrations) { + if (entry.second.scriptURL == scriptURL) { + const auto& registration = this->registrations.at(entry.first); + + if (this->bridge != nullptr) { + return this->bridge->emit("serviceWorker.unregister", registration.json().str()); + } + + this->registrations.erase(entry.first); + } + } + + return false; + } + + bool ServiceWorkerContainer::unregisterServiceWorker (ID id) { + Lock lock(this->mutex); + + for (const auto& entry : this->registrations) { + if (entry.second.id == id) { + return this->unregisterServiceWorker(entry.first); + } + } + + return false; + } + + void ServiceWorkerContainer::skipWaiting (ID id) { + Lock lock(this->mutex); + + for (auto& entry : this->registrations) { + if (entry.second.id == id) { + auto& registration = entry.second; + if ( + registration.state == Registration::State::Installing || + registration.state == Registration::State::Installed + ) { + registration.state = Registration::State::Activating; + + if (this->bridge != nullptr) { + this->bridge->emit("serviceWorker.skipWaiting", registration.json().str()); + } + } + break; + } + } + } + + void ServiceWorkerContainer::updateState (ID id, const String& stateString) { + Lock lock(this->mutex); + + for (auto& entry : this->registrations) { + if (entry.second.id == id) { + auto& registration = entry.second; + if (stateString == "error") { + registration.state = Registration::State::Error; + } else if (stateString == "registered") { + registration.state = Registration::State::Registered; + } else if (stateString == "installing") { + registration.state = Registration::State::Installing; + } else if (stateString == "installed") { + registration.state = Registration::State::Installed; + } else if (stateString == "activating") { + registration.state = Registration::State::Activating; + } else if (stateString == "activated") { + registration.state = Registration::State::Activated; + } else { + break; + } + + if (this->bridge != nullptr) { + this->bridge->emit("serviceWorker.updateState", registration.json().str()); + } + + break; + } + } + } + + bool ServiceWorkerContainer::fetch (const FetchRequest& request, FetchCallback callback) { + Lock lock(this->mutex); + String pathname = request.pathname; + String scope; + + if (this->bridge == nullptr || !this->isReady) { + return false; + } + + if (request.headers.get("runtime-serviceworker-fetch-mode") == "ignore") { + return false; + } + + // TODO(@jwerle): this prevents nested service worker fetches - do we want to prevent this? + if (request.headers.get("runtime-worker-type") == "serviceworker") { + return false; + } + + for (const auto& entry : this->registrations) { + const auto& registration = entry.second; + + if ( + (registration.options.scheme == "*" || registration.options.scheme == request.scheme) && + request.pathname.starts_with(registration.options.scope) + ) { + if (entry.first.size() > scope.size()) { + scope = entry.first; + } + } + } + + if (scope.size() == 0) { + return false; + } + + scope = normalizeScope(scope); + + if (scope.size() > 0 && this->registrations.contains(scope)) { + auto& registration = this->registrations.at(scope); + if ( + !registration.isActive() && + ( + registration.options.scheme == "*" || + registration.options.scheme == request.scheme + ) && + ( + registration.state == Registration::State::Registering || + registration.state == Registration::State::Registered + ) + ) { + this->core->dispatchEventLoop([this, request, callback, ®istration]() { + const auto interval = this->core->setInterval(8, [this, request, callback, ®istration] (auto cancel) { + if (registration.state == Registration::State::Activated) { + cancel(); + this->core->setTimeout(8, [this, request, callback, ®istration] { + if (!this->fetch(request, callback)) { + debug( + #if SOCKET_RUNTIME_PLATFORM_APPLE + "ServiceWorkerContainer: Failed to dispatch fetch request '%s %s%s' for client '%llu'", + #else + "ServiceWorkerContainer: Failed to dispatch fetch request '%s %s%s' for client '%lu'", + #endif + request.method.c_str(), + request.pathname.c_str(), + (request.query.size() > 0 ? String("?") + request.query : String("")).c_str(), + request.client.id + ); + } + }); + } + }); + + this->core->setTimeout(32000, [this, interval] { + this->core->clearInterval(interval); + }); + }); + + return true; + } + + // the ID of the fetch request + const auto id = rand64(); + const auto client = JSON::Object::Entries { + {"id", std::to_string(request.client.id)} + }; + + if (this->protocols.hasHandler(request.scheme)) { + pathname = replace(pathname, scope, ""); + } + + const auto fetch = JSON::Object::Entries { + {"id", std::to_string(id)}, + {"method", request.method}, + {"host", request.hostname}, + {"scheme", request.scheme}, + {"pathname", pathname}, + {"query", request.query}, + {"headers", request.headers.json()}, + {"client", client} + }; + + auto json = registration.json(); + json.set("fetch", fetch); + + this->fetchCallbacks.insert_or_assign(id, callback); + this->fetchRequests.insert_or_assign(id, request); + + return this->bridge->emit("serviceWorker.fetch", json.str()); + } + + return false; + } +} diff --git a/src/serviceworker/container.hh b/src/serviceworker/container.hh new file mode 100644 index 0000000000..3b74079aa8 --- /dev/null +++ b/src/serviceworker/container.hh @@ -0,0 +1,134 @@ +#ifndef SOCKET_RUNTIME_SERVICE_WORKER_CONTAINER_H +#define SOCKET_RUNTIME_SERVICE_WORKER_CONTAINER_H + +#include "../core/headers.hh" +#include "../core/json.hh" +#include "../ipc/client.hh" + +#include "protocols.hh" + +namespace SSC { + // forward + namespace IPC { class Bridge; } + class Core; + + class ServiceWorkerContainer { + public: + using ID = IPC::Client::ID; + using Client = IPC::Client; + + struct RegistrationOptions { + enum class Type { Classic, Module }; + + Type type = Type::Module; + String scope; + String scriptURL; + String scheme = "*"; + ID id = 0; + }; + + struct Registration { + enum class State { + None, + Error, + Registering, + Registered, + Installing, + Installed, + Activating, + Activated + }; + + struct Storage { + Map data; + const JSON::Object json () const; + void set (const String& key, const String& value); + const String get (const String& key) const; + void remove (const String& key); + void clear (); + }; + + ID id = 0; + String scriptURL; + Storage storage; + Atomic<State> state = State::None; + RegistrationOptions options; + + Registration ( + const ID id, + const String& scriptURL, + const State state, + const RegistrationOptions& options + ); + + Registration (const Registration&); + Registration (Registration&&); + ~Registration () = default; + Registration& operator= (const Registration&); + Registration& operator= (Registration&&); + + const JSON::Object json () const; + bool isActive () const; + bool isWaiting () const; + bool isInstalling () const; + const String getStateString () const; + }; + + struct FetchBody { + size_t size = 0; + SharedPointer<char[]> bytes = nullptr; + }; + + struct FetchRequest { + String method; + String scheme = "*"; + String hostname = ""; + String pathname; + String query; + Headers headers; + FetchBody body; + Client client; + + const String str () const; + }; + + struct FetchResponse { + ID id = 0; + int statusCode = 200; + Headers headers; + FetchBody body; + Client client; + }; + + using Registrations = std::map<String, Registration>; + using FetchCallback = std::function<void(const FetchResponse)>; + using FetchCallbacks = std::map<ID, FetchCallback>; + using FetchRequests = std::map<ID, FetchRequest>; + + SharedPointer<Core> core = nullptr; + IPC::Bridge* bridge = nullptr; + AtomicBool isReady = false; + Mutex mutex; + + ServiceWorkerProtocols protocols; + Registrations registrations; + + FetchRequests fetchRequests; + FetchCallbacks fetchCallbacks; + + static ServiceWorkerContainer* sharedContainer (); + ServiceWorkerContainer (SharedPointer<Core> core); + ~ServiceWorkerContainer (); + + void init (IPC::Bridge* bridge); + void reset (); + const Registration& registerServiceWorker (const RegistrationOptions& options); + bool unregisterServiceWorker (ID id); + bool unregisterServiceWorker (String scopeOrScriptURL); + void skipWaiting (ID id); + void updateState (ID id, const String& stateString); + bool fetch (const FetchRequest& request, FetchCallback callback); + bool claimClients (const String& scope); + }; +} +#endif diff --git a/src/serviceworker/protocols.cc b/src/serviceworker/protocols.cc new file mode 100644 index 0000000000..67df2c78b2 --- /dev/null +++ b/src/serviceworker/protocols.cc @@ -0,0 +1,104 @@ +#include "container.hh" +#include "protocols.hh" +#include "../core/debug.hh" + +namespace SSC { + static Vector<String> reserved = { + "socket", + "ipc", + "node", + "npm" + }; + + ServiceWorkerProtocols::ServiceWorkerProtocols ( + ServiceWorkerContainer* serviceWorkerContainer + ) + : serviceWorkerContainer(serviceWorkerContainer) + {} + + ServiceWorkerProtocols::~ServiceWorkerProtocols () {} + + bool ServiceWorkerProtocols::registerHandler (const String& scheme, const Data data) { + Lock lock(this->mutex); + + if (scheme.size() == 0) { + return false; + } + + if (std::find(reserved.begin(), reserved.end(), scheme) != reserved.end()) { + return false; + } + + if (this->mapping.contains(scheme)) { + return false; + } + + this->mapping.insert_or_assign(scheme, Protocol { scheme, data }); + return true; + } + + bool ServiceWorkerProtocols::unregisterHandler (const String& scheme) { + Lock lock(this->mutex); + + if (!this->mapping.contains(scheme)) { + return false; + } + + this->mapping.erase(scheme); + return true; + } + + const ServiceWorkerProtocols::Data ServiceWorkerProtocols::getHandlerData (const String& scheme) { + Lock lock(this->mutex); + + if (!this->mapping.contains(scheme)) { + return Data {}; + } + + return this->mapping.at(scheme).data; + } + + bool ServiceWorkerProtocols::setHandlerData (const String& scheme, const Data data) { + Lock lock(this->mutex); + + if (!this->mapping.contains(scheme)) { + return false; + } + + this->mapping.at(scheme).data = data; + return true; + } + + bool ServiceWorkerProtocols::hasHandler (const String& scheme) { + Lock lock(this->mutex); + return this->mapping.contains(scheme); + } + + const String ServiceWorkerProtocols::getServiceWorkerScope (const String& scheme) { + for (const auto& entry : this->serviceWorkerContainer->registrations) { + const auto& scope = entry.first; + const auto& registration = entry.second; + if (registration.options.scheme == scheme) { + return scope; + } + } + + return ""; + } + + const Vector<String> ServiceWorkerProtocols::getSchemes () const { + Vector<String> schemes; + for (const auto& entry : this->mapping) { + schemes.push_back(entry.first + ":"); + } + return schemes; + } + + const Vector<String> ServiceWorkerProtocols::getProtocols () const { + Vector<String> protocols; + for (const auto& entry : this->mapping) { + protocols.push_back(entry.first); + } + return protocols; + } +} diff --git a/src/serviceworker/protocols.hh b/src/serviceworker/protocols.hh new file mode 100644 index 0000000000..35191871e4 --- /dev/null +++ b/src/serviceworker/protocols.hh @@ -0,0 +1,38 @@ +#ifndef SOCKET_RUNTIME_SERVICE_WORKER_PROTOCOLS_H +#define SOCKET_RUNTIME_SERVICE_WORKER_PROTOCOLS_H + +#include "../core/json.hh" + +namespace SSC { + class ServiceWorkerContainer; + class ServiceWorkerProtocols { + public: + struct Data { + String json = ""; // we store JSON as a string here + }; + + struct Protocol { + String scheme; + Data data; + }; + + using Mapping = std::map<String, Protocol>; + + ServiceWorkerContainer* serviceWorkerContainer; + Mapping mapping; + Mutex mutex; + + ServiceWorkerProtocols (ServiceWorkerContainer* serviceWorkerContainer); + ~ServiceWorkerProtocols (); + + bool registerHandler (const String& scheme, const Data data = { "" }); + bool unregisterHandler (const String& scheme); + const Data getHandlerData (const String& scheme); + bool setHandlerData (const String& scheme, const Data data = { "" }); + bool hasHandler (const String& scheme); + const String getServiceWorkerScope (const String& scheme); + const Vector<String> getSchemes () const; + const Vector<String> getProtocols () const; + }; +} +#endif diff --git a/src/window/android.cc b/src/window/android.cc new file mode 100644 index 0000000000..bf3924a1d7 --- /dev/null +++ b/src/window/android.cc @@ -0,0 +1,608 @@ +#include "../app/app.hh" + +#include "window.hh" + +using namespace SSC; + +namespace SSC { + Window::Window (SharedPointer<Core> core, const Window::Options& options) + : options(options), + core(core), + bridge(core, IPC::Bridge::Options { + options.userConfig, + options.as<IPC::Preload::Options>() + }), + hotkey(this), + dialog(this) + { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + + this->index = this->options.index; + this->bridge.userConfig = options.userConfig; + this->bridge.configureNavigatorMounts(); + + this->bridge.navigateFunction = [this] (const auto url) { + this->navigate(url); + }; + + this->bridge.evaluateJavaScriptFunction = [this] (const auto source) { + this->eval(source); + }; + + this->bridge.client.preload = IPC::Preload::compile({ + .client = UniqueClient { + .id = this->bridge.client.id, + .index = this->bridge.client.index + }, + .index = options.index, + .conduit = this->core->conduit.port, + .userScript = options.userScript + }); + + // `activity.createWindow(index, shouldExitApplicationOnClose): Unit` + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + app->activity, + "createWindow", + "(IZZ)V", + options.index, + options.shouldExitApplicationOnClose, + options.headless + ); + + this->hotkey.init(); + this->bridge.init(); + this->bridge.configureSchemeHandlers({}); + } + + Window::~Window () { + if (this->androidWindowRef) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + attachment.env->DeleteGlobalRef(this->androidWindowRef); + this->androidWindowRef = nullptr; + } + + this->close(); + } + + ScreenSize Window::getScreenSize () { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.getWindowWidth(index): String` + const auto width = CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + app->activity, + "getScreenSizeWidth", + "()I" + ); + + const auto height = CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + app->activity, + "getScreenSizeHeight", + "()I" + ); + + return ScreenSize {width, height}; + } + + void Window::about () { + // XXX(@jwerle): not supported + // TODO(@jwerle): figure out we'll go about making this possible + } + + void Window::eval (const String& source, const EvalCallback& callback) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + const auto token = std::to_string(rand64()); + + // `activity.evaluateJavaScript(index, source): Boolean` + CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + app->activity, + "evaluateJavaScript", + "(ILjava/lang/String;Ljava/lang/String;)Z", + this->index, + attachment.env->NewStringUTF(source.c_str()), + attachment.env->NewStringUTF(token.c_str()) + ); + + this->evaluateJavaScriptCallbacks.insert_or_assign(token, callback); + } + + void Window::show () { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + + // `activity.showWindow(index): Boolean` + CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + app->activity, + "showWindow", + "(I)Z", + this->index + ); + } + + void Window::hide () { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + + // `activity.hideWindow(index): Boolean` + CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + app->activity, + "hideWindow", + "(I)Z", + this->index + ); + } + + void Window::kill () { + return this->close(0); + } + + void Window::exit (int code) { + return this->close(code); + } + + void Window::close (int code) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + + // `activity.closeWindow(index): Boolean` + CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + app->activity, + "closeWindow", + "(I)Z", + this->index + ); + } + + void Window::minimize () { + this->hide(); + } + + void Window::maximize () { + this->show(); + } + + void Window::restore () { + this->show(); + } + + void Window::navigate (const String& url) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + + // `activity.navigateWindow(index, url): Boolean` + const auto result = CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + app->activity, + "navigateWindow", + "(ILjava/lang/String;)Z", + this->index, + attachment.env->NewStringUTF(url.c_str()) + ); + + if (!result) { + this->pendingNavigationLocation = url; + } + } + + const String Window::getTitle () const { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.getWindowTitle(index): String` + const auto titleString = (jstring) CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + app->activity, + "getWindowTitle", + "(I)Ljava/lang/String;", + this->index + ); + + return Android::StringWrap(attachment.env, titleString).str(); + } + + void Window::setTitle (const String& title) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.setWindowTitle(index, url): Boolean` + CallClassMethodFromAndroidEnvironment( + attachment.env, + Boolean, + app->activity, + "setWindowTitle", + "(ILjava/lang/String;)Z", + this->index, + attachment.env->NewStringUTF(title.c_str()) + ); + } + + Window::Size Window::getSize () { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.getWindowWidth(index): String` + const auto width = CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + app->activity, + "getWindowWidth", + "(I)I", + this->index + ); + + const auto height = CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + app->activity, + "getWindowHeight", + "(I)I", + this->index + ); + this->size.width = width; + this->size.height = height; + return Size {width, height}; + } + + const Window::Size Window::getSize () const { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.getWindowWidth(index): Int` + const auto width = CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + app->activity, + "getWindowWidth", + "(I)I", + this->index + ); + + // `activity.getWindowHeight(index): Int` + const auto height = CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + app->activity, + "getWindowHeight", + "(I)I", + this->index + ); + return Size {width, height}; + } + + void Window::setSize (int width, int height, int _) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.setWindowSize(index, w): Int` + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + app->activity, + "setWindowSize", + "(III)Z", + this->index, + width, + height + ); + } + + void Window::setPosition (float x, float y) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + this->position.x = x; + this->position.y = y; + // `activity.setWindowBackgroundColor(index, color)` + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + app->activity, + "setWindowPosition", + "(IFF)Z", + this->index, + x, + y + ); + } + + void Window::setContextMenu (const String&, const String&) { + // XXX(@jwerle): not supported + } + + void Window::closeContextMenu (const String&) { + // XXX(@jwerle): not supported + } + + void Window::closeContextMenu () { + // XXX(@jwerle): not supported + } + + void Window::setBackgroundColor (int r, int g, int b, float a) { + const auto app = App::sharedApplication(); + const auto color = Color(r, g, b, a); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.setWindowBackgroundColor(index, color)` + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + app->activity, + "setWindowBackgroundColor", + "(IJ)Z", + this->index, + color.pack() + ); + } + + void Window::setBackgroundColor (const String& rgba) { + const auto app = App::sharedApplication(); + const auto color = Color(rgba); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.setWindowBackgroundColor(index, color)` + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + app->activity, + "setWindowBackgroundColor", + "(IJ)Z", + this->index, + color.pack() + ); + } + + void Window::setBackgroundColor (const Color& color) { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.setWindowBackgroundColor(index, color)` + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + app->activity, + "setWindowBackgroundColor", + "(IJ)Z", + this->index, + color.pack() + ); + } + + String Window::getBackgroundColor () { + const auto app = App::sharedApplication(); + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + // `activity.getWindowBackgroundColor(index): Int` + const auto color = Color(CallClassMethodFromAndroidEnvironment( + attachment.env, + Int, + app->activity, + "getWindowBackgroundColor", + "(I)I", + this->index + )); + + return color.str(); + } + + void Window::setSystemMenuItemEnabled (bool enabled, int barPos, int menuPos) { + // XXX(@jwerle): not supported + } + + void Window::setSystemMenu (const String& dsl) { + // XXX(@jwerle): not supported + } + + void Window::setMenu (const String& dsl, const bool& isTrayMenu) { + // XXX(@jwerle): not supported + } + + void Window::setTrayMenu (const String& dsl) { + // XXX(@jwerle): not supported + } + + void Window::showInspector () { + // XXX(@jwerle): not supported + } + + void Window::handleApplicationURL (const String& url) { + JSON::Object json = JSON::Object::Entries {{ + "url", url + }}; + + this->bridge.emit("applicationurl", json.str()); + } +} + +extern "C" { + void ANDROID_EXTERNAL(window, Window, onMessage) ( + JNIEnv* env, + jobject self, + jint index, + jstring messageString, + jbyteArray byteArray + ) { + const auto app = App::sharedApplication(); + + if (!app) { + return ANDROID_THROW(env, "Missing 'App' in environment"); + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + return ANDROID_THROW(env, "Invalid window index (%d) requested", index); + } + + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + const auto message = Android::StringWrap(attachment.env, messageString).str(); + const auto size = byteArray != nullptr ? attachment.env->GetArrayLength(byteArray) : 0; + + if (byteArray && size > 0) { + auto bytes = std::make_shared<char[]>(size); + attachment.env->GetByteArrayRegion(byteArray, 0, size, (jbyte*) bytes.get()); + window->bridge.route(message, bytes, size); + } else { + window->bridge.route(message, nullptr, 0); + } + } + + void ANDROID_EXTERNAL(window, Window, onReady) ( + JNIEnv* env, + jobject self, + jint index + ) { + const auto app = App::sharedApplication(); + + if (!app) { + return ANDROID_THROW(env, "Missing 'App' in environment"); + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + return ANDROID_THROW(env, "Invalid window index (%d) requested", index); + } + + window->androidWindowRef = env->NewGlobalRef(self); + } + + void ANDROID_EXTERNAL(window, Window, onEvaluateJavascriptResult) ( + JNIEnv* env, + jobject self, + jint index, + jstring tokenString, + jstring resultString + ) { + const auto app = App::sharedApplication(); + + if (!app) { + return ANDROID_THROW(env, "Missing 'App' in environment"); + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + return ANDROID_THROW(env, "Invalid window index (%d) requested", index); + } + + const auto token = Android::StringWrap(env, tokenString).str(); + const auto result = Android::StringWrap(env, resultString).str(); + + Lock lock(window->mutex); + + if (!window->evaluateJavaScriptCallbacks.contains(token)) { + return; + } + + const auto callback = window->evaluateJavaScriptCallbacks.at(token); + + if (callback != nullptr) { + if (result == "null" || result == "undefined") { + callback(nullptr); + } else if (result == "true") { + callback(true); + } else if (result == "false") { + callback(false); + } else { + double number = 0.0f; + + try { + number = std::stod(result); + } catch (...) { + callback(result); + return; + } + + callback(number); + } + } + } + + jstring ANDROID_EXTERNAL(window, Window, getPendingNavigationLocation) ( + JNIEnv* env, + jobject self, + jint index + ) { + const auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return nullptr; + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + ANDROID_THROW(env, "Invalid window index (%d) requested", index); + return nullptr; + } + + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + const auto pendingNavigationLocation = window->pendingNavigationLocation; + window->pendingNavigationLocation = ""; + return attachment.env->NewStringUTF(pendingNavigationLocation.c_str()); + } + + jstring ANDROID_EXTERNAL(window, Window, getPreloadUserScript) ( + JNIEnv* env, + jobject self, + jint index + ) { + const auto app = App::sharedApplication(); + + if (!app) { + ANDROID_THROW(env, "Missing 'App' in environment"); + return nullptr; + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + ANDROID_THROW(env, "Invalid window index (%d) requested", index); + return nullptr; + } + + auto preloadUserScriptSource = IPC::Preload::compile({ + .features = IPC::Preload::Options::Features { + .useGlobalCommonJS = false, + .useGlobalNodeJS = false, + .useTestScript = false, + .useHTMLMarkup = false, + .useESM = false, + .useGlobalArgs = true + }, + .client = UniqueClient { + .id = window->bridge.client.id, + .index = window->bridge.client.index + }, + .index = window->options.index, + .conduit = window->core->conduit.port, + .userScript = window->options.userScript + }); + + return env->NewStringUTF(preloadUserScriptSource.compile().c_str()); + } + + void ANDROID_EXTERNAL(window, Window, handleApplicationURL) ( + JNIEnv* env, + jobject self, + jint index, + jstring urlString + ) { + const auto app = App::sharedApplication(); + + if (!app) { + return ANDROID_THROW(env, "Missing 'App' in environment"); + } + + const auto window = app->windowManager.getWindow(index); + + if (!window) { + return ANDROID_THROW(env, "Invalid window index (%d) requested", index); + } + + const auto url = Android::StringWrap(env, urlString).str(); + window->handleApplicationURL(url); + } +} diff --git a/src/window/apple.mm b/src/window/apple.mm index c90b435a28..ef07e7bdbd 100644 --- a/src/window/apple.mm +++ b/src/window/apple.mm @@ -1,778 +1,283 @@ #include "window.hh" +#include "../app/app.hh" +#include "../cli/cli.hh" #include "../ipc/ipc.hh" +#include "../core/webview.hh" -@implementation SSCNavigationDelegate -- (void) webView: (WKWebView*) webview - decidePolicyForNavigationAction: (WKNavigationAction*) navigationAction - decisionHandler: (void (^)(WKNavigationActionPolicy)) decisionHandler { - if ( - webview.URL.absoluteString.UTF8String != nullptr && - navigationAction.request.URL.absoluteString.UTF8String != nullptr - ) { - static auto userConfig = SSC::getUserConfig(); - static const auto devHost = SSC::getDevHost(); - static const auto links = SSC::parseStringList(userConfig["meta_application_links"], ' '); - - auto base = SSC::String(webview.URL.absoluteString.UTF8String); - auto request = SSC::String(navigationAction.request.URL.absoluteString.UTF8String); - - const auto applinks = SSC::parseStringList(userConfig["meta_application_links"], ' '); - bool hasAppLink = false; - - if (applinks.size() > 0 && navigationAction.request.URL.host != nullptr) { - auto host = SSC::String(navigationAction.request.URL.host.UTF8String); - for (const auto& applink : applinks) { - const auto parts = SSC::split(applink, '?'); - if (host == parts[0]) { - hasAppLink = true; - break; - } - } - } +using namespace SSC; - if (hasAppLink) { - if (self.bridge != nullptr) { - decisionHandler(WKNavigationActionPolicyCancel); - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ - "url", request - }}; +#if SOCKET_RUNTIME_PLATFORM_IOS +static dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_CONCURRENT, + QOS_CLASS_USER_INITIATED, + -1 +); - self.bridge->router.emit("applicationurl", json.str()); - return; +static dispatch_queue_t queue = dispatch_queue_create( + "socket.runtime.app.queue", + qos +); +#endif + +@implementation SSCWindow +#if SOCKET_RUNTIME_PLATFORM_MACOS +CGFloat MACOS_TRAFFIC_LIGHT_BUTTON_SIZE = 16; + - (void) layoutIfNeeded { + [super layoutIfNeeded]; + + if (self.titleBarView && self.titleBarView.subviews.count == 0) { + const auto minimizeButton = [self standardWindowButton: NSWindowMiniaturizeButton]; + const auto closeButton = [self standardWindowButton: NSWindowCloseButton]; + const auto zoomButton = [self standardWindowButton: NSWindowZoomButton]; + + if (closeButton && minimizeButton && zoomButton) { + [self.titleBarView addSubview: closeButton]; + [self.titleBarView addSubview: minimizeButton]; + [self.titleBarView addSubview: zoomButton]; } } + } - if ( - userConfig["meta_application_protocol"].size() > 0 && - request.starts_with(userConfig["meta_application_protocol"]) - ) { - if (self.bridge != nullptr) { - decisionHandler(WKNavigationActionPolicyCancel); - - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ - "url", request - }}; + - (void) sendEvent: (NSEvent*) event { + if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeLeftMouseDragged) { + if (event.type == NSEventTypeLeftMouseDown) { + [self.webview mouseDown: event]; + } - self.bridge->router.emit("applicationurl", json.str()); + if (event.type == NSEventTypeLeftMouseDragged) { + [self.webview mouseDragged: event]; return; } } - if (request.starts_with("socket:")) { - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - - if (request.starts_with(devHost)) { - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - - decisionHandler(WKNavigationActionPolicyCancel); - return; + [super sendEvent: event]; } - - decisionHandler(WKNavigationActionPolicyAllow); -} - -- (void) webView: (WKWebView*) webView - decidePolicyForNavigationResponse: (WKNavigationResponse*) navigationResponse - decisionHandler: (void (^)(WKNavigationResponsePolicy)) decisionHandler { - decisionHandler(WKNavigationResponsePolicyAllow); -} +#endif @end -#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR -@implementation SSCWindowDelegate -@end -#else @implementation SSCWindowDelegate -- (void) userContentController: (WKUserContentController*) userContentController didReceiveScriptMessage: (WKScriptMessage*) scriptMessage {} -@end -#endif -@implementation SSCBridgedWebView -#if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR -SSC::Vector<SSC::String> draggablePayload; - -int lastX = 0; -int lastY = 0; - -- (BOOL) wantsPeriodicDraggingUpdates { - return YES; -} - -- (BOOL) prepareForDragOperation: (id<NSDraggingInfo>) info { - [info setDraggingFormation: NSDraggingFormationNone]; - return YES; -} - -- (BOOL) performDragOperation: (id<NSDraggingInfo>) info { - return YES; -} - -- (void) concludeDragOperation: (id<NSDraggingInfo>) info { -} - -- (void) updateDraggingItemsForDrag: (id<NSDraggingInfo>) info { -} - -- (NSDragOperation) draggingEntered: (id<NSDraggingInfo>) info { - const auto json = SSC::JSON::Object {}; - const auto payload = SSC::getEmitToRenderProcessJavaScript("dragenter", json.str()); - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; - [self draggingUpdated: info]; - return NSDragOperationGeneric; -} - -- (NSDragOperation) draggingUpdated: (id<NSDraggingInfo>) info { - const auto position = info.draggingLocation; - const auto x = std::to_string(position.x); - const auto y = std::to_string(self.frame.size.height - position.y); - - auto count = draggablePayload.size(); - auto inbound = false; - - if (count == 0) { - inbound = true; - count = [info numberOfValidItemsForDrop]; - } - - const auto data = SSC::JSON::Object::Entries { - {"count", (unsigned int) count}, - {"inbound", inbound}, - {"x", x}, - {"y", y} - }; - - const auto json = SSC::JSON::Object {data}; - const auto payload = SSC::getEmitToRenderProcessJavaScript("drag", json.str()); - - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; - return [super draggingUpdated: info]; -} - -- (void) draggingExited: (id<NSDraggingInfo>) info { - const auto position = info.draggingLocation; - const auto x = std::to_string(position.x); - const auto y = std::to_string(self.frame.size.height - position.y); - - const auto data = SSC::JSON::Object::Entries { - {"x", x}, - {"y", y} - }; - - const auto json = SSC::JSON::Object {data}; - const auto payload = SSC::getEmitToRenderProcessJavaScript("dragend", json.str()); - - draggablePayload.clear(); - - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; -} - -- (void) draggingEnded: (id<NSDraggingInfo>) info { - const auto pasteboard = info.draggingPasteboard; - const auto position = info.draggingLocation; - const auto x = position.x; - const auto y = self.frame.size.height - position.y; - - const auto pasteboardFiles = [pasteboard - readObjectsForClasses: @[NSURL.class] - options: @{} - ]; - - auto files = SSC::JSON::Array::Entries {}; - - for (NSURL* file in pasteboardFiles) { - files.push_back(file.path.UTF8String); - } - - const auto data = SSC::JSON::Object::Entries { - {"files", files}, - {"x", x}, - {"y", y} - }; - - const auto json = SSC::JSON::Object { data }; - debug("files: %s", json.str().c_str()); - const auto payload = SSC::getEmitToRenderProcessJavaScript("dropin", json.str()); - - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; -} - -- (void) updateEvent: (NSEvent*) event { - const auto location = [self convertPoint: event.locationInWindow fromView :nil]; - const auto x = std::to_string(location.x); - const auto y = std::to_string(location.y); - const auto count = draggablePayload.size(); - - if (((int) location.x) == lastX || ((int) location.y) == lastY) { - return [super mouseDown: event]; - } - - const auto data = SSC::JSON::Object::Entries { - {"count", (unsigned int) count}, - {"x", x}, - {"y", y} - }; - - const auto json = SSC::JSON::Object { data }; - const auto payload = SSC::getEmitToRenderProcessJavaScript("drag", json.str()); - - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; -} - -- (void) mouseUp: (NSEvent*) event { - [super mouseUp: event]; - - const auto location = [self convertPoint: event.locationInWindow fromView: nil]; - const auto x = location.x; - const auto y = location.y; - - const auto significantMoveX = (lastX - x) > 6 || (x - lastX) > 6; - const auto significantMoveY = (lastY - y) > 6 || (y - lastY) > 6; - - if (significantMoveX || significantMoveY) { - for (const auto& path : draggablePayload) { - const auto data = SSC::JSON::Object::Entries { - {"src", path}, - {"x", x}, - {"y", y} - }; - - const auto json = SSC::JSON::Object { data }; - const auto payload = SSC::getEmitToRenderProcessJavaScript("drop", json.str()); - - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; - } - } - - const auto data = SSC::JSON::Object::Entries { - {"x", x}, - {"y", y} - }; - - const auto json = SSC::JSON::Object { data }; - auto payload = SSC::getEmitToRenderProcessJavaScript("dragend", json.str()); - - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; -} - -- (void) mouseDown: (NSEvent*) event { - draggablePayload.clear(); - - const auto location = [self convertPoint: event.locationInWindow fromView: nil]; - const auto x = std::to_string(location.x); - const auto y = std::to_string(location.y); - - lastX = (int) location.x; - lastY = (int) location.y; - - SSC::String js( - "(() => {" - " const el = document.elementFromPoint(" + x + "," + y + ");" - " if (!el) return;" - " const found = el.matches('[data-src]') ? el : el.closest('[data-src]');" - " return found && found.dataset.src" - "})()"); - - [self - evaluateJavaScript: @(js.c_str()) - completionHandler: ^(id result, NSError *error) + - (void) userContentController: (WKUserContentController*) userContentController + didReceiveScriptMessage: (WKScriptMessage*) scriptMessage { - if (error) { - NSLog(@"%@", error); - [super mouseDown: event]; + const auto window = (Window*) objc_getAssociatedObject(self, "window"); + if (!window || !scriptMessage || !scriptMessage.body) { return; } - if (![result isKindOfClass: NSString.class]) { - [super mouseDown: event]; + if (![scriptMessage.body isKindOfClass: NSString.class]) { return; } - const auto string = SSC::String([result UTF8String]); - const auto files = SSC::split(string, ';'); - - if (files.size() == 0) { - [super mouseDown: event]; - return; - } - - draggablePayload = files; - [self updateEvent: event]; - }]; -} - -- (void) mouseDragged: (NSEvent*) event { - const auto location = [self convertPoint: event.locationInWindow fromView: nil]; - [super mouseDragged:event]; - - if (!NSPointInRect(location, self.frame)) { - auto payload = SSC::getEmitToRenderProcessJavaScript("dragexit", "{}"); - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; - } - - if (draggablePayload.size() == 0) { - return; - } - - const auto x = location.x; - const auto y = location.y; - const auto significantMoveX = (lastX - x) > 6 || (x - lastX) > 6; - const auto significantMoveY = (lastY - y) > 6 || (y - lastY) > 6; - - if (significantMoveX || significantMoveY) { - const auto data = SSC::JSON::Object::Entries { - {"count", (unsigned int) draggablePayload.size()}, - {"x", x}, - {"y", y} - }; - - const auto json = SSC::JSON::Object { data }; - const auto payload = SSC::getEmitToRenderProcessJavaScript("drag", json.str()); - - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; - } - - if (NSPointInRect(location, self.frame)) { - return; - } - - const auto pasteboard = [NSPasteboard pasteboardWithName: NSPasteboardNameDrag]; - const auto dragItems = [NSMutableArray new]; - const auto iconSize = NSMakeSize(32, 32); // according to documentation - - [pasteboard declareTypes: @[(NSString*) kPasteboardTypeFileURLPromise] owner:self]; - - auto dragPosition = [self convertPoint: event.locationInWindow fromView: nil]; - dragPosition.x -= 16; - dragPosition.y -= 16; - - NSRect imageLocation; - imageLocation.origin = dragPosition; - imageLocation.size = iconSize; - - for (const auto& file : draggablePayload) { - const auto url = [NSURL fileURLWithPath: @(file.c_str())]; - const auto icon = [NSWorkspace.sharedWorkspace iconForContentType: UTTypeURL]; - - NSArray* (^providerBlock)() = ^NSArray* () { - const auto component = [ - [NSDraggingImageComponent.alloc initWithKey: NSDraggingImageComponentIconKey - ] retain]; - - component.frame = NSMakeRect(0, 0, iconSize.width, iconSize.height); - component.contents = icon; - return @[component]; - }; - - auto provider = [NSFilePromiseProvider.alloc initWithFileType: @"public.url" delegate: self]; - - [provider setUserInfo: @(file.c_str())]; - - auto dragItem = [NSDraggingItem.alloc initWithPasteboardWriter: provider]; - - dragItem.draggingFrame = NSMakeRect( - dragPosition.x, - dragPosition.y, - iconSize.width, - iconSize.height - ); - - dragItem.imageComponentsProvider = providerBlock; - [dragItems addObject: dragItem]; - } - - auto session = [self - beginDraggingSessionWithItems: dragItems - event: event - source: self - ]; - - session.draggingFormation = NSDraggingFormationPile; - draggablePayload.clear(); -} - -- (NSDragOperation) draggingSession: (NSDraggingSession*) session - sourceOperationMaskForDraggingContext: (NSDraggingContext) context -{ - return NSDragOperationGeneric; -} - -- (void) filePromiseProvider: (NSFilePromiseProvider*) filePromiseProvider - writePromiseToURL: (NSURL*) url - completionHandler: (void (^)(NSError *errorOrNil)) completionHandler -{ - const auto dest = SSC::String(url.path.UTF8String); - const auto src = SSC::String([filePromiseProvider.userInfo UTF8String]); - const auto data = [@"" dataUsingEncoding: NSUTF8StringEncoding]; - - [data writeToURL: url atomically: YES]; - - const auto json = SSC::JSON::Object { - SSC::JSON::Object::Entries { - {"src", src}, - {"dest", dest} - } - }; - - const auto payload = SSC::getEmitToRenderProcessJavaScript("dropout", json.str()); + const auto string = (NSString*) scriptMessage.body; + const auto uri = String(string.UTF8String); - [self evaluateJavaScript: @(payload.c_str()) completionHandler: nil]; + #if SOCKET_RUNTIME_PLATFORM_IOS + const auto msg = IPC::Message(uri); + if (msg.name == "application.exit" || msg.name == "process.exit") { + const auto code = std::stoi(msg.get("value", "0")); - completionHandler(nil); -} - -- (NSString*) filePromiseProvider: (NSFilePromiseProvider*) filePromiseProvider - fileNameForType: (NSString*) fileType -{ - const auto id = SSC::rand64(); - const auto filename = std::to_string(id) + ".download"; - return @(filename.c_str()); -} - -- (void) webView: (WKWebView*) webView - runOpenPanelWithParameters: (WKOpenPanelParameters*) parameters - initiatedByFrame: (WKFrameInfo*) frame - completionHandler: (void (^)(NSArray<NSURL*>*URLs)) completionHandler -{ - const auto acceptedFileExtensions = parameters._acceptedFileExtensions; - const auto acceptedMIMETypes = parameters._acceptedMIMETypes; - SSC::StringStream contentTypesSpec; - - for (NSString* acceptedMIMEType in acceptedMIMETypes) { - contentTypesSpec << acceptedMIMEType.UTF8String << "|"; - } - - if (acceptedFileExtensions.count > 0) { - contentTypesSpec << "*/*:"; - const auto count = acceptedFileExtensions.count; - int seen = 0; - for (NSString* acceptedFileExtension in acceptedFileExtensions) { - const auto string = SSC::String(acceptedFileExtension.UTF8String); - - if (!string.starts_with(".")) { - contentTypesSpec << "."; + if (code > 0) { + CLI::notify(SIGTERM); + } else { + CLI::notify(SIGUSR2); } + } + #endif - contentTypesSpec << string; - if (++seen < count) { - contentTypesSpec << ","; + if (uri.size() > 0) { + if (!window->bridge.route(uri, nullptr, 0)) { + if (window->onMessage != nullptr) { + window->onMessage(uri); + } } } } - auto contentTypes = SSC::trim(contentTypesSpec.str()); - - if (contentTypes.size() == 0) { - contentTypes = "*/*"; - } - - if (contentTypes.ends_with("|")) { - contentTypes = contentTypes.substr(0, contentTypes.size() - 1); - } - - const auto options = SSC::Dialog::FileSystemPickerOptions { - .directories = false, - .multiple = parameters.allowsMultipleSelection ? true : false, - .contentTypes = contentTypes, - .defaultName = "", - .defaultPath = "", - .title = "Choose a File" - }; + - (SSCWebView*) webView: (SSCWebView*) webview + createWebViewWithConfiguration: (WKWebViewConfiguration*) configuration + forNavigationAction: (WKNavigationAction*) navigationAction + windowFeatures: (WKWindowFeatures*) windowFeatures +{ + // TODO(@jwerle): handle 'window.open()' + return nullptr; +} - SSC::Dialog dialog; - const auto results = dialog.showOpenFilePicker(options); +#if SOCKET_RUNTIME_PLATFORM_MACOS +- (void) menuItemSelected: (NSMenuItem*) menuItem { + auto window = (Window*) objc_getAssociatedObject(self, "window"); - if (results.size() == 0) { - completionHandler(nullptr); + if (!window) { return; } - auto urls = [NSMutableArray new]; - - for (const auto& result : results) { - [urls addObject: [NSURL URLWithString: @(result.c_str())]]; + const auto title = String(menuItem.title.UTF8String); + const auto state = String(menuItem.state == NSControlStateValueOn ? "true" : "false"); + const auto seq = std::to_string(menuItem.tag); + const auto key = String(menuItem.keyEquivalent.UTF8String); + auto parent = String(menuItem.menu.title.UTF8String); + id representedObject = [menuItem representedObject]; + String type = "system"; + + // "contextMenu" is parent menu identifier used in `setContextMenu()` + if (parent == "contextMenu") { + parent = ""; + type = "context"; } - completionHandler(urls); -} -#endif - -#if (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) || (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_15) - -- (void) webView: (WKWebView*) webView - requestDeviceOrientationAndMotionPermissionForOrigin: (WKSecurityOrigin*) origin - initiatedByFrame: (WKFrameInfo*) frame - decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler { - static auto userConfig = SSC::getUserConfig(); + if (representedObject != nullptr) { + const auto parts = split([representedObject UTF8String], ':'); + if (parts.size() > 0) { + type = parts[0]; + } - if (userConfig["permissions_allow_device_orientation"] == "false") { - decisionHandler(WKPermissionDecisionDeny); - return; + if (parts.size() > 1) { + parent = trim(parts[1]); + } } - decisionHandler(WKPermissionDecisionGrant); + window->eval(getResolveMenuSelectionJavaScript(seq, title, parent, type)); } -- (void) webView: (WKWebView*) webView - requestMediaCapturePermissionForOrigin: (WKSecurityOrigin*) origin - initiatedByFrame: (WKFrameInfo*) frame - type: (WKMediaCaptureType) type - decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler { - static auto userConfig = SSC::getUserConfig(); +- (BOOL) windowShouldClose: (SSCWindow*) _ { + auto window = (Window*) objc_getAssociatedObject(self, "window"); + auto app = App::sharedApplication(); - if (userConfig["permissions_allow_user_media"] == "false") { - decisionHandler(WKPermissionDecisionDeny); - return; + if (!app || !window || window->isExiting) { + return true; } - if (type == WKMediaCaptureTypeCameraAndMicrophone) { - if ( - userConfig["permissions_allow_camera"] == "false" || - userConfig["permissions_allow_microphone"] == "false" - ) { - decisionHandler(WKPermissionDecisionDeny); - return; - } - } - - if ( - type == WKMediaCaptureTypeCamera && - userConfig["permissions_allow_camera"] == "false" - ) { - decisionHandler(WKPermissionDecisionDeny); - return; - } + auto index = window->index; + const JSON::Object json = JSON::Object::Entries { + {"data", window->index} + }; - if ( - type == WKMediaCaptureTypeMicrophone && - userConfig["permissions_allow_microphone"] == "false" - ) { - decisionHandler(WKPermissionDecisionDeny); - return; + for (auto window : app->windowManager.windows) { + if (window != nullptr && window->index != index) { + window->eval(getEmitToRenderProcessJavaScript("window-closed", json.str())); + } } - decisionHandler(WKPermissionDecisionGrant); + app->windowManager.destroyWindow(index); + return true; } -#endif - -- (void) webView: (WKWebView*) webView - runJavaScriptAlertPanelWithMessage: (NSString*) message - initiatedByFrame: (WKFrameInfo*) frame - completionHandler: (void (^)(void)) completionHandler { - static auto userConfig = SSC::getUserConfig(); - auto title = userConfig["meta_title"] + ":"; - - if (userConfig.contains("window_alert_title")) { - title = userConfig["window_alert_title"]; +#elif SOCKET_RUNTIME_PLATFORM_IOS +- (void) scrollViewDidScroll: (UIScrollView*) scrollView { + auto window = (Window*) objc_getAssociatedObject(self, "window"); + if (window) { + scrollView.bounds = window->webview.bounds; } - -#if TARGET_OS_IPHONE || TARGET_OS_IPHONE - auto alert = [UIAlertController - alertControllerWithTitle: @(title.c_str()) - message: message - preferredStyle: UIAlertControllerStyleAlert - ]; - - auto ok = [UIAlertAction - actionWithTitle: @"OK" - style: UIAlertActionStyleDefault - handler: ^(UIAlertAction * action) { - completionHandler(); - }]; - - [alert addAction: ok]; - - [webView presentViewController:alert animated: YES completion: nil]; -#else - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText: @(title.c_str())]; - [alert setInformativeText: message]; - [alert addButtonWithTitle: @"OK"]; - [alert runModal]; - completionHandler(); -#endif } - -- (void) webView: (WKWebView*) webView - runJavaScriptConfirmPanelWithMessage: (NSString*) message - initiatedByFrame: (WKFrameInfo*) frame - completionHandler: (void (^)(BOOL result)) completionHandler { - static auto userConfig = SSC::getUserConfig(); - auto title = userConfig["meta_title"] + ":"; - - if (userConfig.contains("window_alert_title")) { - title = userConfig["window_alert_title"]; - } -#if TARGET_OS_IPHONE || TARGET_OS_IPHONE - auto alert = [UIAlertController - alertControllerWithTitle: @(title.c_str()) - message: message - preferredStyle: UIAlertControllerStyleAlert - ]; - - auto ok = [UIAlertAction - actionWithTitle: @"OK" - style: UIAlertActionStyleDefault - handler: ^(UIAlertAction * action) { - completionHandler(YES); - }]; - - auto cancel = [UIAlertAction - actionWithTitle: @"Cancel" - style: UIAlertActionStyleDefault - handler: ^(UIAlertAction * action) { - completionHandler(NO); - }]; - - [alert addAction: ok]; - [alert addAction: cancel]; - - [webView presentViewController: alert animated: YES completion: nil]; -#else - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText: @(title.c_str())]; - [alert setInformativeText: message]; - [alert addButtonWithTitle: @"OK"]; - [alert addButtonWithTitle: @"Cancel"]; - completionHandler([alert runModal] == NSAlertFirstButtonReturn); #endif -} - @end -#if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR namespace SSC { - static bool isDelegateSet = false; - - Window::Window (App& app, WindowOptions opts) - : app(app), - opts(opts), - hotkey(this) + Window::Window (SharedPointer<Core> core, const Window::Options& options) + : core(core), + options(options), + bridge(core, IPC::Bridge::Options { + options.userConfig, + options.as<IPC::Preload::Options>() + }), + hotkey(this), + dialog(this) { - // Window style: titled, closable, minimizable - uint style = NSWindowStyleMaskTitled; - - // Set window to be resizable - if (opts.resizable) { - style |= NSWindowStyleMaskResizable; - } - - if (opts.frameless) { - style |= NSWindowStyleMaskFullSizeContentView; - style |= NSWindowStyleMaskBorderless; - } else if (opts.utility) { - style |= NSWindowStyleMaskUtilityWindow; - } else { - style |= NSWindowStyleMaskClosable; - style |= NSWindowStyleMaskMiniaturizable; - } - - window = [[NSWindow alloc] - initWithContentRect: NSMakeRect(0, 0, opts.width, opts.height) - styleMask: style - backing: NSBackingStoreBuffered - defer: NO]; - - NSArray* draggableTypes = [NSArray arrayWithObjects: - NSPasteboardTypeURL, - NSPasteboardTypeFileURL, - (NSString*) kPasteboardTypeFileURLPromise, - NSPasteboardTypeString, - NSPasteboardTypeHTML, - nil - ]; - - // Position window in center of screen - [window center]; - [window setOpaque: YES]; - // Minimum window size - [window setContentMinSize: NSMakeSize(opts.minWidth, opts.minHeight)]; - [window setBackgroundColor: [NSColor controlBackgroundColor]]; - [window registerForDraggedTypes: draggableTypes]; - // [window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]]; - - if (opts.frameless) { - [window setTitlebarAppearsTransparent: true]; - } - - // window.movableByWindowBackground = true; - window.titlebarAppearsTransparent = true; - - static auto userConfig = SSC::getUserConfig(); + #if SOCKET_RUNTIME_PLATFORM_IOS + const auto frame = UIScreen.mainScreen.bounds; + #endif + const auto processInfo = NSProcessInfo.processInfo; + const auto configuration = [WKWebViewConfiguration new]; + const auto preferences = configuration.preferences; + auto userConfig = options.userConfig; - this->index = opts.index; - this->bridge = new IPC::Bridge(app.core); - this->hotkey.init(this->bridge); + this->index = options.index; + this->windowDelegate = [SSCWindowDelegate new]; - this->bridge->router.dispatchFunction = [this] (auto callback) { - this->app.dispatch(callback); + this->bridge.navigateFunction = [this] (const auto url) { + this->navigate(url); }; - this->bridge->router.evaluateJavaScriptFunction = [this](auto js) { - dispatch_async(dispatch_get_main_queue(), ^{ this->eval(js); }); + this->bridge.evaluateJavaScriptFunction = [this](auto source) { + this->eval(source); }; - this->bridge->router.map("window.eval", [=, this](auto message, auto router, auto reply) { - auto value = message.value; - auto seq = message.seq; - auto script = [NSString stringWithUTF8String: value.c_str()]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [webview evaluateJavaScript: script completionHandler: ^(id result, NSError *error) { - if (result) { - auto msg = String([[NSString stringWithFormat:@"%@", result] UTF8String]); - this->bridge->router.send(seq, msg, Post{}); - } else if (error) { - auto exception = (NSString *) error.userInfo[@"WKJavaScriptExceptionMessage"]; - auto message = [[NSString stringWithFormat:@"%@", exception] UTF8String]; - auto err = encodeURIComponent(String(message)); - - if (err == "(null)") { - this->bridge->router.send(seq, "null", Post{}); - return; - } - - auto json = JSON::Object::Entries { - {"err", JSON::Object::Entries { - {"message", String("Error: ") + err} - }} - }; - - this->bridge->router.send(seq, JSON::Object(json).str(), Post{}); - } else { - this->bridge->router.send(seq, "undefined", Post{}); - } - }]; - }); + this->bridge.client.preload = IPC::Preload::compile({ + .client = UniqueClient { + .id = this->bridge.client.id, + .index = this->bridge.client.index + }, + .index = options.index, + .conduit = this->core->conduit.port, + .userScript = options.userScript }); + configuration.defaultWebpagePreferences.allowsContentJavaScript = YES; - // Initialize WKWebView - WKWebViewConfiguration* config = [WKWebViewConfiguration new]; + #if SOCKET_RUNTIME_PLATFORM_IOS + configuration.allowsInlineMediaPlayback = YES; + #endif + + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; // https://webkit.org/blog/10882/app-bound-domains/ // https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/3585117-limitsnavigationstoappbounddomai - config.limitsNavigationsToAppBoundDomains = YES; + configuration.limitsNavigationsToAppBoundDomains = YES; + configuration.websiteDataStore = WKWebsiteDataStore.defaultDataStore; + + if (@available(macOS 14.0, iOS 17.0, *)) { + [configuration.websiteDataStore.httpCookieStore + setCookiePolicy: WKCookiePolicyAllow + completionHandler: ^(){} + ]; + } + + [configuration.userContentController + addScriptMessageHandler: this->windowDelegate + name: @"external" + ]; + + auto preloadUserScript= [WKUserScript alloc]; + auto preloadUserScriptSource = IPC::Preload::compile({ + .features = IPC::Preload::Options::Features { + .useGlobalCommonJS = false, + .useGlobalNodeJS = false, + .useTestScript = false, + .useHTMLMarkup = false, + .useESM = false, + .useGlobalArgs = true + }, + .client = UniqueClient { + .id = this->bridge.client.id, + .index = this->bridge.client.index + }, + .index = options.index, + .conduit = this->core->conduit.port, + .userScript = options.userScript + }); - [config setURLSchemeHandler: bridge->router.schemeHandler - forURLScheme: @"ipc"]; + [preloadUserScript + initWithSource: @(preloadUserScriptSource.str().c_str()) + injectionTime: WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly: NO + ]; - [config setURLSchemeHandler: bridge->router.schemeHandler - forURLScheme: @"socket"]; + [configuration.userContentController + addUserScript: preloadUserScript + ]; - [config setValue: @NO forKey: @"crossOriginAccessControlCheckEnabled"]; + [configuration + setValue: @NO + forKey: @"crossOriginAccessControlCheckEnabled" + ]; - WKPreferences* prefs = [config preferences]; - prefs.javaScriptCanOpenWindowsAutomatically = YES; + preferences.javaScriptCanOpenWindowsAutomatically = YES; + preferences.elementFullscreenEnabled = userConfig["permissions_allow_fullscreen"] != "false"; @try { if (userConfig["permissions_allow_fullscreen"] == "false") { - [prefs setValue: @NO forKey: @"fullScreenEnabled"]; - [prefs setValue: @NO forKey: @"elementFullscreenEnabled"]; + [preferences setValue: @NO forKey: @"fullScreenEnabled"]; + [preferences setValue: @NO forKey: @"elementFullscreenEnabled"]; } else { - [prefs setValue: @YES forKey: @"fullScreenEnabled"]; - [prefs setValue: @YES forKey: @"elementFullscreenEnabled"]; + [preferences setValue: @YES forKey: @"fullScreenEnabled"]; + [preferences setValue: @YES forKey: @"elementFullscreenEnabled"]; } } @catch (NSException *error) { debug("Failed to set preference: 'fullScreenEnabled': %@", error); @@ -780,26 +285,19 @@ - (void) webView: (WKWebView*) webView @try { if (userConfig["permissions_allow_fullscreen"] == "false") { - [prefs setValue: @NO forKey: @"elementFullscreenEnabled"]; + [preferences setValue: @NO forKey: @"elementFullscreenEnabled"]; } else { - [prefs setValue: @YES forKey: @"elementFullscreenEnabled"]; + [preferences setValue: @YES forKey: @"elementFullscreenEnabled"]; } } @catch (NSException *error) { debug("Failed to set preference: 'elementFullscreenEnabled': %@", error); } - if (SSC::isDebugEnabled()) { - [prefs setValue:@YES forKey:@"developerExtrasEnabled"]; - if (@available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) { - [webview setInspectable: YES]; - } - } - @try { if (userConfig["permissions_allow_clipboard"] == "false") { - [prefs setValue: @NO forKey: @"javaScriptCanAccessClipboard"]; + [preferences setValue: @NO forKey: @"javaScriptCanAccessClipboard"]; } else { - [prefs setValue: @YES forKey: @"javaScriptCanAccessClipboard"]; + [preferences setValue: @YES forKey: @"javaScriptCanAccessClipboard"]; } } @catch (NSException *error) { debug("Failed to set preference: 'javaScriptCanAccessClipboard': %@", error); @@ -807,9 +305,9 @@ - (void) webView: (WKWebView*) webView @try { if (userConfig["permissions_allow_data_access"] == "false") { - [prefs setValue: @NO forKey: @"storageAPIEnabled"]; + [preferences setValue: @NO forKey: @"storageAPIEnabled"]; } else { - [prefs setValue: @YES forKey: @"storageAPIEnabled"]; + [preferences setValue: @YES forKey: @"storageAPIEnabled"]; } } @catch (NSException *error) { debug("Failed to set preference: 'storageAPIEnabled': %@", error); @@ -817,9 +315,9 @@ - (void) webView: (WKWebView*) webView @try { if (userConfig["permissions_allow_device_orientation"] == "false") { - [prefs setValue: @NO forKey: @"deviceOrientationEventEnabled"]; + [preferences setValue: @NO forKey: @"deviceOrientationEventEnabled"]; } else { - [prefs setValue: @YES forKey: @"deviceOrientationEventEnabled"]; + [preferences setValue: @YES forKey: @"deviceOrientationEventEnabled"]; } } @catch (NSException *error) { debug("Failed to set preference: 'deviceOrientationEventEnabled': %@", error); @@ -827,38 +325,26 @@ - (void) webView: (WKWebView*) webView if (userConfig["permissions_allow_notifications"] == "false") { @try { - [prefs setValue: @NO forKey: @"appBadgeEnabled"]; - } @catch (NSException *error) { - debug("Failed to set preference: 'deviceOrientationEventEnabled': %@", error); - } - - @try { - [prefs setValue: @NO forKey: @"notificationsEnabled"]; + [preferences setValue: @NO forKey: @"notificationsEnabled"]; } @catch (NSException *error) { debug("Failed to set preference: 'notificationsEnabled': %@", error); } @try { - [prefs setValue: @NO forKey: @"notificationEventEnabled"]; + [preferences setValue: @NO forKey: @"notificationEventEnabled"]; } @catch (NSException *error) { debug("Failed to set preference: 'notificationEventEnabled': %@", error); } - } else { - @try { - [prefs setValue: @YES forKey: @"appBadgeEnabled"]; - } @catch (NSException *error) { - debug("Failed to set preference: 'appBadgeEnabled': %@", error); - } } - #if !TARGET_OS_IPHONE + #if SOCKET_RUNTIME_PLATFORM_MACOS @try { - [prefs setValue: @YES forKey: @"cookieEnabled"]; + [preferences setValue: @YES forKey: @"cookieEnabled"]; if (userConfig["permissions_allow_user_media"] == "false") { - [prefs setValue: @NO forKey: @"mediaStreamEnabled"]; + [preferences setValue: @NO forKey: @"mediaStreamEnabled"]; } else { - [prefs setValue: @YES forKey: @"mediaStreamEnabled"]; + [preferences setValue: @YES forKey: @"mediaStreamEnabled"]; } } @catch (NSException *error) { debug("Failed to set preference: 'mediaStreamEnabled': %@", error); @@ -867,310 +353,528 @@ - (void) webView: (WKWebView*) webView @try { if (userConfig["permissions_allow_airplay"] == "false") { - config.allowsAirPlayForMediaPlayback = NO; + configuration.allowsAirPlayForMediaPlayback = NO; } else { - config.allowsAirPlayForMediaPlayback = YES; + configuration.allowsAirPlayForMediaPlayback = YES; } } @catch (NSException *error) { - debug("%@", error); + debug("Failed to set preference 'allowsAirPlayForMediaPlayback': %@", error); } - config.defaultWebpagePreferences.allowsContentJavaScript = YES; - config.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; - config.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; - config.processPool = [WKProcessPool new]; - - /** - [config.websiteDataStore.httpCookieStore - setCookiePolicy: WKCookiePolicyAllow - completionHandler: ^(){} - ]; - */ - @try { - [prefs setValue: @YES forKey: @"offlineApplicationCacheIsEnabled"]; + [preferences setValue: @YES forKey: @"offlineApplicationCacheIsEnabled"]; } @catch (NSException *error) { debug("Failed to set preference: 'offlineApplicationCacheIsEnabled': %@", error); } - WKUserContentController* controller = [config userContentController]; + if (options.debug || isDebugEnabled()) { + [preferences setValue: @YES forKey: @"developerExtrasEnabled"]; + } - // Add preload script, normalizing the interface to be cross-platform. - SSC::String preload = createPreload(opts); + this->bridge.init(); + this->hotkey.init(); + this->bridge.configureNavigatorMounts(); + this->bridge.configureSchemeHandlers({ + .webview = configuration + }); - WKUserScript* userScript = [WKUserScript alloc]; + #if SOCKET_RUNTIME_PLATFORM_MACOS + this->webview = [SSCWebView.alloc + initWithFrame: NSZeroRect + configuration: configuration + radius: (CGFloat) options.radius + margin: (CGFloat) options.margin + ]; - [userScript - initWithSource: [NSString stringWithUTF8String:preload.c_str()] - injectionTime: WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly: NO + this->webview.wantsLayer = YES; + this->webview.layer.backgroundColor = NSColor.clearColor.CGColor; + this->webview.customUserAgent = [NSString + stringWithFormat: @("Mozilla/5.0 (Macintosh; Intel Mac OS X %d_%d_%d) AppleWebKit/605.1.15 (KHTML, like Gecko) SocketRuntime/%s"), + processInfo.operatingSystemVersion.majorVersion, + processInfo.operatingSystemVersion.minorVersion, + processInfo.operatingSystemVersion.patchVersion, + SSC::VERSION_STRING.c_str() + ]; + [this->webview setValue: @(0) forKey: @"drawsBackground"]; + #elif SOCKET_RUNTIME_PLATFORM_IOS + this->webview = [SSCWebView.alloc + initWithFrame: frame + configuration: configuration ]; - [controller addUserScript: userScript]; + this->webview.scrollView.delegate = this->windowDelegate; + this->webview.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - webview = [[SSCBridgedWebView alloc] - initWithFrame: NSZeroRect - configuration: config - ]; + this->webview.autoresizingMask = ( + UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight + ); - [webview.configuration - setValue: @YES - forKey: @"allowUniversalAccessFromFileURLs" + this->webview.customUserAgent = [NSString + stringWithFormat: @("Mozilla/5.0 (iPhone; CPU iPhone OS %d_%d_%d like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1 SocketRuntime/%s"), + processInfo.operatingSystemVersion.majorVersion, + processInfo.operatingSystemVersion.minorVersion, + processInfo.operatingSystemVersion.patchVersion, + SSC::VERSION_STRING.c_str() ]; + #endif - [webview.configuration.preferences - setValue: @YES - forKey: @"allowFileAccessFromFileURLs" - ]; + this->webview.allowsBackForwardNavigationGestures = ( + userConfig["webview_navigator_enable_navigation_gestures"] == "true" + ); - [webview.configuration.processPool - performSelector: @selector(_registerURLSchemeAsSecure:) - withObject: @"socket" - ]; + this->webview.UIDelegate = webview; + this->webview.layer.opaque = NO; + + objc_setAssociatedObject( + this->webview, + "window", + (id) this, + OBJC_ASSOCIATION_ASSIGN + ); + + objc_setAssociatedObject( + this->windowDelegate, + "window", + (id) this, + OBJC_ASSOCIATION_ASSIGN + ); + + if (options.debug || isDebugEnabled()) { + if (@available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) { + this->webview.inspectable = YES; + } + } + + if (options.width > 0 && options.height > 0) { + this->setSize(options.width, options.height); + } else if (options.width > 0) { + this->setSize(options.width, window.frame.size.height); + } else if (options.height > 0) { + this->setSize(window.frame.size.width, options.height); + } + + this->bridge.configureWebView(this->webview); + #if SOCKET_RUNTIME_PLATFORM_MACOS + // Window style: titled, closable, minimizable + uint style = NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + + // Set window to be resizable + if (options.resizable) { + style |= NSWindowStyleMaskResizable; + } + + if (options.closable) { + style |= NSWindowStyleMaskClosable; + } - [webview.configuration.processPool - performSelector: @selector(_registerURLSchemeAsSecure:) - withObject: @"ipc" + if (options.minimizable) { + style |= NSWindowStyleMaskMiniaturizable; + } + + this->window = [SSCWindow.alloc + initWithContentRect: NSMakeRect(0, 0, options.width, options.height) + styleMask: style + backing: NSBackingStoreBuffered + defer: NO ]; + // this->window.appearance = [NSAppearance appearanceNamed: NSAppearanceNameVibrantDark]; + this->window.contentMinSize = NSMakeSize(options.minWidth, options.minHeight); + this->window.titleVisibility = NSWindowTitleVisible; + this->window.titlebarAppearsTransparent = true; + // this->window.movableByWindowBackground = true; + this->window.delegate = this->windowDelegate; + this->window.opaque = YES; + this->window.webview = this->webview; + + if (options.maximizable == false) { + this->window.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary; + } - static const auto devHost = SSC::getDevHost(); - if (devHost.starts_with("http:")) { - [webview.configuration.processPool - performSelector: @selector(_registerURLSchemeAsSecure:) - withObject: @"http" - ]; + // Add webview to window + this->window.contentView = this->webview; + + if (options.frameless) { + this->window.titlebarAppearsTransparent = YES; + this->window.movableByWindowBackground = YES; + style = NSWindowStyleMaskFullSizeContentView; + style |= NSWindowStyleMaskBorderless; + style |= NSWindowStyleMaskResizable; + this->window.styleMask = style; + } else if (options.utility) { + style |= NSWindowStyleMaskBorderless; + style |= NSWindowStyleMaskUtilityWindow; + this->window.styleMask = style; } - /* [webview - setValue: [NSNumber numberWithBool: YES] - forKey: @"drawsTransparentBackground" - ]; */ - - // [webview registerForDraggedTypes: - // [NSArray arrayWithObject:NSPasteboardTypeFileURL]]; - // - - windowDelegate = [SSCWindowDelegate new]; - navigationDelegate = [SSCNavigationDelegate new]; - navigationDelegate.bridge = this->bridge; - [controller addScriptMessageHandler: windowDelegate name: @"external"]; - - // set delegates - window.delegate = windowDelegate; - webview.UIDelegate = webview; - webview.navigationDelegate = navigationDelegate; - - if (!isDelegateSet) { - isDelegateSet = true; - - class_replaceMethod( - [SSCWindowDelegate class], - @selector(windowShouldClose:), - imp_implementationWithBlock( - [&](id self, SEL cmd, id notification) { - auto window = (Window*) objc_getAssociatedObject(self, "window"); - if (!window) { - return true; - } + if (options.titlebarStyle == "hidden") { + // hidden title bar and a full-size content window. + style |= NSWindowStyleMaskFullSizeContentView; + style |= NSWindowStyleMaskResizable; + this->window.styleMask = style; + this->window.titleVisibility = NSWindowTitleHidden; + } else if (options.titlebarStyle == "hiddenInset") { + // hidden titlebar with inset/offset window controls + style |= NSWindowStyleMaskFullSizeContentView; + style |= NSWindowStyleMaskTitled; + style |= NSWindowStyleMaskResizable; - if (exiting) return true; + this->window.styleMask = style; + this->window.titleVisibility = NSWindowTitleHidden; - if (window->opts.canExit) { - exiting = true; - window->exit(0); - return true; - } - window->eval(getEmitToRenderProcessJavaScript("windowHide", "{}")); - window->hide(); - return false; - }), - "v@:@" - ); - - class_replaceMethod( - [SSCWindowDelegate class], - @selector(userContentController:didReceiveScriptMessage:), - imp_implementationWithBlock( - [=, this](id self, SEL cmd, WKScriptMessage* scriptMessage) { - auto window = (Window*) objc_getAssociatedObject(self, "window"); - - if (!scriptMessage || !window) return; - id body = [scriptMessage body]; - if (!body || ![body isKindOfClass:[NSString class]]) { - return; - } + auto x = 16.f; + auto y = 42.f; + + if (options.windowControlOffsets.size() > 0) { + auto parts = split(options.windowControlOffsets, 'x'); + try { + x = std::stof(parts[0]); + y = std::stof(parts[1]); + } catch (...) { + debug("invalid arguments for windowControlOffsets"); + } + } - String uri = [body UTF8String]; - if (!uri.size()) return; + auto titleBarView = [NSView.alloc initWithFrame: NSZeroRect]; - if (!bridge->route(uri, nullptr, 0)) { - if (window != nullptr && window->onMessage != nullptr) { - window->onMessage(uri); - } - } - }), - "v@:@" - ); - - class_addMethod( - [SSCWindowDelegate class], - @selector(menuItemSelected:), - imp_implementationWithBlock( - [=](id self, SEL _cmd, id item) { - auto window = (Window*) objc_getAssociatedObject(self, "window"); - - if (!window) { - return; - } + titleBarView.layer.backgroundColor = NSColor.clearColor.CGColor; // Set background color to clear + titleBarView.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; + titleBarView.wantsLayer = YES; - id menuItem = (id) item; - SSC::String title = [[menuItem title] UTF8String]; - SSC::String state = [menuItem state] == NSControlStateValueOn ? "true" : "false"; - SSC::String parent = [[[menuItem menu] title] UTF8String]; - SSC::String seq = std::to_string([menuItem tag]); - SSC::String key = [[menuItem keyEquivalent] UTF8String]; - id representedObject = [menuItem representedObject]; - String type = "system"; - - // "contextMenu" is parent menu identifier used in `setContextMenu()` - if (parent == "contextMenu") { - parent = ""; - type = "context"; - } + const auto closeButton = [this->window standardWindowButton: NSWindowCloseButton]; + const auto minimizeButton = [this->window standardWindowButton: NSWindowMiniaturizeButton]; + const auto zoomButton = [this->window standardWindowButton: NSWindowZoomButton]; - if (representedObject != nullptr) { - const auto parts = split([representedObject UTF8String], ':'); - if (parts.size() > 0) { - type = parts[0]; - } + if (closeButton && minimizeButton && zoomButton) { + [titleBarView addSubview: closeButton]; + [titleBarView addSubview: minimizeButton]; + [titleBarView addSubview: zoomButton]; - if (parts.size() > 1) { - parent = trim(parts[1]); - } - } + const auto viewWidth = window.frame.size.width; + const auto viewHeight = y + MACOS_TRAFFIC_LIGHT_BUTTON_SIZE; + const auto newX = x; + const auto newY = 0.f; - window->eval(getResolveMenuSelectionJavaScript(seq, title, parent, type)); - }), - "v@:@:@:" - ); + titleBarView.frame = NSMakeRect(newX, newY, viewWidth, viewHeight); + + this->window.windowControlOffsets = NSMakePoint(x, y); + this->window.titleBarView = titleBarView; + + [this->webview addSubview: titleBarView]; + } else { + NSLog(@"Failed to retrieve standard window buttons."); + } } - objc_setAssociatedObject( - windowDelegate, - "window", - (id) this, - OBJC_ASSOCIATION_ASSIGN - ); + if (options.aspectRatio.size() > 2) { + const auto parts = split(options.aspectRatio, ':'); + if (parts.size() == 2) { + CGFloat aspectRatio; + + @try { + aspectRatio = std::stof(trim(parts[0])) / std::stof(trim(parts[1])); + } @catch (NSException *error) { + debug("Invalid aspect ratio: %@", error); + } - // Initialize application - [NSApplication sharedApplication]; + auto frame = this->window.frame; + if (!std::isnan(aspectRatio)) { + frame.size.height = frame.size.width / aspectRatio; + this->window.contentAspectRatio = frame.size; + } + } + } - if (userConfig["application_agent"] == "true") { - [NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory]; + const auto appearance = NSAppearance.currentAppearance; + bool didSetBackgroundColor = false; - if (this->opts.index == 0) { - [window setBackgroundColor: [NSColor clearColor]]; - [window setAlphaValue: 0.0]; - [window setIgnoresMouseEvents: YES]; - [window setCanHide: NO]; - [window setOpaque: NO]; + if ([appearance bestMatchFromAppearancesWithNames: @[NSAppearanceNameDarkAqua]]) { + if (options.backgroundColorDark.size()) { + this->setBackgroundColor(options.backgroundColorDark); + didSetBackgroundColor = true; } } else { - [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; + if (options.backgroundColorLight.size()) { + this->setBackgroundColor(options.backgroundColorLight); + didSetBackgroundColor = true; + } } - if (opts.headless) { - [NSApp activateIgnoringOtherApps: NO]; + if (!didSetBackgroundColor) { + this->window.backgroundColor = NSColor.windowBackgroundColor; + } + + // Position window in center of screen + [this->window center]; + + [this->window registerForDraggedTypes: [NSArray arrayWithObjects: + NSPasteboardTypeURL, + NSPasteboardTypeFileURL, + (NSString*) kPasteboardTypeFileURLPromise, + NSPasteboardTypeString, + NSPasteboardTypeHTML, + nil + ]]; + + if (options.index == 0) { + if (options.headless || userConfig["application_agent"] == "true") { + NSApp.activationPolicy = NSApplicationActivationPolicyAccessory; + + if (userConfig["application_agent"] == "true") { + this->window.backgroundColor = NSColor.clearColor; + this->window.alphaValue = 0.0; + this->window.ignoresMouseEvents = YES; + this->window.canHide = NO; + this->window.opaque = NO; + } + } else { + NSApp.activationPolicy = NSApplicationActivationPolicyRegular; + } + + if (options.headless) { + [NSApp activateIgnoringOtherApps: NO]; + } else { + // Sets the app as the active app + [NSApp activateIgnoringOtherApps: YES]; + } + } + #elif SOCKET_RUNTIME_PLATFORM_IOS + this->window = [SSCWindow.alloc initWithFrame: frame]; + this->viewController = [SSCWebViewController new]; + this->viewController.webview = this->webview; + + UIUserInterfaceStyle interfaceStyle = this->window.traitCollection.userInterfaceStyle; + + auto hasBackgroundDark = userConfig.count("window_background_color_dark") > 0; + auto hasBackgroundLight = userConfig.count("window_background_color_light") > 0; + + if (interfaceStyle == UIUserInterfaceStyleDark && hasBackgroundDark) { + this->setBackgroundColor(userConfig["window_background_color_dark"]); + } else if (hasBackgroundLight) { + this->setBackgroundColor(userConfig["window_background_color_light"]); } else { - // Sets the app as the active app - [NSApp activateIgnoringOtherApps: YES]; + this->viewController.webview.backgroundColor = [UIColor systemBackgroundColor]; + this->window.backgroundColor = [UIColor systemBackgroundColor]; + this->viewController.webview.opaque = NO; } - // Add webview to window - [window setContentView: webview]; + [this->viewController.view addSubview:this->webview]; + + this->window.rootViewController = this->viewController; + this->window.rootViewController.view.frame = frame; + #endif - navigate("0", opts.url); + this->position.x = this->window.frame.origin.x; + this->position.y = this->window.frame.origin.y; } Window::~Window () { - this->close(0); + #if !__has_feature(objc_arc) + if (this->processPool) { + [this->processPool release]; + } + + if (this->webview) { + [this->webview release]; + } + + if (this->windowDelegate) { + [this->windowDelegate release]; + } + + if (this->window) { + [this->window release]; + } + #endif + + this->window = nullptr; + this->webview = nullptr; + this->processPool = nullptr; + this->windowDelegate = nullptr; } ScreenSize Window::getScreenSize () { - NSRect e = [[NSScreen mainScreen] frame]; - + #if SOCKET_RUNTIME_PLATFORM_MACOS + const auto frame = NSScreen.mainScreen.frame; + #elif SOCKET_RUNTIME_PLATFORM_IOS + const auto frame = UIScreen.mainScreen.bounds; + #endif return ScreenSize { - .height = (int) e.size.height, - .width = (int) e.size.width + .width = (int) frame.size.width, + .height = (int) frame.size.height }; } void Window::show () { - if (this->opts.headless == true) { + #if SOCKET_RUNTIME_PLATFORM_MACOS + if (this->options.index == 0) { + if (options.headless || this->bridge.userConfig["application_agent"] == "true") { + NSApp.activationPolicy = NSApplicationActivationPolicyAccessory; + } else { + NSApp.activationPolicy = NSApplicationActivationPolicyRegular; + } + } + + if (this->options.headless || this->bridge.userConfig["application_agent"] == "true") { [NSApp activateIgnoringOtherApps: NO]; } else { - [window makeKeyAndOrderFront: nil]; + [this->webview becomeFirstResponder]; + [this->window makeKeyAndOrderFront: nil]; [NSApp activateIgnoringOtherApps: YES]; } + + this->position.x = this->window.frame.origin.x; + this->position.y = this->window.frame.origin.y; + #elif SOCKET_RUNTIME_PLATFORM_IOS + [this->webview becomeFirstResponder]; + [this->window makeKeyAndVisible]; + this->window.hidden = NO; + const auto app = App::sharedApplication(); + for (auto window : app->windowManager.windows) { + if (window != nullptr && reinterpret_cast<Window*>(window.get()) != this) { + window->hide(); + } + } + #endif } void Window::exit (int code) { - exiting = true; - this->close(code);; - if (onExit != nullptr) onExit(code); + isExiting = true; + const auto callback = this->onExit; + this->onExit = nullptr; + if (callback != nullptr) { + callback(code); + } } - void Window::kill () { - } + void Window::kill () {} void Window::close (int code) { - if (this->window != nullptr) { - [this->window performClose: nil]; - - this->window = nullptr; + const auto app = App::sharedApplication(); + if (this->windowDelegate != nullptr) { + objc_removeAssociatedObjects(this->windowDelegate); } - if (this->webview) { - this->webview = nullptr; + if (this->webview != nullptr) { + objc_removeAssociatedObjects(this->webview); + [this->webview stopLoading]; + [this->webview.configuration.userContentController removeAllScriptMessageHandlers]; + [this->webview removeFromSuperview]; + this->webview.navigationDelegate = nullptr; + this->webview.UIDelegate = nullptr; } - if (this->windowDelegate != nullptr) { - objc_removeAssociatedObjects(this->windowDelegate); - this->windowDelegate = nullptr; - } + if (this->window != nullptr) { + #if SOCKET_RUNTIME_PLATFORM_MACOS + auto contentView = this->window.contentView; + auto subviews = NSMutableArray.array; - if (this->navigationDelegate != nullptr) { - this->navigationDelegate = nullptr; + for (NSView* view in contentView.subviews) { + if (view == this->webview) { + this->webview = nullptr; + } + [view removeFromSuperview]; + [view release]; + } + + [this->window performClose: nullptr]; + this->window = nullptr; + this->window.webview = nullptr; + this->window.delegate = nullptr; + this->window.contentView = nullptr; + + if (this->window.titleBarView) { + [this->window.titleBarView removeFromSuperview]; + #if !__has_feature(objc_arc) + [this->window.titleBarView release]; + #endif + } + + this->window.titleBarView = nullptr; + #endif } } void Window::maximize () { + #if SOCKET_RUNTIME_PLATFORM_MACOS [this->window zoom: this->window]; + #endif } void Window::minimize () { + #if SOCKET_RUNTIME_PLATFORM_MACOS [this->window miniaturize: this->window]; + #endif } void Window::restore () { + #if SOCKET_RUNTIME_PLATFORM_MACOS [this->window deminiaturize: this->window]; + #endif } void Window::hide () { + #if SOCKET_RUNTIME_PLATFORM_MACOS if (this->window) { [this->window orderOut: this->window]; - this->eval(getEmitToRenderProcessJavaScript("windowHide", "{}")); } + #elif SOCKET_RUNTIME_PLATFORM_IOS + if (this->window) { + this->window.hidden = YES; + } + #endif + this->eval(getEmitToRenderProcessJavaScript("window-hidden", "{}")); } - void Window::eval (const SSC::String& js) { - if (this->webview != nullptr) { - auto string = [NSString stringWithUTF8String:js.c_str()]; - [this->webview evaluateJavaScript: string completionHandler: nil]; - } + void Window::eval (const String& source, const EvalCallback& callback) { + App::sharedApplication()->dispatch([=, this]() { + if (this->webview != nullptr) { + [this->webview + evaluateJavaScript: @(source.c_str()) + completionHandler: ^(id result, NSError *error) + { + if (error) { + debug("JavaScriptError: %@", error); + + if (callback != nullptr) { + callback(JSON::Error(error.localizedDescription.UTF8String)); + } + + return; + } + + if (callback != nullptr) { + if ([result isKindOfClass: NSString.class]) { + const auto value = String([result UTF8String]); + if (value == "null" || value == "undefined") { + callback(nullptr); + } else if (value == "true") { + callback(true); + } else if (value == "false") { + callback(value); + } else { + double number = 0.0f; + + try { + number = std::stod(value); + } catch (...) { + callback(value); + return; + } + + callback(number); + } + } else if ([result isKindOfClass: NSNumber.class]) { + callback([result doubleValue]); + } + } + }]; + } + }); } void Window::setSystemMenuItemEnabled (bool enabled, int barPos, int menuPos) { + #if SOCKET_RUNTIME_PLATFORM_MACOS if (!this->window || !this->webview) return; NSMenu* menuBar = [NSApp mainMenu]; NSArray* menuBarItems = [menuBar itemArray]; @@ -1187,62 +891,85 @@ - (void) webView: (WKWebView*) webView [menuItem setTarget: nil]; [menuItem setAction: NULL]; + #endif } - void Window::navigate (const SSC::String& seq, const SSC::String& value) { - auto url = [NSURL URLWithString: [NSString stringWithUTF8String: value.c_str()]]; - - if (url != nullptr && this->webview != nullptr) { - if (String(url.scheme.UTF8String) == "file") { - NSString* allowed = [[NSBundle mainBundle] resourcePath]; - [this->webview loadFileURL: url - allowingReadAccessToURL: [NSURL fileURLWithPath: allowed] - ]; - } else { - auto request = [NSMutableURLRequest requestWithURL: url]; - [this->webview loadRequest: request]; - } + void Window::navigate (const String& value) { + App::sharedApplication()->dispatch([=, this]() { + const auto url = [NSURL URLWithString: @(value.c_str())]; - if (seq.size() > 0) { - auto index = std::to_string(this->opts.index); - this->resolvePromise(seq, "0", index); + if (url != nullptr && this->webview != nullptr) { + if (String(url.scheme.UTF8String) == "file") { + static const auto resourcesPath = FileResource::getResourcesPath(); + [this->webview loadFileURL: url + allowingReadAccessToURL: [NSURL fileURLWithPath: @(resourcesPath.string().c_str())] + ]; + } else { + auto request = [NSMutableURLRequest requestWithURL: url]; + [this->webview loadRequest: request]; + } } - } + }); } - SSC::String Window::getTitle () { - if (this->window) { - return SSC::String([this->window.title UTF8String]); + const String Window::getTitle () const { + #if SOCKET_RUNTIME_PLATFORM_MACOS + if (this->window && this->window.title.UTF8String != nullptr) { + return this->window.title.UTF8String; + } + #elif SOCKET_RUNTIME_PLATFORM_IOS + if (this->viewController && this->viewController.title.UTF8String != nullptr) { + return this->viewController.title.UTF8String; } + #endif return ""; } - void Window::setTitle (const SSC::String& value) { + void Window::setTitle (const String& title) { + #if SOCKET_RUNTIME_PLATFORM_MACOS if (this->window) { - auto title = [NSString stringWithUTF8String:value.c_str()]; - [this->window setTitle: title]; + this->window.title = @(title.c_str()); } + #elif SOCKET_RUNTIME_PLATFORM_IOS + if (this->viewController) { + this->viewController.title = @(title.c_str()); + } + #endif } - ScreenSize Window::getSize () { + Window::Size Window::getSize () { if (this->window == nullptr) { - return ScreenSize {0, 0}; + return Size {0, 0}; } - NSRect e = this->window.frame; + const auto frame = this->window.frame; - this->height = e.size.height; - this->width = e.size.width; + this->size.height = frame.size.height; + this->size.width = frame.size.width; - return ScreenSize { - .height = (int) e.size.height, - .width = (int) e.size.width + return Size { + .width = (int) frame.size.width, + .height = (int) frame.size.height + }; + } + + const Window::Size Window::getSize () const { + if (this->window == nullptr) { + return Size {0, 0}; + } + + const auto frame = this->window.frame; + + return Size { + .width = (int) frame.size.width, + .height = (int) frame.size.height }; } void Window::setSize (int width, int height, int hints) { if (this->window) { + #if SOCKET_RUNTIME_PLATFORM_MACOS [this->window setFrame: NSMakeRect(0.f, 0.f, (float) width, (float) height) display: YES @@ -1250,54 +977,140 @@ - (void) webView: (WKWebView*) webView ]; [this->window center]; - - this->height = height; - this->width = width; + #elif SOCKET_RUNTIME_PLATFORM_IOS + auto frame = this->window.frame; + frame.size.width = width; + frame.size.height = height; + this->window.frame = frame; + #endif } + + this->size.height = height; + this->size.width = width; } - int Window::openExternal (const SSC::String& s) { - NSString* nsu = [NSString stringWithUTF8String:s.c_str()]; - return [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: nsu]]; + void Window::setPosition (float x, float y) { + if (this->window) { + #if SOCKET_RUNTIME_PLATFORM_MACOS + const auto point = NSPointFromCGPoint(CGPointMake(x, y)); + this->window.frameTopLeftPoint = point; + #elif SOCKET_RUNTIME_PLATFORM_IOS + auto frame = this->window.frame; + frame.origin.x = x; + frame.origin.y = y; + this->window.frame = frame; + #endif + } + + this->position.x = x; + this->position.y = y; } void Window::closeContextMenu () { - // @TODO(jwerle) + #if SOCKET_RUNTIME_PLATFORM_MACOS + // TODO(@jwerle) + #endif } - void Window::closeContextMenu (const SSC::String &seq) { - // @TODO(jwerle) + void Window::closeContextMenu (const String &instanceId) { + #if SOCKET_RUNTIME_PLATFORM_MACOS + // TODO(@jwerle) + #endif } void Window::showInspector () { + #if SOCKET_RUNTIME_PLATFORM_MACOS if (this->webview) { // This is a private method on the webview, so we need to use // the pragma keyword to suppress the access warning. #pragma clang diagnostic ignored "-Wobjc-method-access" [[this->webview _inspector] show]; } + #endif + } + + void Window::setBackgroundColor (const String& rgbaString) { + NSString *rgba = @(rgbaString.c_str()); + NSRegularExpression *regex = + [NSRegularExpression regularExpressionWithPattern: @"rgba\\((\\d+),\\s*(\\d+),\\s*(\\d+),\\s*([\\d.]+)\\)" + options: NSRegularExpressionCaseInsensitive + error: nil]; + + NSTextCheckingResult *rgbaMatch = + [regex firstMatchInString: rgba + options: 0 + range: NSMakeRange(0, [rgba length])]; + + if (rgbaMatch) { + int r = [[rgba substringWithRange:[rgbaMatch rangeAtIndex:1]] intValue]; + int g = [[rgba substringWithRange:[rgbaMatch rangeAtIndex:2]] intValue]; + int b = [[rgba substringWithRange:[rgbaMatch rangeAtIndex:3]] intValue]; + float a = [[rgba substringWithRange:[rgbaMatch rangeAtIndex:4]] floatValue]; + + this->setBackgroundColor(r, g, b, a); + } else { + debug("invalid arguments for window background color"); + } } void Window::setBackgroundColor (int r, int g, int b, float a) { if (this->window) { - CGFloat sRGBComponents[4] = { r / 255.0, g / 255.0, b / 255.0, a }; - NSColorSpace *colorSpace = [NSColorSpace sRGBColorSpace]; + CGFloat rgba[4] = { r / 255.0, g / 255.0, b / 255.0, a }; + #if SOCKET_RUNTIME_PLATFORM_MACOS + [this->window setBackgroundColor: [NSColor + colorWithColorSpace: NSColorSpace.sRGBColorSpace + components: rgba + count: 4 + ]]; + #elif SOCKET_RUNTIME_PLATFORM_IOS + auto color = [UIColor + colorWithRed: rgba[0] + green: rgba[1] + blue: rgba[2] + alpha: rgba[3] + ]; - [this->window setBackgroundColor: - [NSColor colorWithColorSpace: colorSpace - components: sRGBComponents - count: 4] + [this->window setBackgroundColor: color]; + [this->webview setBackgroundColor: color]; + #endif + } + } + + String Window::getBackgroundColor () { + if (this->window) { + const auto backgroundColor = this->window.backgroundColor; + CGFloat r, g, b, a; + #if SOCKET_RUNTIME_PLATFORM_MACOS + const auto rgba = [backgroundColor colorUsingColorSpace: NSColorSpace.sRGBColorSpace]; + #elif SOCKET_RUNTIME_PLATFORM_IOS + const auto rgba = [backgroundColor colorWithAlphaComponent: 1]; + #endif + + [rgba getRed: &r green: &g blue: &b alpha: &a]; + + const auto string = [NSString + stringWithFormat: @"rgba(%.0f,%.0f,%.0f,%.1f)", + r * 255, + g * 255, + b * 255, + a ]; + + return string.UTF8String; } + + return ""; + } - void Window::setContextMenu (const SSC::String& seq, const SSC::String& value) { + void Window::setContextMenu (const String& instanceId, const String& menuSource) { + #if SOCKET_RUNTIME_PLATFORM_MACOS const auto mouseLocation = NSEvent.mouseLocation; const auto contextMenu = [[NSMenu.alloc initWithTitle: @"contextMenu"] autorelease]; const auto location = NSPointFromCGPoint(CGPointMake(mouseLocation.x, mouseLocation.y)); - const auto menuItems = split(value, '_'); + const auto menuItems = split(menuSource, '\n'); // remove the 'R' prefix as we'll use this value in the menu item "tag" property - const auto id = std::stoi(seq.substr(1)); + const auto id = std::stoi(instanceId.starts_with("R") ? instanceId.substr(1) : instanceId); // context menu item index int index = 0; @@ -1347,19 +1160,24 @@ - (void) webView: (WKWebView*) webView atLocation: location inView: nil ]; + #endif } - void Window::setTrayMenu (const SSC::String& seq, const SSC::String& value) { - this->setMenu(seq, value, true); + void Window::setTrayMenu (const String& value) { + this->setMenu(value, true); } - void Window::setSystemMenu (const SSC::String& seq, const SSC::String& value) { - this->setMenu(seq, value, false); + void Window::setSystemMenu (const String& value) { + this->setMenu(value, false); } - void Window::setMenu (const SSC::String& seq, const SSC::String& source, const bool& isTrayMenu) { - if (source.empty()) return void(0); - SSC::String menuSource = replace(SSC::String(source), "%%", "\n"); + void Window::setMenu (const String& menuSource, const bool& isTrayMenu) { + #if SOCKET_RUNTIME_PLATFORM_MACOS + auto app = App::sharedApplication(); + + if (!app || menuSource.empty()) { + return; + } NSStatusItem *statusItem; NSString *title; @@ -1373,7 +1191,7 @@ - (void) webView: (WKWebView*) webView NSMenuItem *menuItem; menu = [[NSMenu alloc] init]; - // menu = [[NSMenu alloc] initWithTitle:@""]; + // menu = [[NSMenu alloc] initWithTitle: @""]; // id appName = [[NSProcessInfo processInfo] processName]; // title = [@"About " stringByAppendingString:appName]; @@ -1424,7 +1242,7 @@ - (void) webView: (WKWebView*) webView auto parts = split(line, ':'); auto title = parts[0]; NSUInteger mask = 0; - SSC::String key = ""; + String key = ""; if (title.size() > 0 && title.find("!") == 0) { title = title.substr(1); @@ -1502,14 +1320,16 @@ - (void) webView: (WKWebView*) webView } if (title.find("Quit") == 0) { - // nssSelector = [NSString stringWithUTF8String:"terminate:"]; + nssSelector = [NSString stringWithUTF8String:"terminate:"]; } if (title.compare("Minimize") == 0) { nssSelector = [NSString stringWithUTF8String:"performMiniaturize:"]; } - // if (title.compare("Zoom") == 0) nssSelector = [NSString stringWithUTF8String:"performZoom:"]; + if (title.compare("Maximize") == 0) { + nssSelector = [NSString stringWithUTF8String:"performZoom:"]; + } if (title.find("---") != -1) { [ctx addItem: [NSMenuItem separatorItem]]; @@ -1526,16 +1346,16 @@ - (void) webView: (WKWebView*) webView } if (isDisabled) { - [menuItem setTarget:nil]; - [menuItem setAction:NULL]; + [menuItem setTarget: nil]; + [menuItem setAction: NULL]; } - [menuItem setTag:0]; // only contextMenu uses the tag + [menuItem setTag: 0]; // only contextMenu uses the tag } if (!isTrayMenu) { // create a top level menu item - menuItem = [[NSMenuItem alloc] initWithTitle:nssTitle action:nil keyEquivalent:@""]; + menuItem = [[NSMenuItem alloc] initWithTitle:nssTitle action:nil keyEquivalent: @""]; [menu addItem: menuItem]; // set its submenu [menuItem setSubmenu: ctx]; @@ -1545,48 +1365,55 @@ - (void) webView: (WKWebView*) webView } if (isTrayMenu) { - [menu setTitle: nssTitle]; - [menu setDelegate: (id)this->app.delegate]; // bring the main window to the front when clicked - [menu setAutoenablesItems: NO]; + menu.title = nssTitle; + menu.delegate = (id) app->applicationDelegate; // bring the main window to the front when clicked + menu.autoenablesItems = NO; - auto userConfig = SSC::getUserConfig(); + auto userConfig = getUserConfig(); auto bundlePath = [[[NSBundle mainBundle] resourcePath] UTF8String]; - auto cwd = SSC::fs::path(bundlePath); + auto cwd = fs::path(bundlePath); auto trayIconPath = String("application_tray_icon"); - if (SSC::fs::exists(SSC::fs::path(cwd) / (trayIconPath + ".png"))) { - trayIconPath = (SSC::fs::path(cwd) / (trayIconPath + ".png")).string(); - } else if (SSC::fs::exists(SSC::fs::path(cwd) / (trayIconPath + ".jpg"))) { - trayIconPath = (SSC::fs::path(cwd) / (trayIconPath + ".jpg")).string(); - } else if (SSC::fs::exists(SSC::fs::path(cwd) / (trayIconPath + ".jpeg"))) { - trayIconPath = (SSC::fs::path(cwd) / (trayIconPath + ".jpeg")).string(); - } else if (SSC::fs::exists(SSC::fs::path(cwd) / (trayIconPath + ".ico"))) { - trayIconPath = (SSC::fs::path(cwd) / (trayIconPath + ".ico")).string(); + if (fs::exists(fs::path(cwd) / (trayIconPath + ".png"))) { + trayIconPath = (fs::path(cwd) / (trayIconPath + ".png")).string(); + } else if (fs::exists(fs::path(cwd) / (trayIconPath + ".jpg"))) { + trayIconPath = (fs::path(cwd) / (trayIconPath + ".jpg")).string(); + } else if (fs::exists(fs::path(cwd) / (trayIconPath + ".jpeg"))) { + trayIconPath = (fs::path(cwd) / (trayIconPath + ".jpeg")).string(); + } else if (fs::exists(fs::path(cwd) / (trayIconPath + ".ico"))) { + trayIconPath = (fs::path(cwd) / (trayIconPath + ".ico")).string(); } else { trayIconPath = ""; } - NSString *imagePath = [NSString stringWithUTF8String: trayIconPath.c_str()]; - NSImage *image = [[NSImage alloc] initWithContentsOfFile: imagePath]; - NSSize newSize = NSMakeSize(32, 32); - NSImage *resizedImage = [[NSImage alloc] initWithSize:newSize]; + const auto image = [NSImage.alloc initWithContentsOfFile: @(trayIconPath.c_str())]; + const auto newSize = NSMakeSize(32, 32); + const auto resizedImage = [NSImage.alloc initWithSize: newSize]; + [resizedImage lockFocus]; - [image drawInRect:NSMakeRect(0, 0, newSize.width, newSize.height) fromRect:NSZeroRect operation:NSCompositingOperationCopy fraction:1.0]; + [image + drawInRect: NSMakeRect(0, 0, newSize.width, newSize.height) + fromRect: NSZeroRect + operation: NSCompositingOperationCopy + fraction: 1.0 + ]; [resizedImage unlockFocus]; - if (image) this->app.delegate.statusItem.button.image = resizedImage; - - [this->app.delegate.statusItem.button setToolTip: nssTitle]; - [this->app.delegate.statusItem setMenu: menu]; - [this->app.delegate.statusItem retain]; + app->applicationDelegate.statusItem.menu = menu; + app->applicationDelegate.statusItem.button.image = resizedImage; + app->applicationDelegate.statusItem.button.toolTip = nssTitle; + [app->applicationDelegate.statusItem retain]; } else { [NSApp setMainMenu: menu]; } + #endif + } - if (seq.size() > 0) { - auto index = std::to_string(this->opts.index); - this->resolvePromise(seq, "0", index); - } + void Window::handleApplicationURL (const String& url) { + JSON::Object json = JSON::Object::Entries {{ + "url", url + }}; + + this->bridge.emit("applicationurl", json.str()); } } -#endif diff --git a/src/window/dialog.cc b/src/window/dialog.cc index e760b348cd..435aadab6c 100644 --- a/src/window/dialog.cc +++ b/src/window/dialog.cc @@ -1,21 +1,26 @@ +#include "../app/app.hh" #include "../core/debug.hh" + #include "window.hh" -#if defined(__APPLE__) && TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +using namespace SSC; + +#if SOCKET_RUNTIME_PLATFORM_IOS @implementation SSCUIPickerDelegate : NSObject - (void) documentPicker: (UIDocumentPickerViewController*) controller didPickDocumentsAtURLs: (NSArray<NSURL*>*) urls { + Vector<String> paths; for (NSURL* url in urls) { if (url.isFileURL) { - self.dialog->delegatedResults.push_back(url.path.UTF8String); + paths.push_back(url.path.UTF8String); } } - self.dialog->delegateMutex.unlock(); + self.dialog->callback(paths); } - (void) documentPickerWasCancelled: (UIDocumentPickerViewController*) controller { - self.dialog->delegateMutex.unlock(); + self.dialog->callback(Vector<String>()); } - (void) imagePickerController: (UIImagePickerController*) picker @@ -23,33 +28,36 @@ { NSURL* mediaURL = info[UIImagePickerControllerMediaURL]; NSURL* imageURL = info[UIImagePickerControllerImageURL]; + Vector<String> paths; if (mediaURL != nullptr) { - self.dialog->delegatedResults.push_back(mediaURL.path.UTF8String); + paths.push_back(mediaURL.path.UTF8String); } else { - self.dialog->delegatedResults.push_back(imageURL.path.UTF8String); + paths.push_back(imageURL.path.UTF8String); } - self.dialog->delegateMutex.unlock(); [picker dismissViewControllerAnimated: YES completion: nullptr]; + self.dialog->callback(paths); } - (void) imagePickerControllerDidCancel: (UIImagePickerController*) picker { - self.dialog->delegateMutex.unlock(); + self.dialog->callback(Vector<String>()); } @end #endif namespace SSC { - Dialog::Dialog () { - #if defined(__APPLE__) && TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + Dialog::Dialog (Window* window) + : window(window) + { + #if SOCKET_RUNTIME_PLATFORM_IOS this->uiPickerDelegate = [SSCUIPickerDelegate new]; this->uiPickerDelegate.dialog = this; #endif } Dialog::~Dialog () { - #if defined(__APPLE__) && TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + #if SOCKET_RUNTIME_PLATFORM_IOS #if !__has_feature(objc_arc) [this->uiPickerDelegate release]; #endif @@ -57,10 +65,12 @@ namespace SSC { #endif } - String Dialog::showSaveFilePicker ( - const FileSystemPickerOptions& options + bool Dialog::showSaveFilePicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback ) { - const auto results = this->showFileSystemPicker({ + return this->showFileSystemPicker({ + .prefersDarkMode = options.prefersDarkMode, .directories = false, .multiple = false, .files = true, @@ -69,20 +79,15 @@ namespace SSC { .defaultName = options.defaultName, .defaultPath = options.defaultPath, .title = options.title - }); - - if (results.size() == 1) { - return results[0]; - } - - return ""; + }, callback); } - - Vector<String> Dialog::showOpenFilePicker ( - const FileSystemPickerOptions& options + bool Dialog::showOpenFilePicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback ) { return this->showFileSystemPicker({ + .prefersDarkMode = options.prefersDarkMode, .directories = false, .multiple = options.multiple, .files = true, @@ -91,13 +96,15 @@ namespace SSC { .defaultName = options.defaultName, .defaultPath = options.defaultPath, .title = options.title - }); + }, callback); } - Vector<String> Dialog::showDirectoryPicker ( - const FileSystemPickerOptions& options + bool Dialog::showDirectoryPicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback ) { return this->showFileSystemPicker({ + .prefersDarkMode = options.prefersDarkMode, .directories = true, .multiple = options.multiple, .files = false, @@ -106,11 +113,12 @@ namespace SSC { .defaultName = options.defaultName, .defaultPath = options.defaultPath, .title = options.title - }); + }, callback); } - Vector<String> Dialog::showFileSystemPicker ( - const FileSystemPickerOptions& options + bool Dialog::showFileSystemPicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback ) { const auto isSavePicker = options.type == FileSystemPickerOptions::Type::Save; const auto allowDirectories = options.directories; @@ -119,10 +127,13 @@ namespace SSC { const auto defaultName = options.defaultName; const auto defaultPath = options.defaultPath; const auto title = options.title; + const auto app = App::sharedApplication(); Vector<String> paths; - #if defined(__APPLE__) + this->callback = callback; + + #if SOCKET_RUNTIME_PLATFORM_APPLE // state NSMutableArray<UTType *>* contentTypes = [NSMutableArray new]; NSString* suggestedFilename = nullptr; @@ -143,7 +154,7 @@ namespace SSC { // <mime>:<ext>,<ext>|<mime>:<ext>|... for (const auto& contentTypeSpec : split(options.contentTypes, "|")) { const auto parts = split(contentTypeSpec, ":"); - const auto mime = parts[0]; + const auto mime = trim(parts[0]); const auto classes = split(mime, "/"); UTType* supertype = nullptr; @@ -168,7 +179,7 @@ namespace SSC { supertype = UTTypeVideo; prefersMedia = true; } else if (classes[0] == "*") { - supertype = UTTypeContent; + supertype = UTTypeData; } else { supertype = UTTypeCompositeContent; } @@ -193,13 +204,13 @@ namespace SSC { const auto extensions = split(parts[1], ","); for (const auto& extension : extensions) { - [contentTypes - addObjectsFromArray: [UTType - typesWithTag: @(extension.c_str()) - tagClass: UTTagClassFilenameExtension - conformingToType: supertype - ] + auto types = [UTType + typesWithTag: @(extension.c_str()) + tagClass: UTTagClassFilenameExtension + conformingToType: supertype ]; + + [contentTypes addObjectsFromArray: types]; } } } @@ -209,49 +220,47 @@ namespace SSC { } } - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - this->delegateMutex.lock(); - dispatch_async(dispatch_get_main_queue(), ^{ - UIWindow* window = nullptr; + #if SOCKET_RUNTIME_PLATFORM_IOS + UIWindow* window = nullptr; - if (@available(iOS 15.0, *)) { - auto scene = (UIWindowScene*) UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; - window = scene.windows.lastObject; - } else { - window = UIApplication.sharedApplication.windows.lastObject; - } + if (this->window) { + window = this->window->window; + } else if (@available(iOS 15.0, *)) { + auto scene = (UIWindowScene*) UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; + window = scene.windows.lastObject; + } else { + window = UIApplication.sharedApplication.windows.lastObject; + } - if (prefersMedia) { - auto picker = [UIImagePickerController new]; - NSMutableArray<NSString*>* mediaTypes = [NSMutableArray new]; + if (prefersMedia) { + auto picker = [UIImagePickerController new]; + NSMutableArray<NSString*>* mediaTypes = [NSMutableArray new]; - picker.delegate = this->uiPickerDelegate; + picker.delegate = this->uiPickerDelegate; - [window.rootViewController - presentViewController: picker - animated: YES - completion: nullptr - ]; - } else { - auto picker = [UIDocumentPickerViewController.alloc - initForOpeningContentTypes: contentTypes - ]; + [window.rootViewController + presentViewController: picker + animated: YES + completion: nullptr + ]; + } else { + auto picker = [UIDocumentPickerViewController.alloc + initForOpeningContentTypes: contentTypes + ]; - picker.allowsMultipleSelection = allowMultiple ? YES : NO; - picker.modalPresentationStyle = UIModalPresentationFormSheet; - picker.directoryURL = directoryURL; - picker.delegate = this->uiPickerDelegate; + picker.allowsMultipleSelection = allowMultiple ? YES : NO; + picker.modalPresentationStyle = UIModalPresentationFormSheet; + picker.directoryURL = directoryURL; + picker.delegate = this->uiPickerDelegate; - [window.rootViewController - presentViewController: picker - animated: YES - completion: nullptr - ]; - } - }); + [window.rootViewController + presentViewController: picker + animated: YES + completion: nullptr + ]; + } - std::lock_guard<std::mutex> lock(this->delegateMutex); - paths = this->delegatedResults; + return true; #else NSAutoreleasePool* pool = [NSAutoreleasePool new]; @@ -324,12 +333,16 @@ namespace SSC { } [pool release]; + app->dispatch([=, this] () { + callback(paths); + }); + return true; #endif #elif defined(__linux__) && !defined(__ANDROID__) const guint SELECT_RESPONSE = 0; GtkFileChooserAction action; GtkFileChooser *chooser; - GtkFileFilter *filter; // TODO(@jwerle): `gtk_file_filter_add_custom, gtk_file_chooser_add_filter` + GtkFileFilter *filter; GtkWidget *dialog; Vector<GtkFileFilter*> filters; @@ -344,12 +357,43 @@ namespace SSC { } } - return FALSE; + return false; }; + g_object_set( + gtk_settings_get_default(), + "gtk-application-prefer-dark-theme", + options.prefersDarkMode, + nullptr + ); + + if (isSavePicker) { + action = GTK_FILE_CHOOSER_ACTION_SAVE; + } else { + action = GTK_FILE_CHOOSER_ACTION_OPEN; + } + + if (!allowFiles && allowDirectories) { + action = (GtkFileChooserAction) (action | GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER); + } + + String dialogTitle = isSavePicker ? "Save File" : "Open File"; + if (title.size() > 0) { + dialogTitle = title; + } + + dialog = gtk_file_chooser_dialog_new( + dialogTitle.c_str(), + nullptr, + action, + "_Cancel", + GTK_RESPONSE_CANCEL, + nullptr + ); + for (const auto& contentTypeSpec : split(options.contentTypes, "|")) { const auto parts = split(contentTypeSpec, ":"); - const auto mime = parts[0]; + const auto mime = trim(parts[0]); const auto classes = split(mime, "/"); // malformed MIME @@ -358,6 +402,7 @@ namespace SSC { } auto filter = gtk_file_filter_new(); + Set<String> filterExtensionPatterns; #define MAKE_FILTER(userData) \ gtk_file_filter_add_custom( \ @@ -395,40 +440,21 @@ namespace SSC { extension ); + filterExtensionPatterns.insert(pattern); + gtk_file_filter_add_pattern(filter, pattern.c_str()); } } + gtk_file_filter_set_name( + filter, + join(filterExtensionPatterns, ", ").c_str() + ); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter); filters.push_back(filter); } - if (isSavePicker) { - action = GTK_FILE_CHOOSER_ACTION_SAVE; - } else { - action = GTK_FILE_CHOOSER_ACTION_OPEN; - } - - if (!allowFiles && allowDirectories) { - action = (GtkFileChooserAction) (action | GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER); - } - - gtk_init_check(nullptr, nullptr); - - String dialogTitle = isSavePicker ? "Save File" : "Open File"; - if (title.size() > 0) { - dialogTitle = title; - } - - dialog = gtk_file_chooser_dialog_new( - dialogTitle.c_str(), - nullptr, - action, - "_Cancel", - GTK_RESPONSE_CANCEL, - nullptr - ); - chooser = GTK_FILE_CHOOSER(dialog); if (!allowDirectories) { @@ -443,9 +469,7 @@ namespace SSC { gtk_dialog_add_button(GTK_DIALOG(dialog), "Select", SELECT_RESPONSE); } - // if (FILE_DIALOG_OVERWRITE_CONFIRMATION) { gtk_file_chooser_set_do_overwrite_confirmation(chooser, true); - // } if ((!isSavePicker || allowDirectories) && allowMultiple) { gtk_file_chooser_set_select_multiple(chooser, true); @@ -471,17 +495,15 @@ namespace SSC { } } + // FIXME(@jwerle, @heapwolf): hitting 'ESC' or cancelling the dialog + // causes 'assertion 'G_IS_OBJECT (object)' failed' messages guint response = gtk_dialog_run(GTK_DIALOG(dialog)); - for (const auto& filter : filters) { - g_object_unref(filter); - } - filters.clear(); if (response != GTK_RESPONSE_ACCEPT && response != SELECT_RESPONSE) { gtk_widget_destroy(dialog); - return paths; + return false; } // TODO (@heapwolf): validate multi-select @@ -493,14 +515,19 @@ namespace SSC { GSList* filenames = gtk_file_chooser_get_filenames(chooser); for (int i = 0; filenames != nullptr; ++i) { - const auto file = (const char*) filenames->data; - paths.push_back(file); + const auto filename = (const char*) filenames->data; + paths.push_back(filename); + g_free(const_cast<char*>(reinterpret_cast<const char*>(filename))); filenames = filenames->next; } g_slist_free(filenames); - gtk_widget_destroy(dialog); - #elif defined(_WIN32) + gtk_widget_destroy(GTK_WIDGET(dialog)); + app->dispatch([=]() { + callback(paths); + }); + return true; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS IShellItemArray *openResults; IShellItem *saveResult; DWORD dialogOptions; @@ -520,7 +547,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: CoInitializeEx() failed in 'showFileSystemPicker()'"); - return paths; + return false; } // create IFileDialog instance (IFileOpenDialog or IFileSaveDialog) @@ -535,7 +562,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: CoCreateInstance() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } else { result = CoCreateInstance( @@ -548,7 +575,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: CoCreateInstance() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } @@ -561,7 +588,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::GetOptions() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } if (allowDirectories == true && allowFiles == false) { @@ -574,7 +601,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::SetOptions(FOS_PICKFOLDERS) failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } @@ -584,7 +611,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::SetOptions(FOS_ALLOWMULTISELECT) failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } @@ -602,7 +629,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: SHCreateItemFromParsingName() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } if (isSavePicker) { @@ -614,7 +641,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::SetDefaultFolder() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } @@ -632,7 +659,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::SetTitle() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } @@ -650,7 +677,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::SetFileName() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } @@ -663,7 +690,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::Show() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } if (isSavePicker) { @@ -672,7 +699,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::GetResult() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } else { result = dialog.open->GetResults(&openResults); @@ -680,14 +707,15 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IFileDialog::GetResults() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } } if (FAILED(result)) { debug("ERR: IFileDialog::Show() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + callback(paths); + return false; } if (isSavePicker) { @@ -698,10 +726,10 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IShellItem::GetDisplayName() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } - paths.push_back(SSC::convertWStringToString(WString(buf))); + paths.push_back(convertWStringToString(WString(buf))); saveResult->Release(); CoTaskMemFree(buf); @@ -711,7 +739,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IShellItemArray::GetCount() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } for (DWORD i = 0; i < totalResults; i++) { @@ -723,7 +751,7 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IShellItemArray::GetItemAt() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } result = path->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &buf); @@ -731,10 +759,10 @@ namespace SSC { if (FAILED(result)) { debug("ERR: IShellItem::GetDisplayName() failed in 'showFileSystemPicker()'"); CoUninitialize(); - return paths; + return false; } - paths.push_back(SSC::convertWStringToString(WString(buf))); + paths.push_back(convertWStringToString(WString(buf))); path->Release(); CoTaskMemFree(buf); } @@ -751,8 +779,108 @@ namespace SSC { } CoUninitialize(); + app->dispatch([=]() { + callback(paths); + }); + return true; + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + const auto dialog = CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + app->core->platform.activity, + "getDialog", + "()Lsocket/runtime/window/Dialog;" + ); + + // construct the mime types into a packed string + String mimeTypes; + // <mime>:<ext>,<ext>|<mime>:<ext>|... + for (const auto& contentTypeSpec : split(options.contentTypes, "|")) { + const auto parts = split(contentTypeSpec, ":"); + const auto mime = trim(parts[0]); + const auto classes = split(mime, "/"); + if (classes.size() == 2) { + if (mimeTypes.size() == 0) { + mimeTypes = mime; + } else { + mimeTypes += "|" + mime; + } + } + } + + // we'll set the pointer from this instance in this call so + // the `onResults` can reinterpret the `jlong` back into a `Dialog*` + CallVoidClassMethodFromAndroidEnvironment( + attachment.env, + dialog, + "showFileSystemPicker", + "(Ljava/lang/String;ZZZJ)V", + attachment.env->NewStringUTF(mimeTypes.c_str()), + allowDirectories, + allowMultiple, + allowFiles, + reinterpret_cast<jlong>(this) + ); + + return true; #endif - return paths; + return false; } } + +#if SOCKET_RUNTIME_PLATFORM_ANDROID +extern "C" { + void ANDROID_EXTERNAL(window, Dialog, onResults) ( + JNIEnv* env, + jobject self, + jlong pointer, + jobjectArray results + ) { + const auto app = App::sharedApplication(); + + if (!app) { + return ANDROID_THROW(env, "Missing 'App' in environment"); + } + + const auto dialog = reinterpret_cast<Dialog*>(pointer); + + if (!dialog) { + return ANDROID_THROW( + env, + "Missing 'Dialog' in results callback from 'showFileSystemPicker'" + ); + } + + if (dialog->callback == nullptr) { + return ANDROID_THROW( + env, + "Missing 'Dialog' callback in results callback from 'showFileSystemPicker'" + ); + } + + const auto attachment = Android::JNIEnvironmentAttachment(app->jvm); + const auto length = attachment.env->GetArrayLength(results); + + Vector<String> paths; + + for (int i = 0; i < length; ++i) { + const auto uri = (jstring) attachment.env->GetObjectArrayElement(results, i); + if (uri) { + const auto string = Android::StringWrap(attachment.env, CallObjectClassMethodFromAndroidEnvironment( + attachment.env, + uri, + "toString", + "()Ljava/lang/String;" + )).str(); + + paths.push_back(string); + } + } + + const auto callback = dialog->callback; + dialog->callback = nullptr; + callback(paths); + } +} +#endif diff --git a/src/window/dialog.hh b/src/window/dialog.hh new file mode 100644 index 0000000000..334151d7ec --- /dev/null +++ b/src/window/dialog.hh @@ -0,0 +1,91 @@ +#ifndef SOCKET_RUNTIME_WINDOW_DIALOG_H +#define SOCKET_RUNTIME_WINDOW_DIALOG_H + +#include "../platform/platform.hh" + +namespace SSC { + // forward + class Window; + class Dialog; +} + +#if SOCKET_RUNTIME_PLATFORM_IOS +@interface SSCUIPickerDelegate : NSObject< + UIDocumentPickerDelegate, + + // TODO(@jwerle): use 'PHPickerViewControllerDelegate' instead + UIImagePickerControllerDelegate, + UINavigationControllerDelegate +> + @property (nonatomic) SSC::Dialog* dialog; + + // UIDocumentPickerDelegate + - (void) documentPicker: (UIDocumentPickerViewController*) controller + didPickDocumentsAtURLs: (NSArray<NSURL*>*) urls; + - (void) documentPickerWasCancelled: (UIDocumentPickerViewController*) controller; + + // UIImagePickerControllerDelegate + - (void) imagePickerController: (UIImagePickerController*) picker + didFinishPickingMediaWithInfo: (NSDictionary<UIImagePickerControllerInfoKey, id>*) info; + - (void) imagePickerControllerDidCancel: (UIImagePickerController*) picker; +@end +#endif + +namespace SSC { + class Dialog { + public: + struct FileSystemPickerOptions { + enum class Type { Open, Save }; + bool prefersDarkMode = false; + bool directories = false; + bool multiple = false; + bool files = false; + Type type = Type::Open; + String contentTypes; + String defaultName; + String defaultPath; + String title; + }; + + using ShowCallback = Function<void(Vector<String>)>; + + #if SOCKET_RUNTIME_PLATFORM_IOS + SSCUIPickerDelegate* uiPickerDelegate = nullptr; + Vector<String> delegatedResults; + std::mutex delegateMutex; + #endif + + ShowCallback callback = nullptr; + Window* window = nullptr; + + Dialog (Window* window); + Dialog () = default; + Dialog (const Dialog&) = delete; + Dialog (Dialog&&) = delete; + ~Dialog (); + + Dialog& operator = (const Dialog&) = delete; + Dialog& operator = (Dialog&&) = delete; + + bool showSaveFilePicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback + ); + + bool showOpenFilePicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback + ); + + bool showDirectoryPicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback + ); + + bool showFileSystemPicker ( + const FileSystemPickerOptions& options, + const ShowCallback& callback + ); + }; +} +#endif diff --git a/src/window/dialog.kt b/src/window/dialog.kt new file mode 100644 index 0000000000..5428848538 --- /dev/null +++ b/src/window/dialog.kt @@ -0,0 +1,158 @@ +package socket.runtime.window + +import java.lang.Runtime + +import android.content.Intent +import android.net.Uri +import android.Manifest +import android.webkit.WebChromeClient + +import androidx.activity.result.contract.ActivityResultContracts + +import socket.runtime.core.console +import socket.runtime.window.WindowManagerActivity + +/** + * XXX + */ +open class Dialog (val activity: WindowManagerActivity) { + /** + * XXX + */ + open class FileSystemPickerOptions ( + val params: WebChromeClient.FileChooserParams? = null, + val mimeTypes: MutableList<String> = mutableListOf<String>(), + val directories: Boolean = false, + val multiple: Boolean = false, + val files: Boolean = true + ) { + init { + if (params != null && params.acceptTypes.size > 0) { + this.mimeTypes += params.acceptTypes + } + } + } + + var callback: ((results: Array<Uri>) -> Unit)? = null + + val launcherForSingleItem = activity.registerForActivityResult( + ActivityResultContracts.GetContent(), + fun (uri: Uri?) { this.resolve(uri) } + ) + + val launcherForMulitpleItems = activity.registerForActivityResult( + ActivityResultContracts.GetMultipleContents(), + { uris -> this.resolve(uris) } + ) + + // XXX(@jwerle): unused at the moment + val launcherForSingleDocument = activity.registerForActivityResult( + ActivityResultContracts.OpenDocument(), + { uri -> this.resolve(uri) } + ) + + // XXX(@jwerle): unused at the moment + val launcherForMulitpleDocuments = activity.registerForActivityResult( + ActivityResultContracts.OpenMultipleDocuments(), + { uris -> this.resolve(uris) } + ) + + // XXX(@jwerle): unused at the moment + val launcherForSingleVisualMedia = activity.registerForActivityResult( + ActivityResultContracts.PickVisualMedia(), + { uri -> this.resolve(uri) } + ) + + // XXX(@jwerle): unused at the moment + val launcherForMultipleVisualMedia = activity.registerForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia(), + { uris -> this.resolve(uris) } + ) + + fun resolve (uri: Uri?) { + if (uri != null) { + return this.resolve(arrayOf(uri)) + } + + return this.resolve(arrayOf<Uri>()) + } + + fun resolve (uris: List<Uri>) { + this.resolve(Array<Uri>(uris.size, { i -> uris[i] })) + } + + fun resolve (uris: Array<Uri>) { + val callback = this.callback + + /* + for (uri in uris) { + this.activity.applicationContext.contentResolver.takePersistableUriPermission( + uri, + ( + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + ) + } + */ + + if (callback != null) { + this.callback = null + callback(uris) + } + } + + fun showFileSystemPicker ( + options: FileSystemPickerOptions, + callback: ((Array<Uri>) -> Unit)? = null + ) { + val activity = this.activity as socket.runtime.app.AppActivity + val mimeType = + if (options.mimeTypes.size > 0 && options.mimeTypes[0].length > 0) { + options.mimeTypes[0] + } else { "*/*" } + + this.callback = callback + + val permissions = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + + activity.requestPermissions(permissions, { _ -> + activity.runOnUiThread { + // TODO(@jwerle): support the other launcher types above + // through the `showFileSystemPicker()` method some how + if (options.multiple) { + launcherForMulitpleItems.launch(mimeType) + } else { + //launcherForSingleDocument.launch(arrayOf(mimeType)) + launcherForSingleItem.launch(mimeType) + } + } + }) + } + + fun showFileSystemPicker ( + mimeTypes: String, + directories: Boolean = false, + multiple: Boolean = false, + files: Boolean = true, + pointer: Long = 0 + ) { + return this.showFileSystemPicker(FileSystemPickerOptions( + null, + mimeTypes.split("|").toMutableList(), + directories, + multiple, + files + ), fun (uris: Array<Uri>) { + if (pointer != 0L) { + this.onResults(pointer, uris) + } + }) + } + + @Throws(Exception::class) + external fun onResults (pointer: Long, results: Array<Uri>): Unit +} diff --git a/src/window/hotkey.cc b/src/window/hotkey.cc index 3180a4ba8c..abc861d4f6 100644 --- a/src/window/hotkey.cc +++ b/src/window/hotkey.cc @@ -1,8 +1,9 @@ -#include "hotkey.hh" -#include "window.hh" -#include "../core/string.hh" #include "../core/json.hh" #include "../ipc/ipc.hh" +#include "../app/app.hh" + +#include "hotkey.hh" +#include "window.hh" namespace SSC { // bindings are global to the entire application so we maintain these @@ -10,7 +11,7 @@ namespace SSC { static HotKeyBinding::ID nextGlobalBindingID = 1024; static HotKeyContext::Bindings globalBindings; -#if defined(__APPLE__) && (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) +#if SOCKET_RUNTIME_PLATFORM_MACOS static OSStatus carbonEventHandlerCallback ( EventHandlerCallRef eventHandlerCallRef, EventRef eventRef, @@ -26,10 +27,10 @@ namespace SSC { return eventNotHandledErr; } - auto context = reinterpret_cast<HotKeyContext*>(userData); + const auto context = reinterpret_cast<HotKeyContext*>(userData); // if the context was removed somehow, bail early - if (context == nullptr || context->bridge == nullptr) { + if (context == nullptr || context->window == nullptr) { return eventNotHandledErr; } @@ -43,7 +44,6 @@ namespace SSC { &eventHotKeyID ); - context->onHotKeyBindingCallback(eventHotKeyID.id); if (context->bindings.contains(eventHotKeyID.id)) { @@ -57,7 +57,7 @@ namespace SSC { return eventNotHandledErr; } -#elif defined(__linux__) && !defined(__ANDROID__) +#elif SOCKET_RUNTIME_PLATFORM_LINUX static bool gtkKeyPressEventHandlerCallback ( GtkWidget* widget, GdkEventKey* event, @@ -69,13 +69,13 @@ namespace SSC { return false; } - auto context = reinterpret_cast<HotKeyBinding::GTKKeyPressEventContext*>(userData); + const auto context = reinterpret_cast<HotKeyBinding::GTKKeyPressEventContext*>(userData); // if the context was removed somehow, bail early if ( context == nullptr || context->context == nullptr || - context->context->bridge == nullptr + context->context->window == nullptr ) { return false; } @@ -111,7 +111,7 @@ namespace SSC { #endif HotKeyCodeMap::HotKeyCodeMap () { - #if defined(__APPLE__) && (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) + #if SOCKET_RUNTIME_PLATFORM_MACOS keys.insert_or_assign("a", kVK_ANSI_A); keys.insert_or_assign("b", kVK_ANSI_B); keys.insert_or_assign("c", kVK_ANSI_C); @@ -226,7 +226,7 @@ namespace SSC { modifiers.insert_or_assign("ctrl", controlKey); modifiers.insert_or_assign("shift", shiftKey); - #elif defined(__linux__) && !defined(__ANDROID__) + #elif SOCKET_RUNTIME_PLATFORM_LINUX keys.insert_or_assign("a", GDK_KEY_a); keys.insert_or_assign("b", GDK_KEY_b); keys.insert_or_assign("c", GDK_KEY_c); @@ -375,7 +375,7 @@ namespace SSC { modifiers.insert_or_assign("right shift", GDK_SHIFT_MASK); modifiers.insert_or_assign("left shift", GDK_SHIFT_MASK); modifiers.insert_or_assign("shift", GDK_SHIFT_MASK); - #elif defined(_WIN32) + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS keys.insert_or_assign("a", 0x41); keys.insert_or_assign("b", 0x42); keys.insert_or_assign("c", 0x43); @@ -503,13 +503,7 @@ namespace SSC { } const HotKeyCodeMap::Code HotKeyCodeMap::get (Key key) const { - // normalize key to lower case - std::transform( - key.begin(), - key.end(), - key.begin(), - [](auto ch) { return std::tolower(ch); } - ); + key = toLowerCase(key); if (keys.contains(key)) { return keys.at(key); @@ -523,44 +517,20 @@ namespace SSC { } const bool HotKeyCodeMap::isModifier (Key key) const { - // normalize key to lower case - std::transform( - key.begin(), - key.end(), - key.begin(), - [](auto ch) { return std::tolower(ch); } - ); - - return modifiers.contains(key); + return modifiers.contains(toLowerCase(key)); } const bool HotKeyCodeMap::isKey (Key key) const { - // normalize key to lower case - std::transform( - key.begin(), - key.end(), - key.begin(), - [](auto ch) { return std::tolower(ch); } - ); - - return keys.contains(key); + return keys.contains(toLowerCase(key)); } - HotKeyBinding::Sequence HotKeyBinding::parseExpression (Expression input) { static const auto delimiter = "+"; const auto tokens = split(trim(input), delimiter); Sequence sequence; for (auto token : tokens) { - std::transform( - token.begin(), - token.end(), - token.begin(), - [](auto ch) { return std::tolower(ch); } - ); - - token = replace(token, "ctrl", "control"); + token = replace(toLowerCase(token), "ctrl", "control"); if (token == "cmd") { token = "command"; @@ -616,11 +586,10 @@ namespace SSC { this->reset(); } - void HotKeyContext::init (IPC::Bridge* bridge) { - static auto userConfig = SSC::getUserConfig(); - this->bridge = bridge; + void HotKeyContext::init () { + auto userConfig = this->window->bridge.userConfig; - #if defined(__APPLE__) && (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) + #if SOCKET_RUNTIME_PLATFORM_MACOS // Carbon API event type spec static const EventTypeSpec eventTypeSpec = { .eventClass = kEventClassKeyboard, @@ -638,8 +607,7 @@ namespace SSC { ); #endif - this->bridge->router.map("window.hotkey.bind", [this](auto message, auto router, auto reply) mutable { - static auto userConfig = SSC::getUserConfig(); + this->window->bridge.router.map("window.hotkey.bind", [=, this](auto message, auto router, auto reply) mutable { HotKeyBinding::Options options; options.passive = true; // default @@ -653,7 +621,7 @@ namespace SSC { try { id = std::stoul(message.get("id")); } catch (...) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Invalid 'id' given in parameters"} }; @@ -661,7 +629,7 @@ namespace SSC { } if (!this->bindings.contains(id)) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"type", "NotFoundError"}, {"message", "Invalid 'id' given in parameters"} }; @@ -669,10 +637,10 @@ namespace SSC { return reply(IPC::Result::Err { message, err }); } - auto& binding = this->bindings.at(id); + const auto& binding = this->bindings.at(id); if (!binding.isValid()) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Invalid 'expression' in parameters"} }; @@ -680,7 +648,7 @@ namespace SSC { } if (!this->bind(binding.expression, options).isValid()) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Failed to bind existing binding expression to context"} }; return reply(IPC::Result::Err { message, err }); @@ -692,7 +660,7 @@ namespace SSC { sequence.push_back(token); } - auto data = JSON::Object::Entries { + const auto data = JSON::Object::Entries { {"expression", binding.expression}, {"sequence", sequence}, {"hash", binding.hash}, @@ -702,10 +670,14 @@ namespace SSC { return reply(IPC::Result::Data { message, data }); } - auto expression = message.get("expression"); + #if SOCKET_RUNTIME_PLATFORM_LINUX + const auto expression = decodeURIComponent(message.get("expression")); + #else + const auto expression = message.get("expression"); + #endif if (expression.size() == 0) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Expecting 'expression' in parameters"} }; return reply(IPC::Result::Err { message, err }); @@ -719,10 +691,10 @@ namespace SSC { return reply(IPC::Result::Err { message, err }); } - auto binding = this->bind(expression, options); + const auto binding = this->bind(expression, options); if (!binding.isValid()) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Invalid 'expression' in parameters"} }; return reply(IPC::Result::Err { message, err }); @@ -734,7 +706,7 @@ namespace SSC { sequence.push_back(token); } - auto data = JSON::Object::Entries { + const auto data = JSON::Object::Entries { {"expression", binding.expression}, {"sequence", sequence}, {"hash", binding.hash}, @@ -744,13 +716,17 @@ namespace SSC { reply(IPC::Result::Data { message, data }); }); - this->bridge->router.map("window.hotkey.unbind", [this](auto message, auto router, auto reply) mutable { + this->window->bridge.router.map("window.hotkey.unbind", [this](auto message, auto router, auto reply) mutable { static auto userConfig = SSC::getUserConfig(); HotKeyBinding::ID id; + #if SOCKET_RUNTIME_PLATFORM_LINUX + const auto expression = decodeURIComponent(message.get("expression")); + #else const auto expression = message.get("expression"); + #endif if (userConfig["permissions_allow_hotkeys"] == "false") { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"type", "SecurityError"}, {"message", "The HotKey API is not allowed."} }; @@ -761,7 +737,7 @@ namespace SSC { if (this->hasBindingForExpression(expression)) { const auto binding = this->getBindingForExpression(expression); if (!binding.isValid()) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"type", "NotFoundError"}, {"message", "Binding for expression '" + expression + "' is not valid"} }; @@ -769,19 +745,19 @@ namespace SSC { } if (!this->unbind(binding.id)) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Failed to unbind hotkey expression"} }; return reply(IPC::Result::Err { message, err }); } - auto data = JSON::Object::Entries { + const auto data = JSON::Object::Entries { {"id", binding.id} }; return reply(IPC::Result::Data { message, data }); } else { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"type", "NotFoundError"}, {"message", "Binding for expression '" + expression + "' in does not exist"} }; @@ -790,7 +766,7 @@ namespace SSC { } if (!message.has("id") || message.get("id").size() == 0) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Expression 'id' in parameters"} }; return reply(IPC::Result::Err { message, err }); @@ -799,7 +775,7 @@ namespace SSC { try { id = std::stoul(message.get("id")); } catch (...) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Invalid 'id' given in parameters"} }; @@ -807,7 +783,7 @@ namespace SSC { } if (!this->bindings.contains(id)) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"type", "NotFoundError"}, {"message", "Invalid 'id' given in parameters"} }; @@ -816,25 +792,23 @@ namespace SSC { } if (!this->unbind(id)) { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"message", "Failed to unbind hotkey expression"} }; return reply(IPC::Result::Err { message, err }); } - auto data = JSON::Object::Entries { + const auto data = JSON::Object::Entries { {"id", id} }; return reply(IPC::Result::Data { message, data }); }); - this->bridge->router.map("window.hotkey.reset", [this](auto message, auto router, auto reply) mutable { - static auto userConfig = SSC::getUserConfig(); - + this->window->bridge.router.map("window.hotkey.reset", [=, this](auto message, auto router, auto reply) mutable { if (userConfig["permissions_allow_hotkeys"] == "false") { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"type", "SecurityError"}, {"message", "The HotKey API is not allowed."} }; @@ -844,12 +818,11 @@ namespace SSC { return reply(IPC::Result { message.seq, message }); }); - this->bridge->router.map("window.hotkey.bindings", [this](auto message, auto router, auto reply) mutable { - static auto userConfig = SSC::getUserConfig(); + this->window->bridge.router.map("window.hotkey.bindings", [=, this](auto message, auto router, auto reply) mutable { auto data = JSON::Array::Entries {}; if (userConfig["permissions_allow_hotkeys"] == "false") { - auto err = JSON::Object::Entries { + const auto err = JSON::Object::Entries { {"type", "SecurityError"}, {"message", "The HotKey API is not allowed."} }; @@ -864,7 +837,7 @@ namespace SSC { sequence.push_back(token); } - auto json = JSON::Object::Entries { + const auto json = JSON::Object::Entries { {"expression", binding.expression}, {"sequence", sequence}, {"hash", binding.hash}, @@ -877,8 +850,7 @@ namespace SSC { return reply(IPC::Result::Data { message, data }); }); - this->bridge->router.map("window.hotkey.mappings", [this](auto message, auto router, auto reply) mutable { - static auto userConfig = SSC::getUserConfig(); + this->window->bridge.router.map("window.hotkey.mappings", [=, this](auto message, auto router, auto reply) mutable { static const HotKeyCodeMap map; auto modifiers = JSON::Object::Entries {}; @@ -900,7 +872,7 @@ namespace SSC { keys.insert(entry); } - auto data = JSON::Object::Entries { + const auto data = JSON::Object::Entries { {"modifiers", modifiers}, {"keys", keys} }; @@ -930,17 +902,16 @@ namespace SSC { return HotKeyBinding(0, ""); } - if (this->bridge == nullptr) { + if (this->window == nullptr) { return HotKeyBinding(0, ""); } const auto exists = this->hasBindingForExpression(expression); - auto binding = exists ? getBindingForExpression(expression) : HotKeyBinding(nextGlobalBindingID, expression); - #if defined(__APPLE__) && (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) + #if SOCKET_RUNTIME_PLATFORM_MACOS if (!exists) { EventHotKeyID eventHotKeyID; eventHotKeyID.id = binding.id; @@ -959,7 +930,7 @@ namespace SSC { return HotKeyBinding(0, ""); } } - #elif defined(__linux__) && !defined(__ANDROID__) + #elif SOCKET_RUNTIME_PLATFORM_LINUX static const HotKeyCodeMap hotKeyCodeMap; if (!this->gtkKeyPressEventContexts.contains(binding.id)) { @@ -976,7 +947,7 @@ namespace SSC { &context ); } - #elif defined(_WIN32) + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS if (!exists) { const auto status = RegisterHotKey( this->window->window, @@ -1003,7 +974,7 @@ namespace SSC { bool HotKeyContext::unbind (HotKeyBinding::ID id) { Lock lock(this->mutex); - if (this->bridge == nullptr || this->window == nullptr) { + if (this->window == nullptr) { return false; } @@ -1012,12 +983,12 @@ namespace SSC { } const auto& binding = this->bindings.at(id); - #if defined(__APPLE__) && (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) + #if SOCKET_RUNTIME_PLATFORM_MACOS if (UnregisterEventHotKey(binding.eventHotKeyRef) == 0) { this->bindings.erase(id); return true; } - #elif defined(__linux__) && !defined(__ANDROID__) + #elif SOCKET_RUNTIME_PLATFORM_LINUX if (this->window->window == nullptr) { return false; } @@ -1031,7 +1002,7 @@ namespace SSC { } return true; - #elif defined(_WIN32) + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS if (UnregisterHotKey(this->window->window, id)) { this->bindings.erase(id); return true; @@ -1070,7 +1041,7 @@ namespace SSC { } bool HotKeyContext::onHotKeyBindingCallback (HotKeyBinding::ID id) { - auto app = App::instance(); + auto app = App::sharedApplication(); if (app != nullptr && this->bindings.contains(id)) { const auto& binding = this->bindings.at(id); @@ -1084,15 +1055,15 @@ namespace SSC { sequence.push_back(token); } - auto data = JSON::Object::Entries { + const auto data = JSON::Object::Entries { {"expression", binding.expression}, {"sequence", sequence}, {"hash", binding.hash}, {"id", binding.id} }; - auto json = JSON::Object{ data }; - return this->window->bridge->router.emit("hotkey", json.str()); + const auto json = JSON::Object{ data }; + return this->window->bridge.emit("hotkey", json.str()); } return false; diff --git a/src/window/hotkey.hh b/src/window/hotkey.hh index f7955188b0..e6e1774928 100644 --- a/src/window/hotkey.hh +++ b/src/window/hotkey.hh @@ -1,8 +1,7 @@ -#ifndef SSC_WINDOW_HOTKEY_H -#define SSC_WINDOW_HOTKEY_H +#ifndef SOCKET_RUNTIME_WINDOW_HOTKEY_H +#define SOCKET_RUNTIME_WINDOW_HOTKEY_H -#include "../core/platform.hh" -#include "../core/types.hh" +#include "../platform/platform.hh" #include "../ipc/ipc.hh" namespace SSC { @@ -37,7 +36,7 @@ namespace SSC { bool passive = true; }; - #if defined(__linux__) && !defined(__ANDROID__) + #if SOCKET_RUNTIME_PLATFORM_LINUX struct GTKKeyPressEventContext { HotKeyContext* context = nullptr; ID id = 0; @@ -58,7 +57,7 @@ namespace SSC { Options options; - #if defined(__APPLE__) && (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) + #if SOCKET_RUNTIME_PLATFORM_MACOS // Apple Carbon API EventHotKeyRef eventHotKeyRef; #endif @@ -71,10 +70,10 @@ namespace SSC { public: using Bindings = std::map<HotKeyBinding::ID, HotKeyBinding>; - #if defined(__APPLE__) && (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) + #if SOCKET_RUNTIME_PLATFORM_MACOS // Apple Carbon API EventTargetRef eventTarget; - #elif defined(__linux__) && !defined(__ANDROID__) + #elif SOCKET_RUNTIME_PLATFORM_LINUX std::map< HotKeyBinding::ID, HotKeyBinding::GTKKeyPressEventContext @@ -82,7 +81,6 @@ namespace SSC { #endif Window* window = nullptr; - IPC::Bridge* bridge = nullptr; // state Mutex mutex; @@ -90,10 +88,11 @@ namespace SSC { Vector<HotKeyBinding::ID> bindingIds; Bindings& bindings; + HotKeyContext (const HotKeyContext&) = delete; HotKeyContext (Window* window); ~HotKeyContext (); - void init (IPC::Bridge* bridge); + void init (); void reset (); const HotKeyBinding bind ( HotKeyBinding::Expression expression, diff --git a/src/window/linux.cc b/src/window/linux.cc index 6b4435426e..47d3199cca 100644 --- a/src/window/linux.cc +++ b/src/window/linux.cc @@ -1,3 +1,7 @@ +#include <fstream> +#include <sys/utsname.h> + +#include "../app/app.hh" #include "window.hh" static GtkTargetEntry droppableTypes[] = { @@ -8,324 +12,465 @@ static GtkTargetEntry droppableTypes[] = { #define DEFAULT_MONITOR_HEIGHT 364 namespace SSC { - struct WebViewJavaScriptAsyncContext { - IPC::Router::ReplyCallback reply; - IPC::Message message; - Window *window; - }; - - Window::Window (App& app, WindowOptions opts) - : app(app), - opts(opts), - hotkey(this) - { - setenv("GTK_OVERLAY_SCROLLING", "1", 1); - this->accelGroup = gtk_accel_group_new(); - this->popupId = 0; - this->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - this->popup = nullptr; + static void initializeWebContextFromWindow (Window* window) { + static Atomic<bool> isInitialized = false; - gtk_widget_set_can_focus(GTK_WIDGET(this->window), true); + if (isInitialized) { + return; + } - this->index = this->opts.index; - this->bridge = new IPC::Bridge(app.core); + auto webContext = webkit_web_context_get_default(); - this->hotkey.init(this->bridge); + // mounts are configured for all contexts just once + window->bridge.configureNavigatorMounts(); - this->bridge->router.dispatchFunction = [&app] (auto callback) { - app.dispatch([callback] { callback(); }); - }; + g_signal_connect( + G_OBJECT(webContext), + "initialize-notification-permissions", + G_CALLBACK(+[]( + WebKitWebContext* webContext, + gpointer userData + ) { + static const auto app = App::sharedApplication(); + static const auto bundleIdentifier = app->userConfig["meta_bundle_identifier"]; + static const auto areNotificationsAllowed = ( + !app->userConfig.contains("permissions_allow_notifications") || + app->userConfig.at("permissions_allow_notifications") != "false" + ); - this->bridge->router.evaluateJavaScriptFunction = [this] (auto js) { - this->eval(js); - }; + const auto uri = "socket://" + bundleIdentifier; + const auto origin = webkit_security_origin_new_for_uri(uri.c_str()); - this->bridge->router.map("window.eval", [=, &app](auto message, auto router, auto reply) { - WindowManager* windowManager = app.getWindowManager(); - if (windowManager == nullptr) { - // @TODO(jwerle): print warning - return; - } + GList* allowed = nullptr; + GList* disallowed = nullptr; - auto window = windowManager->getWindow(message.index); - static auto userConfig = SSC::getUserConfig(); + webkit_security_origin_ref(origin); - if (userConfig["application_agent"] == "true") { - gtk_window_set_skip_taskbar_hint(GTK_WINDOW(window), TRUE); - } + if (origin) { + if (areNotificationsAllowed) { + disallowed = g_list_append(disallowed, (gpointer) origin); + } else { + allowed = g_list_append(allowed, (gpointer) origin); + } - if (window == nullptr) { - return reply(IPC::Result::Err { message, JSON::Object::Entries { - {"message", "Invalid window index given"} - }}); - } + if (allowed || disallowed) { + webkit_web_context_initialize_notification_permissions( + webContext, + allowed, + disallowed + ); + } + } - auto value = message.get("value"); - auto ctx = new WebViewJavaScriptAsyncContext { reply, message, window }; + if (allowed) { + g_list_free(allowed); + } - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(window->webview), - value.c_str(), - -1, - nullptr, - nullptr, - nullptr, - [](GObject *object, GAsyncResult *res, gpointer data) { - GError *error = nullptr; - auto ctx = reinterpret_cast<WebViewJavaScriptAsyncContext*>(data); - auto value = webkit_web_view_evaluate_javascript_finish( - WEBKIT_WEB_VIEW(ctx->window->webview), - res, - &error - ); + if (disallowed) { + g_list_free(disallowed); + } - if (!value) { - ctx->reply(IPC::Result::Err { ctx->message, JSON::Object::Entries { - {"code", error->code}, - {"message", String(error->message)} - }}); + if (origin) { + webkit_security_origin_unref(origin); + } + }), + nullptr + ); - g_error_free(error); - return; - } else { - if ( - jsc_value_is_null(value) || - jsc_value_is_array(value) || - jsc_value_is_object(value) || - jsc_value_is_number(value) || - jsc_value_is_string(value) || - jsc_value_is_function(value) || - jsc_value_is_undefined(value) || - jsc_value_is_constructor(value) - ) { - auto context = jsc_value_get_context(value); - auto string = jsc_value_to_string(value); - auto exception = jsc_context_get_exception(context); - auto json = JSON::Any {}; + webkit_web_context_set_sandbox_enabled(webContext, true); - if (exception) { - auto message = jsc_exception_get_message(exception); + auto extensionsPath = FileResource::getResourcePath(Path("lib/extensions")); + webkit_web_context_set_web_extensions_directory( + webContext, + extensionsPath.c_str() + ); - if (message == nullptr) { - message = "An unknown error occured"; - } + auto cwd = getcwd(); + auto bytes = socket_runtime_init_get_user_config_bytes(); + auto size = socket_runtime_init_get_user_config_bytes_size(); + static auto data = String(reinterpret_cast<const char*>(bytes), size); + data += "\n[web-process-extension]\n"; + data += "cwd = " + cwd + "\n"; + data += "host = " + getDevHost() + "\n"; + data += "port = " + std::to_string(getDevPort()) + "\n"; + + webkit_web_context_set_web_extensions_initialization_user_data( + webContext, + g_variant_new_from_data( + G_VARIANT_TYPE("ay"), // an array of "bytes" + data.c_str(), + data.size(), + true, + nullptr, + nullptr + ) + ); - ctx->reply(IPC::Result::Err { ctx->message, JSON::Object::Entries { - {"message", String(message)} - }}); - } else if (string) { - ctx->reply(IPC::Result { ctx->message.seq, ctx->message, String(string) }); - } + isInitialized = true; + } - if (string) { - g_free(string); - } - } else { - ctx->reply(IPC::Result::Err { ctx->message, JSON::Object::Entries { - {"message", "Error: An unknown JavaScript evaluation error has occurred"} - }}); - } - } - }, - ctx - ); - }); + Window::Window (SharedPointer<Core> core, const Window::Options& options) + : core(core), + options(options), + bridge(core, IPC::Bridge::Options { + options.userConfig, + options.as<IPC::Preload::Options>() + }), + hotkey(this), + dialog(this) + { + Env::set("GTK_OVERLAY_SCROLLING", "1"); - if (opts.resizable) { - gtk_window_set_default_size(GTK_WINDOW(window), opts.width, opts.height); - gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); - } else { - gtk_widget_set_size_request(window, opts.width, opts.height); - } + auto userConfig = options.userConfig; + auto webContext = webkit_web_context_get_default(); - gtk_window_set_resizable(GTK_WINDOW(window), opts.resizable); - gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); + if (options.index == 0) { + initializeWebContextFromWindow(this); + } - WebKitUserContentManager* cm = webkit_user_content_manager_new(); - webkit_user_content_manager_register_script_message_handler(cm, "external"); + this->settings = webkit_settings_new(); + // TODO(@jwerle); make configurable with '[permissions] allow_media' + webkit_settings_set_zoom_text_only(this->settings, false); + webkit_settings_set_media_playback_allows_inline(this->settings, true); + // TODO(@jwerle); make configurable with '[permissions] allow_dialogs' + webkit_settings_set_allow_modal_dialogs(this->settings, true); + webkit_settings_set_hardware_acceleration_policy( + this->settings, + userConfig["permissions_hardware_acceleration_disabled"] == "true" + ? WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER + : WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS + ); - g_signal_connect( - cm, - "script-message-received::external", - G_CALLBACK(+[]( - WebKitUserContentManager* cm, - WebKitJavascriptResult* result, - gpointer ptr - ) { - auto window = static_cast<Window*>(ptr); - auto value = webkit_javascript_result_get_js_value(result); - auto valueString = jsc_value_to_string(value); - auto str = String(valueString); + webkit_settings_set_enable_webgl(this->settings, true); + webkit_settings_set_enable_media(this->settings, true); + webkit_settings_set_enable_webaudio(this->settings, true); + webkit_settings_set_enable_mediasource(this->settings, true); + webkit_settings_set_enable_encrypted_media(this->settings, true); + webkit_settings_set_enable_dns_prefetching(this->settings, true); + webkit_settings_set_enable_smooth_scrolling(this->settings, true); + webkit_settings_set_enable_developer_extras(this->settings, options.debug); + + webkit_settings_set_enable_back_forward_navigation_gestures( + this->settings, + userConfig["webview_navigator_enable_navigation_gestures"] == "true" + ); - char *buf = nullptr; - size_t bufsize = 0; + webkit_settings_set_media_content_types_requiring_hardware_support( + this->settings, + nullptr + ); - // 'b5' for 'buffer' - if (str.size() >= 2 && str.at(0) == 'b' && str.at(1) == '5') { - size_t size = 0; - auto bytes = jsc_value_to_string_as_bytes(value); - auto data = (char *) g_bytes_get_data(bytes, &size); + auto userAgent = String(webkit_settings_get_user_agent(settings)); - if (size > 6) { - size_t offset = 2 + 4 + 20; // buf offset - auto index = new char[4]{0}; - auto seq = new char[20]{0}; + webkit_settings_set_user_agent( + settings, + (userAgent + " " + "SocketRuntime/" + SSC::VERSION_STRING).c_str() + ); - decodeUTF8(index, data + 2, 4); - decodeUTF8(seq, data + 2 + 4, 20); + webkit_settings_set_enable_media_stream( + this->settings, + userConfig["permissions_allow_user_media"] != "false" + ); - buf = new char[size - offset]{0}; - bufsize = decodeUTF8(buf, data + offset, size - offset); + webkit_settings_set_enable_media_capabilities( + this->settings, + userConfig["permissions_allow_user_media"] != "false" + ); - str = String("ipc://buffer.map?index=") + index + "&seq=" + seq; + webkit_settings_set_enable_webrtc( + this->settings, + userConfig["permissions_allow_user_media"] != "false" + ); - delete [] index; - delete [] seq; - } + webkit_settings_set_javascript_can_access_clipboard( + this->settings, + userConfig["permissions_allow_clipboard"] != "false" + ); - g_bytes_unref(bytes); - } + webkit_settings_set_enable_fullscreen( + this->settings, + userConfig["permissions_allow_fullscreen"] != "false" + ); - if (!window->bridge->route(str, buf, bufsize)) { - if (window->onMessage != nullptr) { - window->onMessage(str); - } - } + webkit_settings_set_enable_html5_local_storage( + this->settings, + userConfig["permissions_allow_data_access"] != "false" + ); - g_free(valueString); - delete [] buf; - }), - this + webkit_settings_set_enable_html5_database( + this->settings, + userConfig["permissions_allow_data_access"] != "false" ); - static auto userConfig = SSC::getUserConfig(); - auto webContext = this->bridge->router.webkitWebContext; + this->accelGroup = gtk_accel_group_new(); + this->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + auto cookieManager = webkit_web_context_get_cookie_manager(webContext); - auto settings = webkit_settings_new(); - auto policies = webkit_website_policies_new_with_policies( - "autoplay", userConfig["permission_allow_autoplay"] != "false" ? WEBKIT_AUTOPLAY_ALLOW : WEBKIT_AUTOPLAY_DENY, - NULL + webkit_cookie_manager_set_accept_policy(cookieManager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); + + this->userContentManager = webkit_user_content_manager_new(); + webkit_user_content_manager_register_script_message_handler(this->userContentManager, "external"); + + auto preloadUserScriptSource = IPC::Preload::compile({ + .features = IPC::Preload::Options::Features { + .useGlobalCommonJS = false, + .useGlobalNodeJS = false, + .useTestScript = false, + .useHTMLMarkup = false, + .useESM = false, + .useGlobalArgs = true + }, + .client = UniqueClient { + .id = this->bridge.client.id, + .index = this->bridge.client.index + }, + .index = options.index, + .conduit = this->core->conduit.port, + .userScript = options.userScript + }); + + auto preloadUserScript = webkit_user_script_new( + preloadUserScriptSource.str().c_str(), + WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, + WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, + nullptr, + nullptr ); - webview = GTK_WIDGET(WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, + webkit_user_content_manager_add_script( + this->userContentManager, + preloadUserScript + ); + + this->policies = webkit_website_policies_new_with_policies( + "autoplay", userConfig["permission_allow_autoplay"] != "false" + ? WEBKIT_AUTOPLAY_ALLOW + : WEBKIT_AUTOPLAY_DENY, + nullptr + ); + + this->webview = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, + "user-content-manager", this->userContentManager, + "website-policies", this->policies, "web-context", webContext, - "settings", settings, - "user-content-manager", cm, - "website-policies", policies, - NULL - ))); + "settings", this->settings, + nullptr + )); - gtk_widget_set_can_focus(GTK_WIDGET(webview), true); + gtk_widget_set_can_focus(GTK_WIDGET(this->webview), true); - webkit_cookie_manager_set_accept_policy(cookieManager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); + this->index = this->options.index; + this->dragStart = {0,0}; + this->shouldDrag = false; + this->contextMenu = nullptr; + this->contextMenuID = 0; - g_signal_connect( - G_OBJECT(webContext), - "initialize-notification-permissions", - G_CALLBACK(+[]( - WebKitWebContext* webContext, - gpointer userData - ) { - static auto userConfig = SSC::getUserConfig(); - static const auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + this->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - auto uri = "socket://" + bundleIdentifier; - auto origin = webkit_security_origin_new_for_uri(uri.c_str()); - GList* allowed = nullptr; - GList* disallowed = nullptr; + this->bridge.navigateFunction = [this] (const auto url) { + this->navigate(url); + }; - webkit_security_origin_ref(origin); + this->bridge.evaluateJavaScriptFunction = [this] (const auto source) { + this->eval(source); + }; - if (origin && allowed && disallowed) { - if (userConfig["permissions_allow_notifications"] == "false") { - disallowed = g_list_append(disallowed, (gpointer) origin); - } else { - allowed = g_list_append(allowed, (gpointer) origin); - } + this->bridge.client.preload = IPC::Preload::compile({ + .client = UniqueClient { + .id = this->bridge.client.id, + .index = this->bridge.client.index + }, + .index = options.index, + .conduit = this->core->conduit.port, + .userScript = options.userScript + }); - if (allowed && disallowed) { - webkit_web_context_initialize_notification_permissions( - webContext, - allowed, - disallowed - ); - } - } + gtk_box_pack_end(GTK_BOX(this->vbox), GTK_WIDGET(this->webview), true, true, 0); - if (allowed) { - g_list_free(allowed); - } + gtk_container_add(GTK_CONTAINER(this->window), this->vbox); - if (disallowed) { - g_list_free(disallowed); + gtk_widget_add_events(this->window, GDK_ALL_EVENTS_MASK); + gtk_widget_grab_focus(GTK_WIDGET(this->webview)); + gtk_widget_realize(GTK_WIDGET(this->window)); + + if (options.resizable) { + gtk_window_set_default_size(GTK_WINDOW(this->window), options.width, options.height); + } else { + gtk_widget_set_size_request(this->window, options.width, options.height); + } + + gtk_window_set_decorated(GTK_WINDOW(this->window), options.frameless == false); + gtk_window_set_resizable(GTK_WINDOW(this->window), options.resizable); + gtk_window_set_position(GTK_WINDOW(this->window), GTK_WIN_POS_CENTER); + gtk_widget_set_can_focus(GTK_WIDGET(this->window), true); + + GdkRGBA webviewBackground = {0.0, 0.0, 0.0, 0.0}; + bool hasDarkValue = this->options.backgroundColorDark.size(); + bool hasLightValue = this->options.backgroundColorLight.size(); + + auto isKDEDarkMode = []() -> bool { + static const auto paths = FileResource::getWellKnownPaths(); + static const auto kdeglobals = paths.home / ".config" / "kdeglobals"; + + if (!FileResource::isFile(kdeglobals)) { + return false; + } + + auto file = FileResource(kdeglobals); + + if (!file.exists() || !file.hasAccess()) { + return false; + } + + const auto bytes = file.read(); + const auto lines = split(bytes, '\n'); + + for (const auto& line : lines) { + if (toLowerCase(line).find("dark") != String::npos) { + return true; } + } - if (origin) { - webkit_security_origin_unref(origin); + return false; + }; + + auto isGnomeDarkMode = [this]() -> bool { + GtkStyleContext* context = gtk_widget_get_style_context(this->window); + GdkRGBA background_color; + // FIXME(@jwerle): this is deprecated + gtk_style_context_get_background_color(context, GTK_STATE_FLAG_NORMAL, &background_color); + + bool is_dark_theme = (0.299* background_color.red + + 0.587* background_color.green + + 0.114* background_color.blue) < 0.5; + + return FALSE; + }; + + if (hasDarkValue || hasLightValue) { + GdkRGBA color = {0}; + + const gchar* desktop_env = getenv("XDG_CURRENT_DESKTOP"); + + if (desktop_env != NULL && g_str_has_prefix(desktop_env, "GNOME")) { + this->isDarkMode = isGnomeDarkMode(); + } else { + this->isDarkMode = isKDEDarkMode(); + } + + if (this->isDarkMode && hasDarkValue) { + gdk_rgba_parse(&color, this->options.backgroundColorDark.c_str()); + } else if (hasLightValue) { + gdk_rgba_parse(&color, this->options.backgroundColorLight.c_str()); + } + + // FIXME(@jwerle): this is deprecated + gtk_widget_override_background_color(this->window, GTK_STATE_FLAG_NORMAL, &color); + } + + webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(webview), &webviewBackground); + + this->hotkey.init(); + this->bridge.init(); + this->bridge.configureSchemeHandlers({ + .webview = settings + }); + + this->bridge.configureWebView(this->webview); + + g_signal_connect( + this->userContentManager, + "script-message-received::external", + G_CALLBACK(+[]( + WebKitUserContentManager* userContentManager, + WebKitJavascriptResult* result, + gpointer ptr + ) { + auto window = static_cast<Window*>(ptr); + auto value = webkit_javascript_result_get_js_value(result); + auto valueString = jsc_value_to_string(value); + auto str = String(valueString); + + if (!window->bridge.route(str, nullptr, 0)) { + if (window->onMessage != nullptr) { + window->onMessage(str); + } } + + g_free(valueString); }), this ); g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "show-notification", G_CALLBACK(+[]( WebKitWebView* webview, WebKitNotification* notification, gpointer userData ) -> bool { - static auto userConfig = SSC::getUserConfig(); - return userConfig["permissions_allow_notifications"] == "false"; + const auto app = App::sharedApplication(); + const auto window = app->windowManager.getWindowForWebView(webview); + + if (window == nullptr) { + return true; + } + + auto userConfig = window->bridge.userConfig; + if (userConfig["permissions_allow_notifications"] == "false") { + return true; + } + + return false; }), this ); // handle `navigator.permissions.query()` g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "query-permission-state", G_CALLBACK((+[]( WebKitWebView* webview, WebKitPermissionStateQuery* query, gpointer user_data ) -> bool { - static auto userConfig = SSC::getUserConfig(); - auto name = String(webkit_permission_state_query_get_name(query)); + const auto app = App::sharedApplication(); + const auto window = app->windowManager.getWindowForWebView(webview); + const auto name = String(webkit_permission_state_query_get_name(query)); if (name == "geolocation") { webkit_permission_state_query_finish( query, - userConfig["permissions_allow_geolocation"] == "false" + window->bridge.userConfig["permissions_allow_geolocation"] == "false" ? WEBKIT_PERMISSION_STATE_DENIED - : WEBKIT_PERMISSION_STATE_PROMPT + : WEBKIT_PERMISSION_STATE_GRANTED ); - } - - if (name == "notifications") { + } else if (name == "notifications") { webkit_permission_state_query_finish( query, - userConfig["permissions_allow_notifications"] == "false" + window->bridge.userConfig["permissions_allow_notifications"] == "false" ? WEBKIT_PERMISSION_STATE_DENIED - : WEBKIT_PERMISSION_STATE_PROMPT + : WEBKIT_PERMISSION_STATE_GRANTED + ); + } else { + webkit_permission_state_query_finish( + query, + WEBKIT_PERMISSION_STATE_PROMPT ); } - - webkit_permission_state_query_finish( - query, - WEBKIT_PERMISSION_STATE_PROMPT - ); return true; })), this ); g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "permission-request", G_CALLBACK((+[]( WebKitWebView* webview, - WebKitPermissionRequest *request, + WebKitPermissionRequest* request, gpointer userData ) -> bool { Window* window = reinterpret_cast<Window*>(userData); @@ -343,19 +488,19 @@ namespace SSC { result = userConfig["permissions_allow_notifications"] != "false"; description = "{{meta_title}} would like display notifications."; } else if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(request)) { - if (webkit_user_media_permission_is_for_audio_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { - name = "microphone"; - result = userConfig["permissions_allow_microphone"] == "false"; - description = "{{meta_title}} would like access to your microphone."; - } + if (userConfig["permissions_allow_user_media"] != "false") { + if (webkit_user_media_permission_is_for_audio_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { + name = "microphone"; + result = userConfig["permissions_allow_microphone"] != "false"; + description = "{{meta_title}} would like access to your microphone."; + } - if (webkit_user_media_permission_is_for_video_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { - name = "camera"; - result = userConfig["permissions_allow_camera"] == "false"; - description = "{{meta_title}} would like access to your camera."; + if (webkit_user_media_permission_is_for_video_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { + name = "camera"; + result = userConfig["permissions_allow_camera"] != "false"; + description = "{{meta_title}} would like access to your camera."; + } } - - result = userConfig["permissions_allow_user_media"] == "false"; } else if (WEBKIT_IS_WEBSITE_DATA_ACCESS_PERMISSION_REQUEST(request)) { name = "storage-access"; result = userConfig["permissions_allow_data_access"] != "false"; @@ -363,6 +508,10 @@ namespace SSC { } else if (WEBKIT_IS_DEVICE_INFO_PERMISSION_REQUEST(request)) { result = userConfig["permissions_allow_device_info"] != "false"; description = "{{meta_title}} would like access to your device information."; + if (result) { + webkit_permission_request_allow(request); + return result; + } } else if (WEBKIT_IS_MEDIA_KEY_SYSTEM_PERMISSION_REQUEST(request)) { result = userConfig["permissions_allow_media_key_system"] != "false"; description = "{{meta_title}} would like access to your media key system."; @@ -370,7 +519,7 @@ namespace SSC { if (result) { auto title = userConfig["meta_title"]; - GtkWidget *dialog = gtk_message_dialog_new( + GtkWidget* dialog = gtk_message_dialog_new( GTK_WINDOW(window->window), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, @@ -392,10 +541,12 @@ namespace SSC { } if (name.size() > 0) { - JSON::Object::Entries json = JSON::Object::Entries { + JSON::Object json = JSON::Object::Entries { {"name", name}, {"state", result ? "granted" : "denied"} }; + // TODO(@heapwolf): properly return this data + // TODO(@jwerle): maybe this could be dispatched to webview } return result; @@ -403,68 +554,6 @@ namespace SSC { this ); - g_signal_connect( - G_OBJECT(webview), - "decide-policy", - G_CALLBACK((+[]( - WebKitWebView* webview, - WebKitPolicyDecision* decision, - WebKitPolicyDecisionType decisionType, - gpointer userData - ) { - static const auto devHost = SSC::getDevHost(); - auto window = static_cast<Window*>(userData); - - if (decisionType != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { - webkit_policy_decision_use(decision); - return true; - } - - const auto nav = WEBKIT_NAVIGATION_POLICY_DECISION (decision); - const auto action = webkit_navigation_policy_decision_get_navigation_action(nav); - const auto req = webkit_navigation_action_get_request(action); - const auto uri = String(webkit_uri_request_get_uri(req)); - - if (uri.starts_with(userConfig["meta_application_protocol"])) { - webkit_policy_decision_ignore(decision); - - if (window != nullptr && window->bridge != nullptr) { - SSC::JSON::Object json = SSC::JSON::Object::Entries { - {"url", uri } - }; - - window->bridge->router.emit("applicationurl", json.str()); - } - - return false; - } - - if (uri.find("socket:") != 0 && uri.find(devHost) != 0) { - webkit_policy_decision_ignore(decision); - return false; - } - - return true; - })), - this - ); - - g_signal_connect( - G_OBJECT(webview), - "load-changed", - G_CALLBACK(+[](WebKitWebView* wv, WebKitLoadEvent event, gpointer arg) { - auto *window = static_cast<Window*>(arg); - if (event == WEBKIT_LOAD_STARTED) { - window->app.isReady = false; - } - - if (event == WEBKIT_LOAD_FINISHED) { - window->app.isReady = true; - } - }), - this - ); - // Calling gtk_drag_source_set interferes with text selection /* gtk_drag_source_set( webview, @@ -472,107 +561,132 @@ namespace SSC { droppableTypes, G_N_ELEMENTS(droppableTypes), GDK_ACTION_COPY - ); */ + ); - /* gtk_drag_dest_set( + gtk_drag_dest_set( webview, GTK_DEST_DEFAULT_ALL, droppableTypes, 1, GDK_ACTION_MOVE - ); */ + ); g_signal_connect( - G_OBJECT(webview), - "drag-begin", - G_CALLBACK(+[](GtkWidget *wv, GdkDragContext *context, gpointer arg) { - auto *w = static_cast<Window*>(arg); - w->isDragInvokedInsideWindow = true; - - GdkDevice* device; - gint wx; - gint wy; - gint x; - gint y; - - device = gdk_drag_context_get_device(context); - gdk_device_get_window_at_position(device, &x, &y); - gdk_device_get_position(device, 0, &wx, &wy); - - String js( - "(() => {" - " let el = null;" - " try { el = document.elementFromPoint(" + std::to_string(x) + "," + std::to_string(y) + "); }" - " catch (err) { console.error(err.stack || err.message || err); }" - " if (!el) return;" - " const found = el.matches('[data-src]') ? el : el.closest('[data-src]');" - " return found && found.dataset.src" - "})()" - ); - - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(wv), - js.c_str(), - -1, - nullptr, - nullptr, - nullptr, - [](GObject* src, GAsyncResult* result, gpointer arg) { - auto *w = static_cast<Window*>(arg); - if (!w) return; - - GError* error = NULL; - auto value = webkit_web_view_evaluate_javascript_finish( - WEBKIT_WEB_VIEW(src), - result, - &error - ); - - if (!value || error) return; + G_OBJECT(this->webview), + "button-release-event", + G_CALLBACK(+[](GtkWidget* wv, GdkEventButton* event, gpointer arg) { + auto* w = static_cast<Window*>(arg); + w->shouldDrag = false; + w->dragStart.x = 0; + w->dragStart.y = 0; + w->dragging.x = 0; + w->dragging.y = 0; + return FALSE; + }), + this + ); - if (!jsc_value_is_string(value)) return; + g_signal_connect( + G_OBJECT(this->webview), + "button-press-event", + G_CALLBACK(+[](GtkWidget* wv, GdkEventButton* event, gpointer arg) { + auto* w = static_cast<Window*>(arg); + w->shouldDrag = false; + + if (event->button == GDK_BUTTON_PRIMARY) { + auto win = GDK_WINDOW(gtk_widget_get_window(w->window)); + gint initialX; + gint initialY; + + gdk_window_get_position(win, &initialX, &initialY); + + w->dragStart.x = initialX; + w->dragStart.y = initialY; + + w->dragging.x = event->x_root; + w->dragging.y = event->y_root; + + GdkDevice* device; + + gint x = event->x; + gint y = event->y; + String sx = std::to_string(x); + String sy = std::to_string(y); + + String js( + "(() => { " + " const v = '--app-region'; " + " let el = document.elementFromPoint(" + sx + "," + sy + "); " + " " + " while (el) { " + " if (getComputedStyle(el).getPropertyValue(v) == 'drag') return 'movable'; " + " el = el.parentElement; " + " } " + " return '' " + "})() " + ); - JSCException *exception; - gchar *str_value = jsc_value_to_string(value); + webkit_web_view_evaluate_javascript( + WEBKIT_WEB_VIEW(wv), + js.c_str(), + -1, + nullptr, + nullptr, + nullptr, + [](GObject* src, GAsyncResult* result, gpointer arg) { + auto* w = static_cast<Window*>(arg); + if (!w) return; + + GError* error = NULL; + auto value = webkit_web_view_evaluate_javascript_finish( + WEBKIT_WEB_VIEW(w->webview), + result, + &error + ); + if (error) return; + if (!value) return; + if (!jsc_value_is_string(value)) return; - w->draggablePayload = split(str_value, ';'); - exception = jsc_context_get_exception(jsc_value_get_context(value)); + auto match = std::string(jsc_value_to_string(value)); + w->shouldDrag = match == "movable"; + return; + }, + w + ); + } - g_free(str_value); - }, - w - ); + return FALSE; }), this ); g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "focus", G_CALLBACK(+[]( - GtkWidget *wv, + GtkWidget* wv, GtkDirectionType direction, gpointer arg) { - auto *w = static_cast<Window*>(arg); + auto* w = static_cast<Window*>(arg); if (!w) return; }), this ); g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "drag-data-get", G_CALLBACK(+[]( - GtkWidget *wv, - GdkDragContext *context, - GtkSelectionData *data, + GtkWidget* wv, + GdkDragContext* context, + GtkSelectionData* data, guint info, guint time, gpointer arg) { - auto *w = static_cast<Window*>(arg); + auto* w = static_cast<Window*>(arg); if (!w) return; if (w->isDragInvokedInsideWindow) { @@ -603,22 +717,57 @@ namespace SSC { ); g_signal_connect( - G_OBJECT(webview), - "drag-motion", + G_OBJECT(this->webview), + "motion-notify-event", G_CALLBACK(+[]( - GtkWidget *wv, - GdkDragContext *context, - gint x, - gint y, - guint32 time, + GtkWidget* wv, + GdkEventMotion* event, gpointer arg) { - auto *w = static_cast<Window*>(arg); - if (!w) return; + auto* w = static_cast<Window*>(arg); + if (!w) return FALSE; + + if (w->shouldDrag && event->state & GDK_BUTTON1_MASK) { + auto win = GDK_WINDOW(gtk_widget_get_window(w->window)); + gint x; + gint y; + + GdkRectangle frame_extents; + gdk_window_get_frame_extents(win, &frame_extents); + + GtkAllocation allocation; + gtk_widget_get_allocation(wv, &allocation); + + gint menubarHeight = 0; + + if (w->menubar) { + GtkAllocation allocationMenubar; + gtk_widget_get_allocation(w->menubar, &allocationMenubar); + menubarHeight = allocationMenubar.height; + } + + int offsetWidth = (frame_extents.width - allocation.width) / 2; + int offsetHeight = (frame_extents.height - allocation.height) - offsetWidth - menubarHeight; + + gdk_window_get_position(win, &x, &y); + + gint offset_x = event->x_root - w->dragging.x; + gint offset_y = event->y_root - w->dragging.y; + + gint newX = x + offset_x; + gint newY = y + offset_y; + + gdk_window_move(win, newX - offsetWidth, newY - offsetHeight); - w->dragLastX = x; - w->dragLastY = y; + w->dragging.x = event->x_root; + w->dragging.y = event->y_root; + } + + return FALSE; + // + // TODO(@heapwolf): refactor legacy drag and drop stuff + // // char* target_uri = g_file_get_uri(drag_info->target_location); int count = w->draggablePayload.size(); @@ -646,53 +795,28 @@ namespace SSC { // https://wiki.gnome.org/action/show/Newcomers/OldDragNDropTutorial?action=show&redirect=Newcomers%2FDragNDropTutorial g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "drag-end", - G_CALLBACK(+[](GtkWidget *wv, GdkDragContext *context, gpointer arg) { - auto *w = static_cast<Window*>(arg); - if (!w) return; - - w->isDragInvokedInsideWindow = false; - w->draggablePayload.clear(); - w->eval(getEmitToRenderProcessJavaScript("dragend", "{}")); - }), - this - ); - - g_signal_connect( - G_OBJECT(window), - "button-release-event", - G_CALLBACK(+[](GtkWidget* window, GdkEventButton event, gpointer arg) { - auto *w = static_cast<Window*>(arg); + G_CALLBACK(+[](GtkWidget* wv, GdkDragContext* context, gpointer arg) { + auto* w = static_cast<Window*>(arg); if (!w) return; - /** - * Calling w->eval() inside button-release-event causes - * a Segmentation Fault on Ubuntu 22, but works fine on - * other linuxes like Ubuntu 20. - * - * The dragend feature causes the application to crash in - * all operations including non drag-drop and causes applications - * that do not use drag and drop at all to crash. - * - * So disabling this experimental linux dragdrop code for now. - */ - // w->isDragInvokedInsideWindow = false; - // w->eval(getEmitToRenderProcessJavaScript("dragend", "{}")); + // w->draggablePayload.clear(); + w->eval(getEmitToRenderProcessJavaScript("dragend", "{}")); }), this ); g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "drag-data-received", G_CALLBACK(+[]( GtkWidget* wv, GdkDragContext* context, gint x, gint y, - GtkSelectionData *data, + GtkSelectionData* data, guint info, guint time, gpointer arg) @@ -722,7 +846,7 @@ namespace SSC { ); g_signal_connect( - G_OBJECT(webview), + G_OBJECT(this->webview), "drag-drop", G_CALLBACK(+[]( GtkWidget* widget, @@ -762,133 +886,144 @@ namespace SSC { }), this ); + */ g_signal_connect( - G_OBJECT(window), + G_OBJECT(this->window), "destroy", - G_CALLBACK(+[](GtkWidget*, gpointer arg) { - auto* w = static_cast<Window*>(arg); - w->close(0); - }), - this - ); + G_CALLBACK((+[](GtkWidget* object, gpointer arg) { + auto app = App::sharedApplication(); + if (app == nullptr || app->shouldExit) { + return FALSE; + } - g_signal_connect( - G_OBJECT(window), - "delete-event", - G_CALLBACK(+[](GtkWidget* widget, GdkEvent*, gpointer arg) { - auto* w = static_cast<Window*>(arg); + auto w = reinterpret_cast<Window*>(arg); + int index = w != nullptr ? w->index : -1; - if (w->opts.canExit == false) { - w->eval(getEmitToRenderProcessJavaScript("windowHide", "{}")); - return gtk_widget_hide_on_delete(widget); + for (auto& window : app->windowManager.windows) { + if (window == nullptr) { + continue; + } + + if (window->window == object) { + index = window->index; + if (window->webview) { + window->webview = g_object_ref(window->webview); + } + window->window = nullptr; + window->vbox = nullptr; + break; + } } - w->close(0); + if (index >= 0) { + for (auto window : app->windowManager.windows) { + if (window == nullptr || window->index == index) { + continue; + } + + JSON::Object json = JSON::Object::Entries { + {"data", index} + }; + + window->eval(getEmitToRenderProcessJavaScript("window-closed", json.str())); + } + + app->windowManager.destroyWindow(index); + } return FALSE; - }), + })), this ); g_signal_connect( - G_OBJECT(window), + G_OBJECT(this->window), "size-allocate", // https://docs.gtk.org/gtk3/method.Window.get_size.html - G_CALLBACK(+[](GtkWidget* widget,GtkAllocation *allocation, gpointer arg) { + G_CALLBACK(+[](GtkWidget* widget,GtkAllocation* allocation, gpointer arg) { auto* w = static_cast<Window*>(arg); - gtk_window_get_size(GTK_WINDOW(widget), &w->width, &w->height); + gtk_window_get_size(GTK_WINDOW(widget), &w->size.width, &w->size.height); }), this ); - String preload = createPreload(opts); - - WebKitUserContentManager *manager = - webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(webview)); - - webkit_user_content_manager_add_script( - manager, - webkit_user_script_new( - preload.c_str(), - WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, - WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, - nullptr, - nullptr - ) - ); - - // ALWAYS on or off - webkit_settings_set_enable_webgl(settings, true); - webkit_settings_set_zoom_text_only(settings, false); - webkit_settings_set_enable_mediasource(settings, true); - webkit_settings_set_enable_encrypted_media(settings, true); - webkit_settings_set_media_playback_allows_inline(settings, true); - webkit_settings_set_enable_dns_prefetching(settings, true); - - // TODO(@jwerle); make configurable with '[permissions] allow_dialogs' - webkit_settings_set_allow_modal_dialogs( - settings, - true - ); - - // TODO(@jwerle); make configurable with '[permissions] allow_media' - webkit_settings_set_enable_media(settings, true); - webkit_settings_set_enable_webaudio(settings, true); - - webkit_settings_set_enable_media_stream( - settings, - userConfig["permissions_allow_user_media"] != "false" - ); - - webkit_settings_set_enable_media_capabilities( - settings, - userConfig["permissions_allow_user_media"] != "false" - ); - - webkit_settings_set_enable_webrtc( - settings, - userConfig["permissions_allow_user_media"] != "false" - ); + if (this->options.aspectRatio.size() > 0) { + g_signal_connect( + window, + "configure-event", + G_CALLBACK(+[](GtkWidget* widget, GdkEventConfigure* event, gpointer ptr) { + auto w = static_cast<Window*>(ptr); + if (!w) return FALSE; + + // TODO(@heapwolf): make the parsed aspectRatio properties so it doesnt need to be recalculated. + auto parts = split(w->options.aspectRatio, ':'); + float aspectWidth = 0; + float aspectHeight = 0; + + try { + aspectWidth = std::stof(trim(parts[0])); + aspectHeight = std::stof(trim(parts[1])); + } catch (...) { + return FALSE; + } - webkit_settings_set_javascript_can_access_clipboard( - settings, - userConfig["permissions_allow_clipboard"] != "false" - ); + if (aspectWidth > 0 && aspectHeight > 0) { + GdkGeometry geom; + geom.min_aspect = aspectWidth / aspectHeight; + geom.max_aspect = geom.min_aspect; + gtk_window_set_geometry_hints(GTK_WINDOW(widget), widget, &geom, GdkWindowHints(GDK_HINT_ASPECT)); + } - webkit_settings_set_enable_fullscreen( - settings, - userConfig["permissions_allow_fullscreen"] != "false" - ); + return FALSE; + }), + this + ); - webkit_settings_set_enable_html5_local_storage( - settings, - userConfig["permissions_allow_data_access"] != "false" - ); + // gtk_window_set_aspect_ratio(GTK_WINDOW(window), aspectRatio, TRUE); + } + } - webkit_settings_set_enable_html5_database( - settings, - userConfig["permissions_allow_data_access"] != "false" - ); + Window::~Window () { + auto app = App::sharedApplication(); - GdkRGBA rgba = {0}; - webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(webview), &rgba); + if (!app || app->shouldExit) { + return; + } - if (this->opts.debug) { - webkit_settings_set_enable_developer_extras(settings, true); + if (this->policies) { + g_object_unref(this->policies); + this->policies = nullptr; } - webkit_settings_set_allow_universal_access_from_file_urls(settings, true); - webkit_settings_set_allow_file_access_from_file_urls(settings, true); + if (this->settings) { + g_object_unref(this->settings); + this->settings = nullptr; + } - // webkit_settings_set_allow_top_navigation_to_data_urls(settings, true); + if (this->userContentManager) { + g_object_unref(this->userContentManager); + this->userContentManager = nullptr; + } - vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + if (this->accelGroup) { + g_object_unref(this->accelGroup); + this->accelGroup = nullptr; + } - gtk_box_pack_end(GTK_BOX(vbox), webview, true, true, 0); + if (this->webview) { + gtk_widget_set_can_focus(GTK_WIDGET(this->webview), false); + this->webview = nullptr; + } - gtk_container_add(GTK_CONTAINER(window), vbox); - gtk_widget_add_events(window, GDK_ALL_EVENTS_MASK); + if (this->vbox) { + gtk_container_remove(GTK_CONTAINER(this->window), this->vbox); + this->vbox = nullptr; + } - gtk_widget_grab_focus(GTK_WIDGET(webview)); + if (this->window) { + auto window = this->window; + this->window = nullptr; + g_object_unref(window); + } } ScreenSize Window::getScreenSize () { @@ -952,39 +1087,153 @@ namespace SSC { width = (int) DEFAULT_MONITOR_WIDTH; } - return ScreenSize { height, width }; + return ScreenSize { width, height }; } - void Window::eval (const String& source) { - auto webview = this->webview; - this->app.dispatch([=, this] { - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(this->webview), - String(source).c_str(), - -1, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr - ); + void Window::eval (const String& source, const EvalCallback& callback) { + auto app = App::sharedApplication(); + app->dispatch([=, this] () { + if (this->webview) { + webkit_web_view_evaluate_javascript( + this->webview, + source.c_str(), + source.size(), + nullptr, // world name + nullptr, // source URI + nullptr, // cancellable + +[]( // callback + GObject* object, + GAsyncResult* result, + gpointer userData + ) { + const auto callback = reinterpret_cast<const EvalCallback*>(userData); + if (callback == nullptr || *callback == nullptr) { + auto value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(object), result, nullptr); + return; + } + + GError* error = nullptr; + auto value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error); + + if (!value) { + if (error != nullptr) { + (*callback)(JSON::Error(error->message)); + g_error_free(error); + } else { + (*callback)(JSON::Error("An unknown error occurred")); + } + } else if (jsc_value_is_string(value)) { + const auto context = jsc_value_get_context(value); + const auto exception = jsc_context_get_exception(context); + const auto stringValue = jsc_value_to_string(value); + + if (exception) { + const auto message = jsc_exception_get_message(exception); + (*callback)(JSON::Error(message)); + } else { + (*callback)(stringValue); + } + + g_free(stringValue); + } else if (jsc_value_is_boolean(value)) { + const auto context = jsc_value_get_context(value); + const auto exception = jsc_context_get_exception(context); + const auto booleanValue = jsc_value_to_boolean(value); + + if (exception) { + const auto message = jsc_exception_get_message(exception); + (*callback)(JSON::Error(message)); + } else { + (*callback)(booleanValue); + } + } else if (jsc_value_is_null(value)) { + const auto context = jsc_value_get_context(value); + const auto exception = jsc_context_get_exception(context); + + if (exception) { + const auto message = jsc_exception_get_message(exception); + (*callback)(JSON::Error(message)); + } else { + (*callback)(nullptr); + } + } else if (jsc_value_is_number(value)) { + const auto context = jsc_value_get_context(value); + const auto exception = jsc_context_get_exception(context); + const auto numberValue = jsc_value_to_double(value); + + if (exception) { + const auto message = jsc_exception_get_message(exception); + (*callback)(JSON::Error(message)); + } else { + (*callback)(numberValue); + } + } else if (jsc_value_is_undefined(value)) { + const auto context = jsc_value_get_context(value); + const auto exception = jsc_context_get_exception(context); + + if (exception) { + const auto message = jsc_exception_get_message(exception); + (*callback)(JSON::Error(message)); + } else { + (*callback)(nullptr); + } + } + + if (value) { + //webkit_javascript_result_unref(reinterpret_cast<WebKitJavascriptResult*>(value)); + } + + delete callback; + }, + callback == nullptr + ? nullptr + : new EvalCallback(std::move(callback)) + ); + } }); } void Window::show () { - gtk_widget_realize(this->window); - - this->index = this->opts.index; - if (this->opts.headless == false) { - gtk_widget_show_all(this->window); - gtk_window_present(GTK_WINDOW(this->window)); - } + auto app = App::sharedApplication(); + app->dispatch([=, this] () { + if (this->window != nullptr) { + gtk_widget_realize(this->window); + + this->index = this->options.index; + if (this->options.headless == false) { + gtk_widget_show_all(this->window); + gtk_window_present(GTK_WINDOW(this->window)); + } + } + }); } void Window::hide () { gtk_widget_realize(this->window); gtk_widget_hide(this->window); - this->eval(getEmitToRenderProcessJavaScript("windowHide", "{}")); + this->eval(getEmitToRenderProcessJavaScript("window-hidden", "{}")); + } + + void Window::setBackgroundColor (const String& rgba) { + const auto parts = split(trim(replace(replace(rgba, "rgba(", ""), ")", "")), ','); + int r = 0, g = 0, b = 0; + float a = 1.0; + + if (parts.size() == 4) { + try { r = std::stoi(trim(parts[0])); } + catch (...) {} + + try { g = std::stoi(trim(parts[1])); } + catch (...) {} + + try { b = std::stoi(trim(parts[2])); } + catch (...) {} + + try { a = std::stof(trim(parts[3])); } + catch (...) {} + + return this->setBackgroundColor(r, g, b, a); + } } void Window::setBackgroundColor (int r, int g, int b, float a) { @@ -994,28 +1243,60 @@ namespace SSC { color.blue = b / 255.0; color.alpha = a; - gtk_widget_realize(this->window); + if (this->window) { + gtk_widget_realize(this->window); + // FIXME(@jwerle): this is deprecated + gtk_widget_override_background_color( + this->window, GTK_STATE_FLAG_NORMAL, &color + ); + } + } + + String Window::getBackgroundColor () { + GtkStyleContext* context = gtk_widget_get_style_context(this->window); + + GdkRGBA color; // FIXME(@jwerle): this is deprecated - gtk_widget_override_background_color( - this->window, GTK_STATE_FLAG_NORMAL, &color + gtk_style_context_get_background_color(context, gtk_widget_get_state_flags(this->window), &color); + + char string[100]; + + snprintf( + string, + sizeof(string), + "rgba(%d, %d, %d, %f)", + (int) (255 * color.red), + (int) (255 * color.green), + (int) (255 * color.blue), + color.alpha ); + + return string; } void Window::showInspector () { - // this->webview->inspector.show(); + if (this->webview) { + const auto inspector = webkit_web_view_get_inspector(this->webview); + if (inspector) { + webkit_web_inspector_show(inspector); + } + } } void Window::exit (int code) { - if (onExit != nullptr) onExit(code); + const auto callback = this->onExit; + this->onExit = nullptr; + if (callback != nullptr) { + callback(code); + } } - void Window::kill () { - // gtk releases objects automatically. - } + void Window::kill () {} - void Window::close (int code) { - if (opts.canExit) { - this->exit(code); + void Window::close (int _) { + if (this->window) { + g_object_ref(this->window); + gtk_window_close(GTK_WINDOW(this->window)); } } @@ -1031,42 +1312,44 @@ namespace SSC { gtk_window_deiconify(GTK_WINDOW(window)); } - void Window::navigate (const String &seq, const String &url) { - webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url.c_str()); - - if (seq.size() > 0) { - auto index = std::to_string(this->opts.index); - this->resolvePromise(seq, "0", index); + void Window::navigate (const String& url) { + static auto app = App::sharedApplication(); + auto webview = this->webview; + if (webview) { + app->dispatch([=] () { + webkit_web_view_load_uri(webview, url.c_str()); + }); } } - SSC::String Window::getTitle () { - auto title = gtk_window_get_title(GTK_WINDOW(window)); - return String(title != nullptr ? title : ""); - } + const String Window::getTitle () const { + if (this->window != nullptr) { + const auto title = gtk_window_get_title(GTK_WINDOW(this->window)); + if (title != nullptr) { + return title; + } + } - void Window::setTitle (const String &s) { - gtk_widget_realize(window); - gtk_window_set_title(GTK_WINDOW(window), s.c_str()); + return ""; } - int Window::openExternal (const String& url) { + void Window::setTitle (const String& s) { gtk_widget_realize(window); - return gtk_show_uri_on_window(GTK_WINDOW(window), url.c_str(), GDK_CURRENT_TIME, nullptr); + gtk_window_set_title(GTK_WINDOW(window), s.c_str()); } void Window::about () { - GtkWidget *dialog = gtk_dialog_new(); + GtkWidget* dialog = gtk_dialog_new(); gtk_window_set_default_size(GTK_WINDOW(dialog), 300, 200); - GtkWidget *body = gtk_dialog_get_content_area(GTK_DIALOG(GTK_WINDOW(dialog))); - GtkContainer *content = GTK_CONTAINER(body); + GtkWidget* body = gtk_dialog_get_content_area(GTK_DIALOG(GTK_WINDOW(dialog))); + GtkContainer* content = GTK_CONTAINER(body); String imgPath = "/usr/share/icons/hicolor/256x256/apps/" + - app.appData["build_name"] + + this->bridge.userConfig["build_name"] + ".png"; - GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_scale( + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_scale( imgPath.c_str(), 60, 60, @@ -1074,25 +1357,25 @@ namespace SSC { nullptr ); - GtkWidget *img = gtk_image_new_from_pixbuf(pixbuf); + GtkWidget* img = gtk_image_new_from_pixbuf(pixbuf); gtk_widget_set_margin_top(img, 20); gtk_widget_set_margin_bottom(img, 20); gtk_box_pack_start(GTK_BOX(content), img, false, false, 0); - String title_value(app.appData["build_name"] + " v" + app.appData["meta_version"]); + String title_value(this->bridge.userConfig["build_name"] + " v" + this->bridge.userConfig["meta_version"]); String version_value("Built with ssc v" + SSC::VERSION_FULL_STRING); - GtkWidget *label_title = gtk_label_new(""); + GtkWidget* label_title = gtk_label_new(""); gtk_label_set_markup(GTK_LABEL(label_title), title_value.c_str()); gtk_container_add(content, label_title); - GtkWidget *label_op_version = gtk_label_new(""); + GtkWidget* label_op_version = gtk_label_new(""); gtk_label_set_markup(GTK_LABEL(label_op_version), version_value.c_str()); gtk_container_add(content, label_op_version); - GtkWidget *label_copyright = gtk_label_new(""); - gtk_label_set_markup(GTK_LABEL(label_copyright), app.appData["meta_copyright"].c_str()); + GtkWidget* label_copyright = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(label_copyright), this->bridge.userConfig["meta_copyright"].c_str()); gtk_container_add(content, label_copyright); g_signal_connect( @@ -1109,8 +1392,18 @@ namespace SSC { gtk_dialog_run(GTK_DIALOG(dialog)); } - ScreenSize Window::getSize () { - return ScreenSize { this->height, this->width }; + Window::Size Window::getSize () { + gtk_widget_get_size_request( + this->window, + &this->size.width, + &this->size.height + ); + + return this->size; + } + + const Window::Size Window::getSize () const { + return this->size; } void Window::setSize (int width, int height, int hints) { @@ -1134,31 +1427,37 @@ namespace SSC { gtk_window_set_geometry_hints(GTK_WINDOW(window), nullptr, &g, h); } - this->width = width; - this->height = height; + this->size.width = width; + this->size.height = height; + } + + void Window::setPosition (float x, float y) { + gtk_window_move(GTK_WINDOW(this->window), (int) x, (int) y); + this->position.x = x; + this->position.y = y; } - void Window::setTrayMenu (const String& seq, const String& value) { - this->setMenu(seq, value, true); + void Window::setTrayMenu (const String& value) { + this->setMenu(value, true); } - void Window::setSystemMenu (const String& seq, const String& value) { - this->setMenu(seq, value, false); + void Window::setSystemMenu (const String& value) { + this->setMenu(value, false); } - void Window::setMenu (const String& seq, const String& source, const bool& isTrayMenu) { - if (source.empty()) { + void Window::setMenu (const String& menuSource, const bool& isTrayMenu) { + if (menuSource.empty()) { return; } - auto menuSource = replace(SSC::String(source), "%%", "\n"); // copy and deserialize - auto clear = [this](GtkWidget* menu) { - GList *iter; - GList *children = gtk_container_get_children(GTK_CONTAINER(menu)); + GList* iter; + GList* children = gtk_container_get_children(GTK_CONTAINER(menu)); for (iter = children; iter != nullptr; iter = g_list_next(iter)) { - gtk_widget_destroy(GTK_WIDGET(iter->data)); + if (iter && iter->data) { + gtk_widget_destroy(GTK_WIDGET(iter->data)); + } } g_list_free(children); @@ -1171,6 +1470,12 @@ namespace SSC { menubar = menubar == nullptr ? gtk_menu_bar_new() : clear(menubar); } + GtkStyleContext* context = gtk_widget_get_style_context(this->window); + + GdkRGBA color = {0.0, 0.0, 0.0, 0.0}; + webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(this->webview), &color); + gtk_widget_override_background_color(menubar, GTK_STATE_FLAG_NORMAL, &color); + auto menus = split(menuSource, ';'); for (auto m : menus) { @@ -1181,8 +1486,9 @@ namespace SSC { auto menuParts = split(line, ':'); auto menuTitle = menuParts[0]; // if this is a tray menu, append directly to the tray instead of a submenu. - auto *ctx = isTrayMenu ? menutray : gtk_menu_new(); - GtkWidget *menuItem = gtk_menu_item_new_with_label(menuTitle.c_str()); + auto* ctx = isTrayMenu ? menutray : gtk_menu_new(); + GtkWidget* menuItem = gtk_menu_item_new_with_label(menuTitle.c_str()); + gtk_widget_override_background_color(menuItem, GTK_STATE_FLAG_NORMAL, &color); if (isTrayMenu && menuSource.size() == 1) { if (menuParts.size() > 1) { @@ -1192,7 +1498,7 @@ namespace SSC { g_signal_connect( G_OBJECT(menuItem), "activate", - G_CALLBACK(+[](GtkWidget *t, gpointer arg) { + G_CALLBACK(+[](GtkWidget* t, gpointer arg) { auto w = static_cast<Window*>(arg); auto title = gtk_menu_item_get_label(GTK_MENU_ITEM(t)); auto parent = gtk_widget_get_name(t); @@ -1209,7 +1515,7 @@ namespace SSC { auto title = parts[0]; String key = ""; - GtkWidget *item; + GtkWidget* item; if (parts[0].find("---") != -1) { item = gtk_separator_menu_item_new(); @@ -1222,6 +1528,10 @@ namespace SSC { if (key.size() > 0) { auto accelerator = split(parts[1], '+'); + if (accelerator.size() <= 1) { + continue; + } + auto modifier = trim(accelerator[1]); // normalize modifier to lower case std::transform( @@ -1272,7 +1582,7 @@ namespace SSC { g_signal_connect( G_OBJECT(item), "activate", - G_CALLBACK(+[](GtkWidget *t, gpointer arg) { + G_CALLBACK(+[](GtkWidget* t, gpointer arg) { auto w = static_cast<Window*>(arg); auto title = gtk_menu_item_get_label(GTK_MENU_ITEM(t)); auto parent = gtk_widget_get_name(t); @@ -1285,7 +1595,7 @@ namespace SSC { g_signal_connect( G_OBJECT(item), "activate", - G_CALLBACK(+[](GtkWidget *t, gpointer arg) { + G_CALLBACK(+[](GtkWidget* t, gpointer arg) { auto w = static_cast<Window*>(arg); auto title = gtk_menu_item_get_label(GTK_MENU_ITEM(t)); auto parent = gtk_widget_get_name(t); @@ -1306,6 +1616,7 @@ namespace SSC { } gtk_widget_set_name(item, menuTitle.c_str()); + gtk_widget_override_background_color(menuItem, GTK_STATE_FLAG_NORMAL, &color); gtk_menu_shell_append(GTK_MENU_SHELL(ctx), item); } @@ -1319,8 +1630,8 @@ namespace SSC { if (isTrayMenu) { static auto userConfig = SSC::getUserConfig(); - static auto app = App::instance(); - GtkStatusIcon *trayIcon; + static auto app = App::sharedApplication(); + GtkStatusIcon* trayIcon; auto cwd = app->getcwd(); auto trayIconPath = String("application_tray_icon"); @@ -1349,23 +1660,18 @@ namespace SSC { g_signal_connect( trayIcon, "activate", - G_CALLBACK(+[](GtkWidget *t, gpointer arg) { + G_CALLBACK(+[](GtkWidget* t, gpointer arg) { auto w = static_cast<Window*>(arg); gtk_menu_popup_at_pointer(GTK_MENU(w->menutray), NULL); - w->bridge->router.emit("tray", "true"); + w->bridge.emit("tray", true); }), this ); gtk_widget_show_all(menutray); } else { - gtk_box_pack_start(GTK_BOX(vbox), menubar, false, false, 0); + gtk_box_pack_start(GTK_BOX(this->vbox), menubar, false, false, 0); gtk_widget_show_all(menubar); } - - if (seq.size() > 0) { - auto index = std::to_string(this->opts.index); - this->resolvePromise(seq, "0", index); - } } void Window::setSystemMenuItemEnabled (bool enabled, int barPos, int menuPos) { @@ -1373,49 +1679,49 @@ namespace SSC { } void Window::closeContextMenu () { - if (popupId > 0) { - popupId = 0; - auto seq = std::to_string(popupId); + if (this->contextMenuID > 0) { + const auto seq = std::to_string(this->contextMenuID); + this->contextMenuID = 0; closeContextMenu(seq); } } - void Window::closeContextMenu (const String &seq) { - if (popup != nullptr) { - auto ptr = popup; - popup = nullptr; + void Window::closeContextMenu (const String& seq) { + if (contextMenu != nullptr) { + auto ptr = contextMenu; + contextMenu = nullptr; closeContextMenu(ptr, seq); } } void Window::closeContextMenu ( - GtkWidget *popupMenu, - const String &seq + GtkWidget* contextMenu, + const String& seq ) { - if (popupMenu != nullptr) { - gtk_menu_popdown((GtkMenu *) popupMenu); - gtk_widget_destroy(popupMenu); + if (contextMenu != nullptr) { + gtk_menu_popdown((GtkMenu* ) contextMenu); + gtk_widget_destroy(contextMenu); this->eval(getResolveMenuSelectionJavaScript(seq, "", "contextMenu", "context")); } } void Window::setContextMenu ( - const String &seq, - const String &value + const String& seq, + const String& menuSource ) { closeContextMenu(); + if (menuSource.empty()) return void(0); // members - popup = gtk_menu_new(); + this->contextMenu = gtk_menu_new(); try { - popupId = std::stoi(seq); + this->contextMenuID = std::stoi(seq); } catch (...) { - popupId = 0; + this->contextMenuID = 0; } - auto menuData = replace(value, "_", "\n"); - auto menuItems = split(menuData, '\n'); + auto menuItems = split(menuSource, '\n'); for (auto itemData : menuItems) { if (trim(itemData).size() == 0) { @@ -1423,35 +1729,37 @@ namespace SSC { } if (itemData.find("---") != -1) { - auto *item = gtk_separator_menu_item_new(); + auto* item = gtk_separator_menu_item_new(); gtk_widget_show(item); - gtk_menu_shell_append(GTK_MENU_SHELL(popup), item); + gtk_menu_shell_append(GTK_MENU_SHELL(this->contextMenu), item); continue; } auto pair = split(itemData, ':'); - auto meta = String(seq + ";" + pair[0].c_str()); - auto *item = gtk_menu_item_new_with_label(pair[0].c_str()); + auto meta = String(seq + ";" + itemData); + auto* item = gtk_menu_item_new_with_label(pair[0].c_str()); g_signal_connect( G_OBJECT(item), "activate", - G_CALLBACK(+[](GtkWidget *t, gpointer arg) { + G_CALLBACK(+[](GtkWidget* t, gpointer arg) { auto window = static_cast<Window*>(arg); - auto label = gtk_menu_item_get_label(GTK_MENU_ITEM(t)); - auto title = String(label); + if (!window) return; + auto meta = gtk_widget_get_name(t); auto pair = split(meta, ';'); auto seq = pair[0]; + auto items = split(pair[1], ":"); - window->eval(getResolveMenuSelectionJavaScript(seq, title, "contextMenu", "context")); + if (items.size() != 2) return; + window->eval(getResolveMenuSelectionJavaScript(seq, trim(items[0]), trim(items[1]), "context")); }), this ); gtk_widget_set_name(item, meta.c_str()); gtk_widget_show(item); - gtk_menu_shell_append(GTK_MENU_SHELL(popup), item); + gtk_menu_shell_append(GTK_MENU_SHELL(this->contextMenu), item); } GdkRectangle rect; @@ -1475,13 +1783,13 @@ namespace SSC { rect.x = x - 1; rect.y = y - 1; - gtk_widget_add_events(popup, GDK_ALL_EVENTS_MASK); - gtk_widget_set_can_focus(popup, true); - gtk_widget_show_all(popup); - gtk_widget_grab_focus(popup); + gtk_widget_add_events(contextMenu, GDK_ALL_EVENTS_MASK); + gtk_widget_set_can_focus(contextMenu, true); + gtk_widget_show_all(contextMenu); + gtk_widget_grab_focus(contextMenu); gtk_menu_popup_at_rect( - GTK_MENU(popup), + GTK_MENU(contextMenu), win, &rect, GDK_GRAVITY_SOUTH_WEST, @@ -1489,4 +1797,20 @@ namespace SSC { event ); } + + void Window::handleApplicationURL (const String& url) { + JSON::Object json = JSON::Object::Entries {{ + "url", url + }}; + + if (this->index == 0 && this->window && this->webview) { + gtk_widget_show_all(GTK_WIDGET(this->window)); + gtk_widget_grab_focus(GTK_WIDGET(this->webview)); + gtk_widget_grab_focus(GTK_WIDGET(this->window)); + gtk_window_activate_focus(GTK_WINDOW(this->window)); + gtk_window_present(GTK_WINDOW(this->window)); + } + + this->bridge.emit("applicationurl", json.str()); + } } diff --git a/src/window/manager.cc b/src/window/manager.cc new file mode 100644 index 0000000000..36ab905350 --- /dev/null +++ b/src/window/manager.cc @@ -0,0 +1,455 @@ +#include "../app/app.hh" +#include "window.hh" + +namespace SSC { + WindowManager::WindowManager (SharedPointer<Core> core) + : core(core), + windows(SOCKET_RUNTIME_MAX_WINDOWS + SOCKET_RUNTIME_MAX_WINDOWS_RESERVED) + {} + + WindowManager::~WindowManager () { + this->destroy(); + } + + void WindowManager::WindowManager::destroy () { + if (this->destroyed) { + return; + } + + this->windows.clear(); + this->destroyed = true; + } + + void WindowManager::WindowManager::configure (const WindowManagerOptions& options) { + if (this->destroyed) { + return; + } + + this->options = options; + if (this->options.userConfig.size() == 0) { + this->options.userConfig = getUserConfig(); + } + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::WindowManager::getWindow ( + int index, + WindowStatus status + ) { + Lock lock(this->mutex); + + if (this->destroyed || index < 0 || index >= this->windows.size()) { + return nullptr; + } + + if ( + this->getWindowStatus(index) > WindowStatus::WINDOW_NONE && + this->getWindowStatus(index) < status && + this->windows[index] != nullptr + ) { + return this->windows[index]; + } + + return nullptr; + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::WindowManager::getWindow (int index) { + return this->getWindow(index, WindowStatus::WINDOW_EXITING); + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::getWindowForBridge ( + const IPC::Bridge* bridge + ) { + for (const auto& window : this->windows) { + if (window != nullptr && &window->bridge == bridge) { + return this->getWindow(window->index); + } + } + return nullptr; + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::getWindowForWebView (WebView* webview) { + for (const auto& window : this->windows) { + if (window != nullptr && window->webview == webview) { + return window; + } + } + return nullptr; + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::getWindowForClient (const IPC::Client& client) { + for (const auto& window : this->windows) { + if (window != nullptr && window->bridge.client.id == client.id) { + return this->getWindow(window->index); + } + } + + return nullptr; + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::WindowManager::getOrCreateWindow (int index) { + return this->getOrCreateWindow(index, Window::Options {}); + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::WindowManager::getOrCreateWindow ( + int index, + const Window::Options& options + ) { + if (this->destroyed || index < 0 || index >= this->windows.size()) { + return nullptr; + } + + if (this->getWindowStatus(index) == WindowStatus::WINDOW_NONE) { + Window::Options optionsCopy = options; + optionsCopy.index = index; + return this->createWindow(optionsCopy); + } + + return this->getWindow(index); + } + + WindowManager::WindowStatus WindowManager::getWindowStatus (int index) { + Lock lock(this->mutex); + + if (this->destroyed || index < 0 || index >= this->windows.size()) { + return WindowStatus::WINDOW_NONE; + } + + const auto window = this->windows[index]; + if (window != nullptr) { + return window->status; + } + + return WindowStatus::WINDOW_NONE; + } + + void WindowManager::destroyWindow (int index) { + Lock lock(this->mutex); + + if (this->destroyed || index < 0 || index >= this->windows.size()) { + return; + } + + auto window = this->windows[index]; + if (window != nullptr) { + window->close(); + + this->windows[index] = nullptr; + if (window->options.shouldExitApplicationOnClose) { + App::sharedApplication()->dispatch([window]() { + window->exit(0); + }); + } else { + window->kill(); + } + } + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::createWindow ( + const Window::Options& options + ) { + Lock lock(this->mutex); + + if ( + this->destroyed || + options.index < 0 || + options.index >= this->windows.size() + ) { + return nullptr; + } + + if (this->windows[options.index] != nullptr) { + return this->windows[options.index]; + } + + auto screen = Window::getScreenSize(); + + float width = options.width <= 0 + ? Window::getSizeInPixels(this->options.defaultWidth, screen.width) + : options.width; + float height = options.height <= 0 + ? Window::getSizeInPixels(this->options.defaultHeight, screen.height) + : options.height; + float minWidth = options.minWidth <= 0 + ? Window::getSizeInPixels(this->options.defaultMinWidth, screen.width) + : options.minWidth; + float minHeight = options.minHeight <= 0 + ? Window::getSizeInPixels(this->options.defaultMinHeight, screen.height) + : options.minHeight; + float maxWidth = options.maxWidth <= 0 + ? Window::getSizeInPixels(this->options.defaultMaxWidth, screen.width) + : options.maxWidth; + float maxHeight = options.maxHeight <= 0 + ? Window::getSizeInPixels(this->options.defaultMaxHeight, screen.height) + : options.maxHeight; + + Window::Options windowOptions = { + .minimizable = options.minimizable, + .maximizable = options.maximizable, + .resizable = options.resizable, + .closable = options.closable, + .frameless = options.frameless, + .utility = options.utility, + .shouldExitApplicationOnClose = options.shouldExitApplicationOnClose, + .maxHeight = maxHeight, + .minHeight = minHeight, + .height = height, + .maxWidth = maxWidth, + .minWidth = minWidth, + .width = width, + .radius = options.radius, + .margin = options.margin, + .aspectRatio = options.aspectRatio, + .titlebarStyle = options.titlebarStyle, + .windowControlOffsets = options.windowControlOffsets, + .backgroundColorLight = options.backgroundColorLight, + .backgroundColorDark = options.backgroundColorDark, + }; + + windowOptions.RUNTIME_PRIMORDIAL_OVERRIDES = options.RUNTIME_PRIMORDIAL_OVERRIDES; + windowOptions.userScript = options.userScript; + windowOptions.userConfig = this->options.userConfig; + windowOptions.headless = options.headless; + windowOptions.features = options.features; + windowOptions.debug = isDebugEnabled() || options.debug; + windowOptions.index = options.index; + windowOptions.argv = options.argv; + + for (auto const &entry : parseStringList(this->options.userConfig["build_env"])) { + const auto key = trim(entry); + + if (!Env::has(key)) { + continue; + } + + const auto value = decodeURIComponent(Env::get(key)); + + windowOptions.env[key] = value; + } + + if (options.userConfig.contains("build_env")) { + for (auto const &entry : parseStringList(options.userConfig.at("build_env"))) { + const auto key = trim(entry); + + if (!Env::has(key)) { + continue; + } + + const auto value = decodeURIComponent(Env::get(key)); + + windowOptions.env[key] = value; + } + } + + for (const auto& tuple : options.userConfig) { + windowOptions.userConfig[tuple.first] = tuple.second; + } + + auto window = std::make_shared<ManagedWindow>(*this, this->core, windowOptions); + + window->status = WindowStatus::WINDOW_CREATED; + window->onExit = this->options.onExit; + window->onMessage = this->options.onMessage; + + this->windows[options.index] = window; + + #if SOCKET_RUNTIME_PLATFORM_ANDROID + if (window->options.headless) { + window->status = WindowStatus::WINDOW_HIDDEN; + } else { + window->status = WindowStatus::WINDOW_SHOWN; + } + #endif + + return this->windows.at(options.index); + } + + SharedPointer<WindowManager::ManagedWindow> WindowManager::createDefaultWindow (const Window::Options& options) { + static const auto devHost = SSC::getDevHost(); + auto windowOptions = Window::Options { + .minimizable = options.minimizable, + .maximizable = options.maximizable, + .resizable = options.resizable, + .closable = options.closable, + .frameless = options.frameless, + .utility = options.utility, + .shouldExitApplicationOnClose = true, + .height = options.height, + .width = options.width, + .titlebarStyle = options.titlebarStyle, + .windowControlOffsets = options.windowControlOffsets, + .backgroundColorLight = options.backgroundColorLight, + .backgroundColorDark = options.backgroundColorDark, + }; + + windowOptions.index = 0; + windowOptions.headless = ( + options.userConfig.contains("build_headless") && + options.userConfig.at("build_headless") == "true" + ); + + if (options.userConfig.size() > 0) { + windowOptions.userConfig = options.userConfig; + } + + return this->createWindow(windowOptions); + } + + JSON::Array WindowManager::json (const Vector<int>& indices) { + auto i = 0; + JSON::Array result; + for (auto index : indices) { + auto window = this->getWindow(index); + if (window != nullptr) { + result[i++] = window->json(); + } + } + return result; + } + + bool WindowManager::emit (const String& event, const JSON::Any& json) { + bool status = false; + for (const auto& window : this->windows) { + if ( + window != nullptr && + // only "shown" or "hidden" managed windows will + // have events dispatched to them + window->status >= WINDOW_HIDDEN && + window->status < WINDOW_CLOSING + ) { + if (window->emit(event, json)) { + status = true; + } + } + } + + return status; + } + + WindowManager::ManagedWindow::ManagedWindow ( + WindowManager &manager, + SharedPointer<Core> core, + const Window::Options& options + ) : index(options.index), + Window(core, options), + manager(manager) + {} + + WindowManager::ManagedWindow::~ManagedWindow () {} + + void WindowManager::ManagedWindow::show () { + auto index = std::to_string(this->index); + this->backgroundColor = Color(Window::getBackgroundColor()); + status = WindowStatus::WINDOW_SHOWING; + Window::show(); + status = WindowStatus::WINDOW_SHOWN; + } + + void WindowManager::ManagedWindow::hide () { + auto index = std::to_string(this->index); + status = WindowStatus::WINDOW_HIDING; + Window::hide(); + status = WindowStatus::WINDOW_HIDDEN; + } + + void WindowManager::ManagedWindow::close (int code) { + if (status < WindowStatus::WINDOW_CLOSING) { + auto index = std::to_string(this->index); + status = WindowStatus::WINDOW_CLOSING; + Window::close(code); + status = WindowStatus::WINDOW_CLOSED; + } + } + + void WindowManager::ManagedWindow::exit (int code) { + if (status < WindowStatus::WINDOW_EXITING) { + auto index = std::to_string(this->index); + status = WindowStatus::WINDOW_EXITING; + Window::exit(code); + status = WindowStatus::WINDOW_EXITED; + } + } + + void WindowManager::ManagedWindow::kill () { + if (status < WindowStatus::WINDOW_KILLING) { + auto index = std::to_string(this->index); + status = WindowStatus::WINDOW_KILLING; + Window::kill(); + status = WindowStatus::WINDOW_KILLED; + } + } + + JSON::Object WindowManager::ManagedWindow::json () const { + const auto id = this->bridge.id; + const auto size = this->getSize(); + const auto index = this->index; + const auto readyState = String( + this->readyState == Window::ReadyState::Loading + ? "loading" + : this->readyState == Window::ReadyState::Interactive + ? "interactive" + : this->readyState == Window::ReadyState::Complete + ? "complete" + : "none" + ); + + return JSON::Object::Entries { + {"id", std::to_string(id)}, + {"index", index}, + {"title", this->getTitle()}, + {"width", size.width}, + {"height", size.height}, + {"status", this->status}, + {"readyState", readyState}, + {"backgroundColor", this->backgroundColor.json()}, + {"position", JSON::Object::Entries { + {"x", this->position.x}, + {"y", this->position.y} + }} + }; + } + + void WindowManager::ManagedWindow::onReadyStateChange (const ReadyState& readyState) { + if (readyState == ReadyState::Complete) { + const auto pendingApplicationURLs = this->pendingApplicationURLs; + this->pendingApplicationURLs.clear(); + for (const auto& url : pendingApplicationURLs) { + Window::handleApplicationURL(url); + } + } + } + + void WindowManager::ManagedWindow::handleApplicationURL (const String& url) { + Lock lock(this->mutex); + this->pendingApplicationURLs.push_back(url); + if (this->readyState == ReadyState::Complete) { + const auto pendingApplicationURLs = this->pendingApplicationURLs; + this->pendingApplicationURLs.clear(); + for (const auto& url : pendingApplicationURLs) { + Window::handleApplicationURL(url); + } + } + } + + bool WindowManager::ManagedWindow::emit (const String& event, const JSON::Any& json) { + return this->bridge.emit(event, json); + } + + void WindowManager::ManagedWindow::setBackgroundColor (int r, int g, int b, float a) { + this->backgroundColor = Color(r, g, b, a); + Window::setBackgroundColor(r, g, b, a); + } + + void WindowManager::ManagedWindow::setBackgroundColor (const String& rgba) { + this->backgroundColor = Color(rgba); + Window::setBackgroundColor(rgba); + } + + void WindowManager::ManagedWindow::setBackgroundColor (const Color& color) { + this->backgroundColor = color; + Window::setBackgroundColor(color.str()); + } + + String WindowManager::ManagedWindow::getBackgroundColor () { + return this->backgroundColor.str(); + } +} diff --git a/src/window/manager.kt b/src/window/manager.kt new file mode 100644 index 0000000000..4c1d8d215f --- /dev/null +++ b/src/window/manager.kt @@ -0,0 +1,402 @@ +// vim: set sw=2: +package socket.runtime.window + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.os.Bundle +import android.view.WindowManager + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.commit +import androidx.fragment.app.FragmentManager + +import socket.runtime.app.App +import socket.runtime.core.console +import socket.runtime.window.WindowFragment +import socket.runtime.window.WindowOptions + +import __BUNDLE_IDENTIFIER__.R + +/** + * A `WindowFragmentManager` manages `WindowFragment` instances. + */ +open class WindowFragmentManager ( + protected val activity: WindowManagerActivity +) { + open val fragments = mutableListOf<WindowFragment>() + open val manager = activity.supportFragmentManager + + /** + * Creates a new `WindowFragment` from `WindowOptions` if `options.index` + * does not exist, otherwise this function is a "no-op". + */ + fun createWindowFragment (options: WindowOptions) { + val manager = this.manager + if (!this.hasWindowFragment(options.index)) { + val fragment = WindowFragment.newInstance(options) + this.fragments.add(fragment) + this.activity.runOnUiThread { + manager.commit { + // .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right) + setReorderingAllowed(true) + add(R.id.window, fragment) + if (options.headless) { + hide(fragment) + } else { + addToBackStack("window#${options.index}") + } + } + } + } + } + + /** + * Shows a `WindowFragment` for a given `index`. + * This function returns `true` if the `WindowFragment` exists and was shown, + * otherwise `false`. + */ + fun showWindowFragment (index: Int): Boolean { + val fragment = this.fragments.find { it.index == index } + + if (fragment != null) { + this.activity.runOnUiThread { + this.manager.beginTransaction() + // .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right) + .show(fragment) + .commit() + } + return true + } + + return false + } + + /** + * Hides a `WindowFragment` for a given `index`. + * This function returns `true` if the `WindowFragment` exists and was hidden, + * otherwise `false`. + */ + fun hideWindowFragment (index: Int): Boolean { + val fragment = this.fragments.find { it.index == index } + + if (fragment != null) { + this.activity.runOnUiThread { + this.manager.beginTransaction() + // .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right) + .hide(fragment) + .commit() + } + return true + } + + return false + } + + /** + * Closes a `WindowFragment` for a given `index`. + * This function returns `true` if the `WindowFragment` exists and was hidden, + * otherwise `false`. + */ + fun closeWindowFragment (index: Int): Boolean { + val fragment = this.fragments.find { it.index == index } + + if (fragment != null) { + this.fragments.remove(fragment) + this.activity.runOnUiThread { + this.manager.beginTransaction() + // .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right) + .remove(fragment) + .commit() + } + return true + } + + return false + } + + /** + * Returns `true` if there is a `WindowFragment` for the given `index`, + * otherwise `false`. + */ + fun hasWindowFragment (index: Int): Boolean { + return this.fragments.find { it.index == index } != null + } + + /** + * Gets a `WindowFragment` at `index` if one exists, otherwise + * `null` is returned. + */ + fun getWindowFragment (index: Int): WindowFragment? { + return this.fragments.find { it.index == index } + } + + /** + * "Pops" a `WindowFragment` from the back stack, navigating to the + * previous `WindowFragment`. + * This function returns `true` if a `WindowFragment` was "popped" + * from the back stack, otherwise `false`. + */ + fun popWindowFragment (): Boolean { + if (this.manager.backStackEntryCount > 0) { + this.manager.popBackStack() + return true + } + + this.activity.finish() + return false + } + + /** + * Navigates a `WindowFragment` at a given `index` window's webview to + * a given `url` + * This function returns `true` if a `WindowFragment` did navigate to the + * given `url`, otherwise `false`. + */ + fun navigateWindowFragment (index: Int, url: String): Boolean { + val fragment = this.fragments.find { it.index == index } + val window = fragment?.window + + if (window != null) { + window.navigate(url) + return true + } + + return false + } + + /** + * Evaluates a JavaScript `source` string at a given window `index`. + */ + fun evaluateJavaScriptInWindowFragmentView (index: Int, source: String, token: String): Boolean { + val fragments = this.fragments + if (this.hasWindowFragment(index)) { + kotlin.concurrent.thread { + activity.runOnUiThread { + val fragment = fragments.find { it.index == index } + fragment?.webview?.evaluateJavascript(source, { result -> + val window = fragment.window + if (window != null) { + window.onEvaluateJavascriptResult(index, token, result) + } + }) + } + } + return true + } + + return false + } + + /** + * Gets the window fragment width. + */ + fun getWindowFragmentWidth (index: Int): Int { + val fragment = this.fragments.find { it.index == index } + + if (fragment != null) { + return fragment.webview.measuredWidth + } + + return 0 + } + + /** + * Gets the window fragment height. + */ + fun getWindowFragmentHeight (index: Int): Int { + val fragment = this.fragments.find { it.index == index } + + if (fragment != null) { + return fragment.webview.measuredHeight + } + + return 0 + } + + /** + * Sets the window fragment width and height. + */ + fun setWindowFragmentSize (index: Int, width: Int, height: Int): Boolean { + val fragment = this.fragments.find { it.index == index } ?: return false + val window = fragment.window ?: return false + window.setSize(width, height) + return true + } + + /** + * Gets the window fragment title + */ + fun getWindowFragmentTitle (index: Int): String { + val fragment = this.fragments.find { it.index == index } ?: return "" + val window = fragment.window ?: return "" + return window.title + } + + /** + * Sets the window fragment title + */ + fun setWindowFragmentTitle (index: Int, title: String): Boolean { + val fragment = this.fragments.find { it.index == index } ?: return false + val window = fragment.window ?: return false + window.title = title + return true + } + + /** + * XXX + */ + fun setWindowFragmentViewBackgroundColor (index: Int, color: Long): Boolean { + val fragment = this.fragments.find { it.index == index } ?: return false + fragment.webview.setBackgroundColor(color.toInt()) + return true + } + + /** + * XXX + */ + fun getWindowFragmentBackgroundColor (index: Int): Int { + val fragment = this.fragments.find { it.index == index } ?: return 0 + val drawable = fragment.webview.background as ColorDrawable + val color = drawable.color + return 0xFFFFFF and color + } + + /** + * XXX + */ + fun setWindowFragmentViewPosition(index: Int, x: Float, y: Float): Boolean { + val fragment = this.fragments.find { it.index == index } ?: return false + fragment.webview.setX(x) + fragment.webview.setY(y) + return true + } +} + +/** + * The activity that is responsible for managing various WindowFragment + * instances. + */ +open class WindowManagerActivity : AppCompatActivity(R.layout.window_container_view) { + open val windowFragmentManager = WindowFragmentManager(this) + open val dialog = Dialog(this) + + override fun onBackPressed () { + // this.windowFragmentManager.popWindowFragment() + } + + override fun onActivityResult ( + requestCode: Int, + resultCode: Int, + intent: Intent? + ) { + super.onActivityResult(requestCode, resultCode, intent) + } + + /** + * Creates a new window at a given `index`. + */ + fun createWindow ( + index: Int = 0, + shouldExitApplicationOnClose: Boolean = false, + headless: Boolean = false + ) { + this.windowFragmentManager.createWindowFragment(WindowOptions( + index = index, + shouldExitApplicationOnClose = shouldExitApplicationOnClose, + headless = headless + )) + } + + /** + * Shows a window at a given `index`. + */ + fun showWindow (index: Int): Boolean { + return this.windowFragmentManager.showWindowFragment(index) + } + + /** + * Hides a window at a given `index`. + */ + fun hideWindow (index: Int): Boolean { + return this.windowFragmentManager.hideWindowFragment(index) + } + + /** + * Closes a window at a given `index`. + */ + fun closeWindow (index: Int): Boolean { + return this.windowFragmentManager.closeWindowFragment(index) + } + + /** + * Navigates to a given `url` for a window at a given `index`. + */ + fun navigateWindow (index: Int, url: String): Boolean { + return this.windowFragmentManager.navigateWindowFragment(index, url) + } + + /** + * Get the measured window width at a given `index`. + */ + fun getWindowWidth (index: Int): Int { + return this.windowFragmentManager.getWindowFragmentWidth(index) + } + + /** + * Get the measured window height at a given `index`. + */ + fun getWindowHeight (index: Int): Int { + return this.windowFragmentManager.getWindowFragmentHeight(index) + } + + /** + * Sets the window `width` and `height` at a given `index`. + */ + fun setWindowFragmentSize (index: Int, width: Int, height: Int): Boolean { + return this.windowFragmentManager.setWindowFragmentSize(index, width, height) + } + + /** + * XXX + */ + fun getWindowBackgroundColor (index: Int): Int { + return this.windowFragmentManager.getWindowFragmentBackgroundColor(index) + } + + /** + * Get the window title at a given `index`. + */ + fun getWindowTitle (index: Int): String { + return this.windowFragmentManager.getWindowFragmentTitle(index) + } + + /** + * Sets the window title at a given `index`. + */ + fun setWindowTitle (index: Int, title: String): Boolean { + return this.windowFragmentManager.setWindowFragmentTitle(index, title) + } + + /** + * Sets the window background color at a given `index`. + */ + fun setWindowBackgroundColor (index: Int, color: Long): Boolean { + return this.windowFragmentManager.setWindowFragmentViewBackgroundColor(index, color) + } + + /** + * XXX + */ + fun setWindowPosition (index: Int, x: Float, y: Float): Boolean { + return this.windowFragmentManager.setWindowFragmentViewPosition(index, x, y) + } + + /** + * Evaluates JavaScript source in a window at a given `index`. + */ + fun evaluateJavaScript (index: Int, source: String, token: String): Boolean { + return this.windowFragmentManager.evaluateJavaScriptInWindowFragmentView(index, source, token) + } +} diff --git a/src/window/options.hh b/src/window/options.hh deleted file mode 100644 index 9cc96fcdcc..0000000000 --- a/src/window/options.hh +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef SSC_WINDOW_OPTIONS_H -#define SSC_WINDOW_OPTIONS_H - -#include "../core/types.hh" - -namespace SSC { - struct WindowOptions { - bool resizable = true; - bool frameless = false; - bool utility = false; - bool canExit = false; - float width = 0; - float height = 0; - float minWidth = 0; - float minHeight = 0; - float maxWidth = 0; - float maxHeight = 0; - int index = 0; - int debug = 0; - int port = 0; - bool isTest = false; - bool headless = false; - String cwd = ""; - String title = ""; - String url = "data:text/html,<html>"; - String argv = ""; - String preload = ""; - String env; - Map appData; - MessageCallback onMessage = [](const String) {}; - ExitCallback onExit = nullptr; - }; -} -#endif diff --git a/src/window/win.cc b/src/window/win.cc index a8e8351589..cee504f077 100644 --- a/src/window/win.cc +++ b/src/window/win.cc @@ -1,36 +1,17 @@ -#include <shlwapi.h> -#include <objidl.h> -#include <wrl.h> -#include <shellapi.h> -#include <fileapi.h> -#include <urlmon.h> +#include "../app/app.hh" #include "window.hh" - -#include "WebView2.h" -#include "WebView2EnvironmentOptions.h" - -#pragma comment(lib, "Shlwapi.lib") -#pragma comment(lib, "urlmon.lib") - -#ifndef CHECK_FAILURE -#define CHECK_FAILURE(...) -#endif +#include <winuser.h> using namespace Microsoft::WRL; -namespace SSC { - static inline void alert (const SSC::WString &ws) { - MessageBoxA(nullptr, SSC::convertWStringToString(ws).c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); - } - - static inline void alert (const SSC::String &s) { - MessageBoxA(nullptr, s.c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); - } - - static inline void alert (const char* s) { - MessageBoxA(nullptr, s, _TEXT("Alert"), MB_OK | MB_ICONSTOP); - } +extern BOOL ChangeWindowMessageFilterEx ( + HWND hwnd, + UINT message, + DWORD action, + void* unused +); +namespace SSC { class CDataObject : public IDataObject { public: HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject); @@ -154,7 +135,7 @@ namespace SSC { } HRESULT __stdcall CDataObject::GetCanonicalFormatEtc (FORMATETC *pFormatEct, FORMATETC *pFormatEtcOut) { - pFormatEtcOut->ptd = NULL; + pFormatEtcOut->ptd = nullptr; return E_NOTIMPL; } @@ -267,7 +248,7 @@ namespace SSC { } class DragDrop : public IDropTarget { - SSC::Vector<SSC::String> draggablePayload; + Vector<String> draggablePayload; unsigned int refCount; public: @@ -289,7 +270,7 @@ namespace SSC { AddRef(); return S_OK; } - *ppv = NULL; + *ppv = nullptr; return E_NOINTERFACE; }; @@ -322,7 +303,7 @@ namespace SSC { *dragEffect = DROPEFFECT_MOVE; format.cfFormat = CF_TEXT; - format.ptd = NULL; + format.ptd = nullptr; format.dwAspect = DVASPECT_CONTENT; format.lindex = -1; format.tymed = TYMED_HGLOBAL; @@ -335,10 +316,10 @@ namespace SSC { this->draggablePayload.clear(); if (list != 0) { - draggablePayload = SSC::split(SSC::String(list), ';'); + draggablePayload = split(String(list), ';'); - SSC::String json = ( + String json = ( "{" " \"count\":" + std::to_string(this->draggablePayload.size()) + "," " \"inbound\": true," @@ -347,7 +328,7 @@ namespace SSC { "}" ); - auto payload = SSC::getEmitToRenderProcessJavaScript("drag", json); + auto payload = getEmitToRenderProcessJavaScript("drag", json); this->window->eval(payload); } @@ -393,7 +374,7 @@ namespace SSC { point.x = dragPoint.x - position.x; point.y = dragPoint.y - position.y; - SSC::String json = ( + String json = ( "{" " \"count\":" + std::to_string(this->draggablePayload.size()) + "," " \"inbound\": false," @@ -402,7 +383,7 @@ namespace SSC { "}" ); - auto payload = SSC::getEmitToRenderProcessJavaScript("drag", json); + auto payload = getEmitToRenderProcessJavaScript("drag", json); this->window->eval(payload); return S_OK; @@ -423,7 +404,7 @@ namespace SSC { STGMEDIUM medium = { TYMED_HGLOBAL, { 0 }, 0 }; UINT len = 0; - SSC::Vector<SSC::String> files = this->draggablePayload; + Vector<String> files = this->draggablePayload; for (auto &file : files) { file = file.substr(12); @@ -433,14 +414,14 @@ namespace SSC { globalMemory = GlobalAlloc(GHND, sizeof(DROPFILES) + len + 1); if (!globalMemory) { - return NULL; + return E_POINTER; } dropFiles = (DROPFILES*) GlobalLock(globalMemory); if (!dropFiles) { GlobalFree(globalMemory); - return NULL; + return E_POINTER; } dropFiles->fNC = TRUE; @@ -449,8 +430,8 @@ namespace SSC { GetCursorPos(&(dropFiles->pt)); char *dropFilePtr = (char *) &dropFiles[1]; - for (SSC::Vector<SSC::String>::size_type i = 0; i < files.size(); ++i) { - SSC::String &file = files[i]; + for (Vector<String>::size_type i = 0; i < files.size(); ++i) { + String &file = files[i]; len = (file.length() + 1); @@ -519,7 +500,7 @@ namespace SSC { HDROP drop; int count; - SSC::StringStream filesStringArray; + StringStream filesStringArray; GetClientRect(child, &rect); position = { rect.left, rect.top }; @@ -532,7 +513,7 @@ namespace SSC { format.cfFormat = CF_HDROP; format.lindex = -1; format.tymed = TYMED_HGLOBAL; - format.ptd = NULL; + format.ptd = nullptr; if ( SUCCEEDED(dataObject->QueryGetData(&format)) && @@ -541,10 +522,10 @@ namespace SSC { *dragEffect = DROPEFFECT_COPY; drop = (HDROP) GlobalLock(medium.hGlobal); - count = DragQueryFile(drop, 0xFFFFFFFF, NULL, 0); + count = DragQueryFile(drop, 0xFFFFFFFF, nullptr, 0); for (int i = 0; i < count; i++) { - int size = DragQueryFile(drop, i, NULL, 0); + int size = DragQueryFile(drop, i, nullptr, 0); TCHAR* buf = new TCHAR[size + 1]; DragQueryFile(drop, i, buf, size + 1); @@ -552,7 +533,7 @@ namespace SSC { // append escaped file path with wrapped quotes ('"') filesStringArray << '"' - << SSC::replace(SSC::String(buf), "\\\\", "\\\\") + << replace(String(buf), "\\\\", "\\\\") << '"'; if (i < count - 1) { @@ -583,7 +564,7 @@ namespace SSC { ) { *dragEffect = DROPEFFECT_MOVE; for (auto &src : this->draggablePayload) { - SSC::String json = ( + String json = ( "{" " \"src\": \"" + src + "\"," " \"x\":" + std::to_string(point.x) + "," @@ -591,17 +572,17 @@ namespace SSC { "}" ); - this->window->eval(SSC::getEmitToRenderProcessJavaScript("drop", json)); + this->window->eval(getEmitToRenderProcessJavaScript("drop", json)); } - SSC::String json = ( + String json = ( "{" " \"x\":" + std::to_string(point.x) + "," " \"y\":" + std::to_string(point.y) + "" "}" ); - this->window->eval(SSC::getEmitToRenderProcessJavaScript("dragend", json)); + this->window->eval(getEmitToRenderProcessJavaScript("dragend", json)); } } @@ -614,887 +595,617 @@ namespace SSC { }; }; - Window::Window (App& app, WindowOptions opts) - : app(app), - opts(opts), - hotkey(this) + Window::Window (SharedPointer<Core> core, const Window::Options& options) + : core(core), + options(options), + bridge(core, IPC::Bridge::Options { + options.userConfig, + options.as<IPC::Preload::Options>() + }), + hotkey(this), + dialog(this) { - static auto userConfig = SSC::getUserConfig(); - const bool isAgent = userConfig["application_agent"] == "true" && opts.index == 0; - app.isReady = false; + // this may be an "empty" path if not available + static const auto edgeRuntimePath = FileResource::getMicrosoftEdgeRuntimePath(); + static auto app = App::sharedApplication(); + app->isReady = false; + + if (!edgeRuntimePath.empty()) { + const auto string = convertWStringToString(edgeRuntimePath.string()); + const auto value = replace(string, "\\\\", "\\\\"); + // inject the `EDGE_RUNTIME_DIRECTORY` environment variable directly into + // the userConfig so it is available as an env var in the webview runtime + this->bridge.userConfig["env_EDGE_RUNTIME_DIRECTORY"] = value; + debug("Microsoft Edge Runtime directory set to '%ls'", edgeRuntimePath.c_str()); + } - this->index = opts.index; - if (isAgent && opts.index == 0) { - window = CreateWindowEx( + auto userConfig = this->bridge.userConfig; + // only the root window can handle "agent" tasks + const bool isAgent = ( + userConfig["application_agent"] == "true" && + options.index == 0 + ); + + if (isAgent) { + this->window = CreateWindowEx( WS_EX_TOOLWINDOW, userConfig["meta_bundle_identifier"].c_str(), userConfig["meta_title"].c_str(), WS_OVERLAPPEDWINDOW, 100000, 100000, - opts.width, - opts.height, - NULL, - NULL, - app.hInstance, - NULL + options.width, + options.height, + nullptr, + nullptr, + app->hInstance, + nullptr ); } else { - window = CreateWindowEx( - opts.headless + DWORD style = WS_THICKFRAME; + + if (options.frameless) { + style |= WS_POPUP; + } else { + style |= WS_OVERLAPPED; + // Windows does not have the ability to reposition the decorations + // In this case, we can assume that the user will draw their own controls. + if (options.titlebarStyle != "hidden" && options.titlebarStyle != "hiddenInset") { + if (options.closable) { + style |= WS_CAPTION | WS_SYSMENU; + + if (options.minimizable) { + style |= WS_MINIMIZEBOX; + } + + if (options.maximizable) { + style |= WS_MAXIMIZEBOX; + } + } + } + } + + this->window = CreateWindowEx( + options.headless ? WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE : WS_EX_APPWINDOW | WS_EX_ACCEPTFILES, userConfig["meta_bundle_identifier"].c_str(), userConfig["meta_title"].c_str(), - WS_OVERLAPPEDWINDOW, + style, 100000, 100000, - opts.width, - opts.height, - NULL, - NULL, - app.hInstance, - NULL + options.width, + options.height, + nullptr, + nullptr, + app->hInstance, + nullptr ); } - HRESULT initResult = OleInitialize(NULL); - - this->drop = new DragDrop(this); + auto webviewEnvironmentOptions = Microsoft::WRL::Make<CoreWebView2EnvironmentOptions>(); + webviewEnvironmentOptions->put_AdditionalBrowserArguments(L"--enable-features=msWebView2EnableDraggableRegions"); - this->bridge = new IPC::Bridge(app.core); - this->hotkey.init(this->bridge); - this->bridge->router.dispatchFunction = [&app] (auto callback) { - app.dispatch([callback] { callback(); }); + this->drop = std::make_shared<DragDrop>(this); + this->bridge.navigateFunction = [this] (const auto url) { + this->navigate(url); }; - this->bridge->router.evaluateJavaScriptFunction = [this] (auto js) { - this->eval(js); + this->bridge.evaluateJavaScriptFunction = [this] (const auto source) { + this->eval(source); }; - // - // In theory these allow you to do drop files in elevated mode - // - ChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD); - ChangeWindowMessageFilter(WM_COPYDATA, MSGFLT_ADD); - ChangeWindowMessageFilter(0x0049, MSGFLT_ADD); + this->bridge.client.preload = IPC::Preload::compile({ + .client = UniqueClient { + .id = this->bridge.client.id, + .index = this->bridge.client.index + }, + .index = options.index, + .conduit = this->core->conduit.port, + .userScript = options.userScript + }); - UpdateWindow(window); - ShowWindow(window, isAgent ? SW_HIDE : SW_SHOWNORMAL); - SetWindowLongPtr(window, GWLP_USERDATA, (LONG_PTR) this); + if (options.aspectRatio.size() > 0) { + auto parts = split(options.aspectRatio, ':'); + double aspectRatio = 0; - // this is something like "C:\\Users\\josep\\AppData\\Local\\Microsoft\\Edge SxS\\Application\\123.0.2386.0" - auto EDGE_RUNTIME_DIRECTORY = convertStringToWString(trim(Env::get("SOCKET_EDGE_RUNTIME_DIRECTORY"))); + try { + aspectRatio = std::stof(trim(parts[0])) / std::stof(trim(parts[1])); + } catch (...) { + debug("invalid aspect ratio"); + } - if (EDGE_RUNTIME_DIRECTORY.size() > 0 && fs::exists(EDGE_RUNTIME_DIRECTORY)) { - usingCustomEdgeRuntimeDirectory = true; - opts.appData["env_EDGE_RUNTIME_DIRECTORY"] = replace(convertWStringToString(EDGE_RUNTIME_DIRECTORY), "\\\\", "\\\\"); - debug("Using Edge Runtime Directory: %ls", EDGE_RUNTIME_DIRECTORY.c_str()); - } else { - EDGE_RUNTIME_DIRECTORY = L""; + if (aspectRatio > 0) { + RECT rect; + GetClientRect(this->window, &rect); + // SetWindowAspectRatio(window, MAKELONG((long)(rect.bottom * aspectRatio), rect.bottom), nullptr); + } } - wchar_t modulefile[MAX_PATH]; - GetModuleFileNameW(NULL, modulefile, MAX_PATH); - auto file = (fs::path { modulefile }).filename(); - auto filename = SSC::convertStringToWString(file.string()); - auto path = SSC::convertStringToWString(Env::get("APPDATA")); - this->modulePath = fs::path(modulefile); + // in theory these allow you to do drop files in elevated mode + // FIXME(@jwerle): `ChangeWindowMessageFilter` will be deprecated and potentially removed + // but `ChangeWindowMessageFilterEx` doesn't seem to be available for linkage + ChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD); + ChangeWindowMessageFilter(WM_COPYDATA, MSGFLT_ADD); + ChangeWindowMessageFilter(0x0049, MSGFLT_ADD); + //ChangeWindowMessageFilterEx(this->window, WM_DROPFILES, /* MSGFLT_ALLOW */ 1, nullptr); + //ChangeWindowMessageFilterEx(this->window, WM_COPYDATA, /* MSGFLT_ALLOW*/ 1, nullptr); + //ChangeWindowMessageFilterEx(this->window, 0x0049, /* MSGFLT_ALLOW */ 1, nullptr); + + UpdateWindow(this->window); + ShowWindow(this->window, isAgent ? SW_HIDE : SW_SHOWNORMAL); - auto options = Microsoft::WRL::Make<CoreWebView2EnvironmentOptions>(); - options->put_AdditionalBrowserArguments(L"--allow-file-access-from-files"); + // make this `Window` instance as `GWLP_USERDATA` + SetWindowLongPtr(this->window, GWLP_USERDATA, (LONG_PTR) this); - Microsoft::WRL::ComPtr<ICoreWebView2EnvironmentOptions4> options4; - HRESULT oeResult = options.As(&options4); - if (oeResult != S_OK) { - // UNREACHABLE - cannot continue - } + this->hotkey.init(); + this->bridge.init(); - const int MAX_ALLOWED_SCHEME_ORIGINS = 5; - int allowedSchemeOriginsCount = 4; - const WCHAR* allowedSchemeOrigins[MAX_ALLOWED_SCHEME_ORIGINS] = { - L"about://*", - L"https://*", - L"file://*", - L"socket://*" - }; + static const auto APPDATA = Path(convertStringToWString(Env::get("APPDATA"))); - static const auto devHost = SSC::getDevHost(); - if (devHost.starts_with("http:")) { - allowedSchemeOrigins[allowedSchemeOriginsCount++] = convertStringToWString(devHost).c_str(); + if (APPDATA.empty() || !fs::exists(APPDATA)) { + throw std::runtime_error( + "Environment is in an invalid state: Could not determine 'APPDATA' path" + ); } - auto ipcSchemeRegistration = Microsoft::WRL::Make<CoreWebView2CustomSchemeRegistration>(L"ipc"); - ipcSchemeRegistration->put_HasAuthorityComponent(TRUE); - ipcSchemeRegistration->put_TreatAsSecure(TRUE); - ipcSchemeRegistration->SetAllowedOrigins(allowedSchemeOriginsCount, allowedSchemeOrigins); - - auto socketSchemeRegistration = Microsoft::WRL::Make<CoreWebView2CustomSchemeRegistration>(L"socket"); - socketSchemeRegistration->put_HasAuthorityComponent(TRUE); - socketSchemeRegistration->put_TreatAsSecure(TRUE); - socketSchemeRegistration->SetAllowedOrigins(allowedSchemeOriginsCount, allowedSchemeOrigins); - - // If someone can figure out how to allocate this so we can do it in a loop that'd be great, but even Ms is doing it like this: - // https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environmentoptions4?view=webview2-1.0.1587.40 - ICoreWebView2CustomSchemeRegistration* registrations[2] = { - ipcSchemeRegistration.Get(), - socketSchemeRegistration.Get() - }; + // XXX(@jwerle): is this path correct? maybe we should use the bundle identifier here + // instead of the executable filename + static const auto edgeRuntimeUserDataPath = ({ + wchar_t modulefile[MAX_PATH]; + GetModuleFileNameW(nullptr, modulefile, MAX_PATH); + auto file = (fs::path { modulefile }).filename(); + auto filename = convertStringToWString(file.string()); + APPDATA / filename; + }); - options4->SetCustomSchemeRegistrations(2, static_cast<ICoreWebView2CustomSchemeRegistration**>(registrations)); - - auto init = [&, opts]() -> HRESULT { - return CreateCoreWebView2EnvironmentWithOptions( - EDGE_RUNTIME_DIRECTORY.size() > 0 ? EDGE_RUNTIME_DIRECTORY.c_str() : nullptr, - (path + L"/" + filename).c_str(), - options.Get(), - Microsoft::WRL::Callback<IEnvHandler>( - [&, opts](HRESULT result, ICoreWebView2Environment* env) -> HRESULT { - env->CreateCoreWebView2Controller( - window, - Microsoft::WRL::Callback<IConHandler>( - [&, opts](HRESULT result, ICoreWebView2Controller* c) -> HRESULT { - static auto userConfig = SSC::getUserConfig(); - if (c != nullptr) { - controller = c; - controller->get_CoreWebView2(&webview); - - RECT bounds; - GetClientRect(window, &bounds); - controller->put_Bounds(bounds); - controller->AddRef(); - controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); - } + this->bridge.configureSchemeHandlers({ + .webview = webviewEnvironmentOptions + }); - ICoreWebView2Settings* Settings; - webview->get_Settings(&Settings); - Settings->put_IsScriptEnabled(TRUE); - Settings->put_AreDefaultScriptDialogsEnabled(TRUE); - Settings->put_IsWebMessageEnabled(TRUE); - Settings->put_AreHostObjectsAllowed(TRUE); - Settings->put_IsStatusBarEnabled(FALSE); - - Settings->put_AreDefaultContextMenusEnabled(TRUE); - if (isDebugEnabled()) { - Settings->put_AreDevToolsEnabled(TRUE); - } else { - Settings->put_AreDevToolsEnabled(FALSE); - } + CreateCoreWebView2EnvironmentWithOptions( + edgeRuntimePath.empty() ? nullptr : convertStringToWString(edgeRuntimePath.string()).c_str(), + convertStringToWString(edgeRuntimeUserDataPath.string()).c_str(), + webviewEnvironmentOptions.Get(), + Microsoft::WRL::Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>([=, this]( + HRESULT result, + ICoreWebView2Environment* env + ) mutable { + return env->CreateCoreWebView2Controller( + this->window, + Microsoft::WRL::Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>([=, this]( + HRESULT result, + ICoreWebView2Controller* controller + ) mutable { + const auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + + if (result != S_OK) { + return result; + } - Settings->put_IsBuiltInErrorPageEnabled(FALSE); - Settings->put_IsZoomControlEnabled(FALSE); + if (controller == nullptr) { + return E_HANDLE; + } - auto settings3 = (ICoreWebView2Settings3*) Settings; - if (!isDebugEnabled()) { - settings3->put_AreBrowserAcceleratorKeysEnabled(FALSE); + // configure the webview controller + do { + RECT bounds; + GetClientRect(this->window, &bounds); + this->controller = controller; + this->controller->get_CoreWebView2(&this->webview); + this->controller->put_Bounds(bounds); + this->controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); + this->controller->AddRef(); + this->bridge.configureWebView(this->webview); + } while (0); + + // configure the webview settings + do { + ICoreWebView2Settings* settings = nullptr; + ICoreWebView2Settings2* settings2 = nullptr; + ICoreWebView2Settings3* settings3 = nullptr; + ICoreWebView2Settings6* settings6 = nullptr; + ICoreWebView2Settings9* settings9 = nullptr; + + const auto wantsDebugMode = this->options.debug || isDebugEnabled(); + + this->webview->get_Settings(&settings); + + settings2 = reinterpret_cast<ICoreWebView2Settings2*>(settings); + settings3 = reinterpret_cast<ICoreWebView2Settings3*>(settings); + settings6 = reinterpret_cast<ICoreWebView2Settings6*>(settings); + settings9 = reinterpret_cast<ICoreWebView2Settings9*>(settings); + + settings->put_IsScriptEnabled(true); + settings->put_IsStatusBarEnabled(false); + settings->put_IsWebMessageEnabled(true); + settings->put_AreDevToolsEnabled(wantsDebugMode); + settings->put_AreHostObjectsAllowed(true); + settings->put_IsZoomControlEnabled(false); + settings->put_IsBuiltInErrorPageEnabled(false); + settings->put_AreDefaultContextMenusEnabled(true); + settings->put_AreDefaultScriptDialogsEnabled(true); + + // TODO(@jwerle): set user agent for runtime + // settings2->put_UserAgent(); + + settings3->put_AreBrowserAcceleratorKeysEnabled(wantsDebugMode); + + settings6->put_IsPinchZoomEnabled(false); + settings6->put_IsSwipeNavigationEnabled( + this->bridge.userConfig["webview_navigator_enable_navigation_destures"] == "true" + ); + + settings9->put_IsNonClientRegionSupportEnabled(true); + } while (0); + + // enumerate all child windows to re-register drag/drop + EnumChildWindows( + this->window, + [](HWND handle, LPARAM param) -> BOOL { + const auto length = GetWindowTextLengthW(handle); + const auto pointer = GetWindowLongPtr(reinterpret_cast<HWND>(param), GWLP_USERDATA); + auto window = reinterpret_cast<Window*>(pointer); + + if (length > 0) { + auto buffer = std::make_shared<wchar_t[]>(length + 1); + auto text = convertWStringToString(buffer.get()); + GetWindowTextW(handle, buffer.get(), length + 1); + + if (text.find("Chrome") != String::npos) { + RevokeDragDrop(handle); + RegisterDragDrop(handle, window->drop.get()); + window->drop->childWindow = handle; } + } - auto settings6 = (ICoreWebView2Settings6*) Settings; - settings6->put_IsPinchZoomEnabled(FALSE); - settings6->put_IsSwipeNavigationEnabled(FALSE); - - EnumChildWindows(window, [](HWND hWnd, LPARAM window) -> BOOL { - int l = GetWindowTextLengthW(hWnd); - - if (l > 0) { - wchar_t* buf = new wchar_t[l+1]; - GetWindowTextW(hWnd, buf, l+1); + return true; + }, + reinterpret_cast<LPARAM>(this->window) + ); - if (SSC::convertWStringToString(buf).find("Chrome") != SSC::String::npos) { - RevokeDragDrop(hWnd); - Window* w = reinterpret_cast<Window*>(GetWindowLongPtr((HWND)window, GWLP_USERDATA)); - w->drop->childWindow = hWnd; - RegisterDragDrop(hWnd, w->drop); - } + // configure webview + do { + ICoreWebView2_22* webview22 = nullptr; + ICoreWebView2_3* webview3 = reinterpret_cast<ICoreWebView2_3*>(this->webview); + + this->webview->QueryInterface(IID_PPV_ARGS(&webview22)); + this->webview->AddWebResourceRequestedFilter(L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); + + if (webview22 != nullptr) { + this->bridge.userConfig["env_COREWEBVIEW2_22_AVAILABLE"] = "true"; + + webview22->AddWebResourceRequestedFilterWithRequestSourceKinds( + L"*", + COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, + COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL + ); + } + + webview3->SetVirtualHostNameToFolderMapping( + convertStringToWString(bundleIdentifier).c_str(), + FileResource::getResourcesPath().c_str(), + COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW + ); + } while (0); + + // configure the user script preload + do { + auto preloadUserScriptSource = IPC::Preload::compile({ + .features = IPC::Preload::Options::Features { + .useGlobalCommonJS = false, + .useGlobalNodeJS = false, + .useTestScript = false, + .useHTMLMarkup = false, + .useESM = false, + .useGlobalArgs = true + }, + .client = UniqueClient { + .id = this->bridge.client.id, + .index = this->bridge.client.index + }, + .index = this->options.index, + .conduit = this->core->conduit.port, + .userScript = this->options.userScript + }); + + this->webview->AddScriptToExecuteOnDocumentCreated( + // Note that this may not do anything as preload goes out of scope before event fires + // Consider using w->preloadJavascript, but apps work without this + convertStringToWString(preloadUserScriptSource.str()).c_str(), + Microsoft::WRL::Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>( + [&](HRESULT error, PCWSTR id) -> HRESULT { return S_OK; } + ).Get() + ); + } while (0); + + // configure webview permission request handler + do { + EventRegistrationToken token; + this->webview->add_PermissionRequested( + Microsoft::WRL::Callback<ICoreWebView2PermissionRequestedEventHandler>([=, this]( + ICoreWebView2 *_, + ICoreWebView2PermissionRequestedEventArgs *args + ) mutable { + COREWEBVIEW2_PERMISSION_KIND kind; + args->get_PermissionKind(&kind); + + if (kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE) { + if ( + userConfig["permissions_allow_microphone"] == "false" || + userConfig["permissions_allow_user_media"] == "false" + ) { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); } - return TRUE; - }, (LPARAM)window); - - reinterpret_cast<ICoreWebView2_3*>(webview)->SetVirtualHostNameToFolderMapping( - convertStringToWString(userConfig["meta_bundle_identifier"]).c_str(), - this->modulePath.parent_path().c_str(), - COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW - ); - - EventRegistrationToken tokenNavigation; - - webview->add_NavigationStarting( - Microsoft::WRL::Callback<ICoreWebView2NavigationStartingEventHandler>( - [&](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs *e) { - static const auto devHost = SSC::getDevHost(); - - PWSTR uri; - e->get_Uri(&uri); - SSC::String url(SSC::convertWStringToString(uri)); - - if (url.starts_with(userConfig["meta_application_protocol"])) { - e->put_Cancel(true); - Window* w = reinterpret_cast<Window*>(GetWindowLongPtr((HWND)window, GWLP_USERDATA)); - if (w != nullptr) { - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ - "url", url - }}; - w->bridge->router.emit("applicationurl", json.str()); - } - } else if (url.find("socket:") != 0 && url.find("file://") != 0 && url.find(devHost) != 0) { - e->put_Cancel(true); - } - - CoTaskMemFree(uri); - return S_OK; - } - ).Get(), - &tokenNavigation - ); - - EventRegistrationToken tokenSchemaFilter; - webview->AddWebResourceRequestedFilter(L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); - webview->AddWebResourceRequestedFilter(L"socket:*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); - webview->AddWebResourceRequestedFilter(L"socket:*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_XML_HTTP_REQUEST); - - ICoreWebView2_22* webview22 = nullptr; - webview->QueryInterface(IID_PPV_ARGS(&webview22)); - - if (webview22 != nullptr) { - webview22->AddWebResourceRequestedFilterWithRequestSourceKinds( - L"*", - COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, - COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL - ); - - debug("Configured CoreWebView2 (ICoreWebView2_22) request filter with all request source kinds"); - } - - webview->add_WebResourceRequested( - Microsoft::WRL::Callback<ICoreWebView2WebResourceRequestedEventHandler>( - [&, opts](ICoreWebView2*, ICoreWebView2WebResourceRequestedEventArgs* args) { - static auto userConfig = SSC::getUserConfig(); - static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; - - Window* w = reinterpret_cast<Window*>(GetWindowLongPtr((HWND)window, GWLP_USERDATA)); - - ICoreWebView2WebResourceRequest* req = nullptr; - ICoreWebView2Environment* env = nullptr; - ICoreWebView2_2* webview2 = nullptr; - - LPWSTR req_uri; - LPWSTR req_method; - - String method; - String uri; + } - webview->QueryInterface(IID_PPV_ARGS(&webview2)); - webview2->get_Environment(&env); - args->get_Request(&req); + if (kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA) { + if ( + userConfig["permissions_allow_camera"] == "false" || + userConfig["permissions_allow_user_media"] == "false" + ) { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } - req->get_Uri(&req_uri); - uri = convertWStringToString(req_uri); - CoTaskMemFree(req_uri); + if (kind == COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION) { + if (userConfig["permissions_allow_geolocation"] == "false") { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } - req->get_Method(&req_method); - method = convertWStringToString(req_method); - CoTaskMemFree(req_method); + if (kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS) { + if (userConfig["permissions_allow_notifications"] == "false") { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } - bool ipc_scheme = false; - bool socket_scheme = false; - bool handled = false; + if (kind == COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS) { + if (userConfig["permissions_allow_sensors"] == "false") { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } - if (uri.compare(0, 4, "ipc:") == 0) { - ipc_scheme = true; - } else if (uri.compare(0, 7, "socket:") == 0) { - socket_scheme = true; - } else { - return S_OK; - } + if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) { + if (userConfig["permissions_allow_clipboard"] == "false") { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } - // Handle CORS preflight request. - if (method.compare("OPTIONS") == 0) { - ICoreWebView2WebResourceResponse* res = nullptr; - env->CreateWebResourceResponse( - nullptr, - 204, - L"OK", - L"Connection: keep-alive\n" - L"Access-Control-Allow-Headers: *\n" - L"Access-Control-Allow-Origin: *\n" - L"Access-Control-Allow-Methods: GET, POST, PUT, HEAD\n", - &res - ); - args->put_Response(res); - - return S_OK; - } + if (kind == COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY) { + if (userConfig["permissions_allow_autoplay"] == "false") { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } + if (kind == COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS) { + if (userConfig["permissions_allow_local_fonts"] == "false") { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } else { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } - ICoreWebView2Deferral* deferral; - HRESULT hr = args->GetDeferral(&deferral); - - char* body_ptr = nullptr; - size_t body_length = 0; - - if (ipc_scheme) { - if (method.compare("POST") == 0 || method.compare("PUT") == 0) { - IStream* body_data; - DWORD actual; - HRESULT r; - auto msg = IPC::Message(uri); - msg.isHTTP = true; - // TODO(heapwolf): Make sure index and seq are set. - if (w->bridge->router.hasMappedBuffer(msg.index, msg.seq)) { - IPC::MessageBuffer buf = w->bridge->router.getMappedBuffer(msg.index, msg.seq); - ICoreWebView2SharedBuffer* shared_buf = buf.shared_buf; - size_t size = buf.size; - char* data = new char[size]; - w->bridge->router.removeMappedBuffer(msg.index, msg.seq); - shared_buf->OpenStream(&body_data); - r = body_data->Read(data, size, &actual); - if (r == S_OK || r == S_FALSE) { - body_ptr = data; - body_length = actual; - } else { - delete[] data; - } - shared_buf->Close(); - } - } - - handled = w->bridge->route(uri, body_ptr, body_length, [&, args, deferral, env, body_ptr](auto result) { - String headers; - char* body; - size_t length; - - if (body_ptr != nullptr) { - delete[] body_ptr; - } - - if (result.post.body != nullptr) { - length = result.post.length; - body = new char[length]; - memcpy(body, result.post.body, length); - headers = "Content-Type: application/octet-stream\n"; - } else { - length = result.str().size(); - body = new char[length]; - memcpy(body, result.str().c_str(), length); - headers = "Content-Type: application/json\n"; - } - - headers += "Connection: keep-alive\n"; - headers += "Access-Control-Allow-Headers: *\n"; - headers += "Access-Control-Allow-Origin: *\n"; - headers += "Content-Length: "; - headers += std::to_string(length); - headers += "\n"; - - // Completing the response in the call to dispatch because the - // put_Response() must be called from the same thread that made - // the request. This assumes that the request was made from the - // main thread, since that's where dispatch() will call its cb. - app.dispatch([&, body, length, headers, args, deferral, env] { - ICoreWebView2WebResourceResponse* res = nullptr; - IStream* bytes = SHCreateMemStream((const BYTE*)body, length); - env->CreateWebResourceResponse( - bytes, - 200, - L"OK", - convertStringToWString(headers).c_str(), - &res - ); - args->put_Response(res); - deferral->Complete(); - delete[] body; - }); - }); - } + return S_OK; + }).Get(), + &token + ); + } while (0); + + // configure webview callback for `window.open()` + do { + EventRegistrationToken token; + this->webview->add_NewWindowRequested( + Microsoft::WRL::Callback<ICoreWebView2NewWindowRequestedEventHandler>( + [&](ICoreWebView2* webview, ICoreWebView2NewWindowRequestedEventArgs* args) { + // TODO(@jwerle): handle 'window.open()' + args->put_Handled(true); + return S_OK; + } + ).Get(), + &token + ); + } while (0); + + // configure webview message handler + do { + EventRegistrationToken token; + this->webview->add_WebMessageReceived( + Microsoft::WRL::Callback<ICoreWebView2WebMessageReceivedEventHandler>([=, this]( + ICoreWebView2* webview, + ICoreWebView2WebMessageReceivedEventArgs* args + ) -> HRESULT { + if (this->onMessage == nullptr) { + return S_OK; + } - if (socket_scheme) { - if (method.compare("GET") == 0 || method.compare("HEAD") == 0) { - if (uri.starts_with("socket:///")) { - uri = uri.substr(10); - } else if (uri.starts_with("socket://")) { - uri = uri.substr(9); - } else if (uri.starts_with("socket:")) { - uri = uri.substr(7); - } - - auto path = String( - uri.starts_with(bundleIdentifier) - ? uri.substr(bundleIdentifier.size()) - : "socket/" + uri - ); - - const auto parts = split(path, '?'); - const auto query = parts.size() > 1 ? String("?") + parts[1] : ""; - path = parts[0]; - - auto ext = fs::path(path).extension().string(); - - if (ext.size() > 0 && !ext.starts_with(".")) { - ext = "." + ext; - } - - if (!uri.starts_with(bundleIdentifier)) { - if (path.ends_with("/")) { - path = path.substr(0, path.size() - 1); - } - - if (ext.size() == 0 && !path.ends_with(".js")) { - path += ".js"; - } - - if (path == "/") { - uri = "socket://" + bundleIdentifier + "/"; - } else { - uri = "socket://" + bundleIdentifier + "/" + path; - } - - String headers; - - auto moduleUri = replace(uri, "\\\\", "/"); - auto moduleSource = trim(tmpl( - moduleTemplate, - Map { {"url", String(moduleUri)} } - )); - - auto length = moduleSource.size(); - - headers = "Content-Type: text/javascript\n"; - headers += "Connection: keep-alive\n"; - headers += "Access-Control-Allow-Headers: *\n"; - headers += "Access-Control-Allow-Origin: *\n"; - headers += "Content-Length: "; - headers += std::to_string(length); - headers += "\n"; - headers += userConfig["webview_headers"]; - - handled = true; - - if (method.compare("HEAD") == 0) { - ICoreWebView2WebResourceResponse* res = nullptr; - env->CreateWebResourceResponse( - nullptr, - 200, - L"OK", - convertStringToWString(headers).c_str(), - &res - ); - args->put_Response(res); - deferral->Complete(); - } else { - auto body = new char[length]; - memcpy(body, moduleSource.c_str(), length); - - app.dispatch([&, body, length, headers, args, deferral, env] { - ICoreWebView2WebResourceResponse* res = nullptr; - IStream* bytes = SHCreateMemStream((const BYTE*)body, length); - env->CreateWebResourceResponse( - bytes, - 200, - L"OK", - convertStringToWString(headers).c_str(), - &res - ); - args->put_Response(res); - deferral->Complete(); - delete[] body; - }); - } - } else { - if (path.ends_with("//")) { - path = path.substr(0, path.size() - 2); - } - - auto parsedPath = IPC::Router::parseURL(path); - auto rootPath = this->modulePath.parent_path(); - auto resolved = IPC::Router::resolveURLPathForWebView(parsedPath.path, rootPath.string()); - auto mount = IPC::Router::resolveNavigatorMountForWebView(parsedPath.path); - path = resolved.path; - - if (mount.path.size() > 0) { - if (mount.resolution.redirect) { - auto redirectURL = mount.resolution.path; - if (parsedPath.queryString.size() > 0) { - redirectURL += "?" + parsedPath.queryString; - } - - if (parsedPath.fragment.size() > 0) { - redirectURL += "#" + parsedPath.fragment; - } - - ICoreWebView2WebResourceResponse* res = nullptr; - env->CreateWebResourceResponse( - nullptr, - 301, - L"Moved Permanently", - WString( - convertStringToWString("Location: ") + convertStringToWString(redirectURL) + L"\n" + - convertStringToWString("Content-Location: ") + convertStringToWString(redirectURL) + L"\n" - ).c_str(), - &res - ); - - args->put_Response(res); - deferral->Complete(); - return S_OK; - } - } else if (path.size() == 0 && userConfig.contains("webview_default_index")) { - path = userConfig["webview_default_index"]; - } else if (resolved.redirect) { - auto redirectURL = resolved.path; - if (parsedPath.queryString.size() > 0) { - redirectURL += "?" + parsedPath.queryString; - } - - if (parsedPath.fragment.size() > 0) { - redirectURL += "#" + parsedPath.fragment; - } - - ICoreWebView2WebResourceResponse* res = nullptr; - env->CreateWebResourceResponse( - nullptr, - 301, - L"Moved Permanently", - WString( - convertStringToWString("Location: ") + convertStringToWString(redirectURL) + L"\n" + - convertStringToWString("Content-Location: ") + convertStringToWString(redirectURL) + L"\n" - ).c_str(), - &res - ); - - args->put_Response(res); - deferral->Complete(); - return S_OK; - } - - if (mount.path.size() > 0) { - path = mount.path; - } else if (path.size() > 0) { - path = fs::absolute(rootPath / path.substr(1)).string(); - } - - LARGE_INTEGER fileSize; - auto handle = CreateFile(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL); - auto getSizeResult = GetFileSizeEx(handle, &fileSize); - - if (handle) { - CloseHandle(handle); - } - - if (getSizeResult) { - handled = true; - app.dispatch([&, path, args, deferral, env] { - ICoreWebView2WebResourceResponse* res = nullptr; - LPWSTR mimeType = (wchar_t*) L"application/octet-stream"; - IStream* stream = nullptr; - String headers = ""; - - if (path.ends_with(".js") || path.ends_with(".mjs") || path.ends_with(".cjs")) { - mimeType = (wchar_t*) L"text/javascript"; - } else if (path.ends_with(".wasm")) { - mimeType = (wchar_t*) L"application/wasm"; - } else if (path.ends_with(".ts")) { - mimeType = (wchar_t*) L"application/typescript"; - } else if (path.ends_with(".html")) { - mimeType = (wchar_t*) L"text/html"; - } else if (path.ends_with(".css")) { - mimeType = (wchar_t*) L"text/css"; - } else if (path.ends_with(".png")) { - mimeType = (wchar_t*) L"image/png"; - } else if (path.ends_with(".jpg") || path.ends_with(".jpeg")) { - mimeType = (wchar_t*) L"image/jpeg"; - } else if (path.ends_with(".json")) { - mimeType = (wchar_t*) L"application/json"; - } else if (path.ends_with(".jsonld")) { - mimeType = (wchar_t*) L"application/ld+json"; - } else if (path.ends_with(".opus")) { - mimeType = (wchar_t*) L"audio/opus"; - } else if (path.ends_with(".oga")) { - mimeType = (wchar_t*) L"audio/ogg"; - } else if (path.ends_with(".mp3")) { - mimeType = (wchar_t*) L"audio/mp3"; - } else if (path.ends_with(".mp4")) { - mimeType = (wchar_t*) L"video/mp4"; - } else if (path.ends_with(".mpeg")) { - mimeType = (wchar_t*) L"video/mpeg"; - } else if (path.ends_with(".ogv")) { - mimeType = (wchar_t*) L"video/ogg"; - } else { - FindMimeFromData(0, convertStringToWString(path).c_str(), 0, 0, 0, 0, &mimeType, 0); - } - - headers = "Content-Type: "; - headers += convertWStringToString(mimeType) + "\n"; - headers += "Connection: keep-alive\n"; - headers += "Access-Control-Allow-Headers: *\n"; - headers += "Access-Control-Allow-Origin: *\n"; - headers += "Content-Length: "; - headers += std::to_string(fileSize.QuadPart); - headers += "\n"; - headers += userConfig["webview_headers"]; - - if (SHCreateStreamOnFileA(path.c_str(), STGM_READ, &stream) == S_OK) { - env->CreateWebResourceResponse( - stream, - 200, - L"OK", - convertStringToWString(headers).c_str(), - &res - ); - } else { - env->CreateWebResourceResponse( - nullptr, - 404, - L"Not Found", - L"Access-Control-Allow-Origin: *", - &res - ); - } - args->put_Response(res); - deferral->Complete(); - }); - } - } - } - } + ICoreWebView2Environment12* environment12 = nullptr; + ICoreWebView2Environment* environment = nullptr; + ICoreWebView2_18* webview18 = nullptr; + ICoreWebView2_2* webview2 = nullptr; - if (!handled) { - ICoreWebView2WebResourceResponse* res = nullptr; - env->CreateWebResourceResponse( - nullptr, - 404, - L"Not Found", - L"Access-Control-Allow-Origin: *", - &res - ); - args->put_Response(res); - deferral->Complete(); - } + LPWSTR string; + args->TryGetWebMessageAsString(&string); + const auto message = IPC::Message(convertWStringToString(string)); + CoTaskMemFree(string); - return S_OK; - } - ).Get(), - &tokenSchemaFilter - ); + this->webview->QueryInterface(IID_PPV_ARGS(&webview2)); + this->webview->QueryInterface(IID_PPV_ARGS(&webview18)); + webview2->get_Environment(&environment); + environment->QueryInterface(IID_PPV_ARGS(&environment12)); - EventRegistrationToken tokenNewWindow; - - webview->add_NewWindowRequested( - Microsoft::WRL::Callback<ICoreWebView2NewWindowRequestedEventHandler>( - [&](ICoreWebView2* wv, ICoreWebView2NewWindowRequestedEventArgs* e) { - // TODO(heapwolf): Called when window.open() is called in JS, but the new - // window won't have all the setup and request interception. This setup should - // be moved to another location where it can be run for any new window. Right - // now ipc won't work for any new window. - e->put_Handled(true); - return S_OK; - } - ).Get(), - &tokenNewWindow - ); + if (!this->bridge.route(message.str(), nullptr, 0)) { + onMessage(message.str()); + } - WindowOptions options = opts; - webview->QueryInterface(IID_PPV_ARGS(&webview22)); - options.appData["env_COREWEBVIEW2_22_AVAILABLE"] = webview22 != nullptr ? "true" : ""; - auto preload = createPreload(options); - webview->AddScriptToExecuteOnDocumentCreated( - // Note that this may not do anything as preload goes out of scope before event fires - // Consider using w->preloadJavascript, but apps work without this - SSC::convertStringToWString(preload).c_str(), - Microsoft::WRL::Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>( - [&](HRESULT error, PCWSTR id) -> HRESULT { - return S_OK; - } - ).Get() - ); + return S_OK; + }).Get(), + &token + ); + } while (0); + + // configure webview web resource requested callback + do { + EventRegistrationToken token; + webview->add_WebResourceRequested( + Microsoft::WRL::Callback<ICoreWebView2WebResourceRequestedEventHandler>([=, this]( + ICoreWebView2* webview, + ICoreWebView2WebResourceRequestedEventArgs* args + ) { + ICoreWebView2WebResourceRequest* platformRequest = nullptr; + ICoreWebView2Environment* env = nullptr; + ICoreWebView2Deferral* deferral = nullptr; + + // get platform request and environment from event args + do { + ICoreWebView2_2* webview2 = nullptr; + if (webview->QueryInterface(IID_PPV_ARGS(&webview2)) != S_OK) { + return E_FAIL; + } - EventRegistrationToken tokenMessage; - - webview->add_WebMessageReceived( - Microsoft::WRL::Callback<IRecHandler>([&](ICoreWebView2* webview, IArgs* args) -> HRESULT { - LPWSTR messageRaw; - args->TryGetWebMessageAsString(&messageRaw); - SSC::WString message_w(messageRaw); - CoTaskMemFree(messageRaw); - if (onMessage != nullptr) { - SSC::String message = SSC::convertWStringToString(message_w); - auto msg = IPC::Message{message}; - Window* w = reinterpret_cast<Window*>(GetWindowLongPtr((HWND)window, GWLP_USERDATA)); - ICoreWebView2_2* webview2 = nullptr; - ICoreWebView2Environment* env = nullptr; - ICoreWebView2_18* webView18 = nullptr; - ICoreWebView2Environment12* environment = nullptr; - - webview->QueryInterface(IID_PPV_ARGS(&webview2)); - webview2->get_Environment(&env); - env->QueryInterface(IID_PPV_ARGS(&environment)); - - webview->QueryInterface(IID_PPV_ARGS(&webView18)); - - // this should only come from `postMessage()` - if (msg.name == "buffer.create") { - auto seq = msg.seq; - auto size = std::stoull(msg.get("size", "0")); - auto index = msg.index; - ICoreWebView2SharedBuffer* sharedBuffer = nullptr; - // TODO(heapwolf): What to do if creation fails, or size == 0? - HRESULT cshr = environment->CreateSharedBuffer(size, &sharedBuffer); - String additionalData = "{\"seq\":\""; - additionalData += seq; - additionalData += "\",\"index\":"; - additionalData += std::to_string(index); - additionalData += "}"; - cshr = webView18->PostSharedBufferToScript( - sharedBuffer, - COREWEBVIEW2_SHARED_BUFFER_ACCESS_READ_WRITE, - convertStringToWString(additionalData).c_str() - ); - IPC::MessageBuffer msg_buf(sharedBuffer, size); - // TODO(heapwolf): This will leak memory if the buffer is created and - // placed on the map then never removed. Since there's no Window cleanup - // that will remove unused buffers when the window is closed. - w->bridge->router.setMappedBuffer(index, seq, msg_buf); - return S_OK; - } + if (webview2->get_Environment(&env) != S_OK) { + return E_FAIL; + } - if (!w->bridge->route(message, nullptr, 0)) { - onMessage(message); - } - } + if (args->get_Request(&platformRequest) != S_OK) { + return E_FAIL; + } + } while (0); - return S_OK; - }).Get(), - &tokenMessage + auto request = IPC::SchemeHandlers::Request::Builder( + &this->bridge.schemeHandlers, + platformRequest, + env ); - EventRegistrationToken tokenPermissionRequested; - webview->add_PermissionRequested( - Microsoft::WRL::Callback<ICoreWebView2PermissionRequestedEventHandler>([&]( - ICoreWebView2 *webview, - ICoreWebView2PermissionRequestedEventArgs *args - ) -> HRESULT { - static auto userConfig = SSC::getUserConfig(); - COREWEBVIEW2_PERMISSION_KIND kind; - args->get_PermissionKind(&kind); - - if (kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE) { - if ( - userConfig["permissions_allow_microphone"] == "false" || - userConfig["permissions_allow_user_media"] == "false" - ) { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); - } - } - - if (kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA) { - if ( - userConfig["permissions_allow_camera"] == "false" || - userConfig["permissions_allow_user_media"] == "false" - ) { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + // get and set HTTP method + do { + LPWSTR method; + platformRequest->get_Method(&method); + request.setMethod(convertWStringToString(method)); + } while (0); + + // iterator all HTTP headers and set them + do { + ICoreWebView2HttpRequestHeaders* headers = nullptr; + ComPtr<ICoreWebView2HttpHeadersCollectionIterator> iterator; + BOOL hasCurrent = false; + BOOL hasNext = false; + + if (platformRequest->get_Headers(&headers) == S_OK && headers->GetIterator(&iterator) == S_OK) { + while (SUCCEEDED(iterator->get_HasCurrentHeader(&hasCurrent)) && hasCurrent) { + LPWSTR name; + LPWSTR value; + + if (iterator->GetCurrentHeader(&name, &value) != S_OK) { + break; } - } - - if (kind == COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION) { - if (userConfig["permissions_allow_geolocation"] == "false") { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); - } - } - - if (kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS) { - if (userConfig["permissions_allow_notifications"] == "false") { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); - } - } - if (kind == COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS) { - if (userConfig["permissions_allow_sensors"] == "false") { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + request.setHeader(convertWStringToString(name), convertWStringToString(value)); + if (iterator->MoveNext(&hasNext) != S_OK || !hasNext) { + break; } } - - if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) { - if (userConfig["permissions_allow_clipboard"] == "false") { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); - } - } - - if (kind == COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY) { - if (userConfig["permissions_allow_autoplay"] == "false") { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + } while (0); + + // get request body + if ( + request.request->method == "POST" || + request.request->method == "PUT" || + request.request->method == "PATCH" + ) { + IStream* content = nullptr; + if (platformRequest->get_Content(&content) == S_OK && content != nullptr) { + STATSTG stat; + content->Stat(&stat, 0); + size_t size = stat.cbSize.QuadPart; + if (size > 0) { + auto buffer = std::make_shared<char[]>(size); + if (content->Read(buffer.get(), size, nullptr) == S_OK) { + request.setBody(IPC::SchemeHandlers::Body { + size, + std::move(buffer) + }); } } + } + } - if (kind == COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS) { - if (userConfig["permissions_allow_local_fonts"] == "false") { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); - } else { - args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); - } - } + auto req = request.build(); + if (args->GetDeferral(&deferral) != S_OK) { + return E_FAIL; + } - return S_OK; - }).Get(), - &tokenPermissionRequested - ); + const auto handled = this->bridge.schemeHandlers.handleRequest(req, [=](const auto& response) mutable { + args->put_Response(response.platformResponse); + deferral->Complete(); + }); - app.isReady = true; + if (!handled) { + auto response = IPC::SchemeHandlers::Response(req, 404); + response.finish(); + args->put_Response(response.platformResponse); + deferral->Complete(); + } return S_OK; - } - ).Get() - ); + }).Get(), + &token + ); + } while (0); + // notify app is ready + app->isReady = true; return S_OK; - } - ).Get() - ); - }; - - auto res = init(); + }).Get() + ); + }).Get() + ); + } - if (!SUCCEEDED(res)) { - std::cerr << "Webview2 failed to initialize: " << std::to_string(res) << std::endl; - } + Window::~Window () { } ScreenSize Window::getScreenSize () { return ScreenSize { - .height = GetSystemMetrics(SM_CYFULLSCREEN), - .width = GetSystemMetrics(SM_CXFULLSCREEN) + .width = GetSystemMetrics(SM_CXFULLSCREEN), + .height = GetSystemMetrics(SM_CYFULLSCREEN) }; } void Window::about () { - auto text = SSC::String( - app.appData["build_name"] + " " + - "v" + app.appData["meta_version"] + "\n" + - "Built with ssc v" + SSC::VERSION_FULL_STRING + "\n" + - app.appData["meta_copyright"] + auto app = App::sharedApplication(); + auto text = String( + this->bridge.userConfig["build_name"] + " " + + "v" + this->bridge.userConfig["meta_version"] + "\n" + + "Built with ssc v" + VERSION_FULL_STRING + "\n" + + this->bridge.userConfig["meta_copyright"] ); MSGBOXPARAMS mbp; mbp.cbSize = sizeof(MSGBOXPARAMS); - mbp.hwndOwner = window; - mbp.hInstance = app.hInstance; + mbp.hwndOwner = this->window; + mbp.hInstance = app->hInstance; mbp.lpszText = text.c_str(); - mbp.lpszCaption = app.appData["build_name"].c_str(); + mbp.lpszCaption = this->bridge.userConfig["build_name"].c_str(); mbp.dwStyle = MB_USERICON; mbp.dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); - mbp.lpfnMsgBoxCallback = NULL; + mbp.lpfnMsgBoxCallback = nullptr; mbp.dwContextHelpId = 0; MessageBoxIndirect(&mbp); @@ -1503,8 +1214,8 @@ namespace SSC { void Window::kill () { if (this->controller != nullptr) this->controller->Close(); if (this->window != nullptr) { - if (menubar != NULL) DestroyMenu(menubar); - if (menutray != NULL) DestroyMenu(menutray); + if (menubar != nullptr) DestroyMenu(menubar); + if (menutray != nullptr) DestroyMenu(menutray); DestroyWindow(this->window); } } @@ -1515,18 +1226,14 @@ namespace SSC { void Window::exit (int code) { if (this->onExit != nullptr) { - std::cerr << "WARNING: Window#" << index << " exiting with code " << code << std::endl; - if (menubar != NULL) DestroyMenu(menubar); - if (menutray != NULL) DestroyMenu(menutray); + if (menubar != nullptr) DestroyMenu(menubar); + if (menutray != nullptr) DestroyMenu(menutray); this->onExit(code); } - else { - std::cerr << "WARNING: Window#" << index << " window->onExit is null in Window::exit()" << std::endl; - } } void Window::close (int code) { - if (opts.canExit) { + if (options.shouldExitApplicationOnClose) { this->exit(0); DestroyWindow(window); } else { @@ -1547,12 +1254,12 @@ namespace SSC { } void Window::show () { - static auto userConfig = SSC::getUserConfig(); + static auto userConfig = getUserConfig(); auto isAgent = userConfig.count("application_agent") != 0; - if (isAgent && this->opts.index == 0) return; + if (isAgent && this->options.index == 0) return; - if (this->opts.headless == false) { + if (this->options.headless == false) { ShowWindow(window, SW_SHOWNORMAL); UpdateWindow(window); @@ -1577,14 +1284,14 @@ namespace SSC { rc.right = 0; InvalidateRect(this->window, &rc, true); DrawMenuBar(this->window); - RedrawWindow(this->window, NULL, NULL, RDW_INVALIDATE | RDW_ERASE); + RedrawWindow(this->window, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE); } } void Window::hide () { ShowWindow(window, SW_HIDE); UpdateWindow(window); - this->eval(getEmitToRenderProcessJavaScript("windowHide", "{}")); + this->eval(getEmitToRenderProcessJavaScript("window-hidden", "{}")); } void Window::resize (HWND window) { @@ -1597,63 +1304,94 @@ namespace SSC { controller->put_Bounds(bounds); } - void Window::eval (const SSC::String& s) { - app.dispatch([&, this, s] { + void Window::eval (const String& source, const EvalCallback& callback) { + auto app = App::sharedApplication(); + app->dispatch([=, this] { if (this->webview == nullptr) { return; } this->webview->ExecuteScript( - SSC::convertStringToWString(s).c_str(), - nullptr + convertStringToWString(source).c_str(), + Microsoft::WRL::Callback<ICoreWebView2ExecuteScriptCompletedHandler>( + [=, this](HRESULT error, PCWSTR result) -> HRESULT { + if (callback != nullptr) { + if (error != S_OK) { + // TODO(@jwerle): figure out how to get the error message here + callback(JSON::Error("An unknown error occurred")); + return error; + } + + const auto string = convertWStringToString(result); + if (string == "null" || string == "undefined") { + callback(nullptr); + } else if (string == "true") { + callback(true); + } else if (string == "false") { + callback(false); + } else { + double number = 0.0f; + + try { + number = std::stod(string); + } catch (...) { + callback(string); + return S_OK; + } + + callback(number); + } + } + + return S_OK; + } + ).Get() ); }); } - void Window::navigate (const SSC::String& seq, const SSC::String& value) { - auto index = std::to_string(this->opts.index); + void Window::navigate (const String& url) { + auto app = App::sharedApplication(); + auto index = std::to_string(this->options.index); - app.dispatch([&, this, seq, value, index] { + app->dispatch([=, this] { EventRegistrationToken token; this->webview->add_NavigationCompleted( Microsoft::WRL::Callback<ICoreWebView2NavigationCompletedEventHandler>( - [&, this, seq, index, token](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT { - SSC::String state = "1"; - + [=, this](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT { BOOL success; args->get_IsSuccess(&success); - - if (success) { - state = "0"; - } - - this->resolvePromise(seq, state, index); webview->remove_NavigationCompleted(token); - return S_OK; }) .Get(), &token ); - webview->Navigate(SSC::convertStringToWString(value).c_str()); + webview->Navigate(convertStringToWString(url).c_str()); }); } - SSC::String Window::getTitle () { - int len = GetWindowTextLength(window) + 1; - LPTSTR title = new TCHAR[len]; - GetWindowText(window, title, len); - String title_s = convertWStringToString(title); - delete[] title; - return title_s; + const String Window::getTitle () const { + if (window != nullptr) { + const auto size = GetWindowTextLength(window) + 1; + LPTSTR text = new TCHAR[size]{0}; + if (text != nullptr) { + GetWindowText(window, text, size); + const auto title = convertWStringToString(text); + delete [] text; + return title; + } + } + + return ""; } - void Window::setTitle (const SSC::String& title) { + void Window::setTitle (const String& title) { SetWindowText(window, title.c_str()); } - ScreenSize Window::getSize () { + Window::Size Window::getSize () { // 100 are the min width/height that can be returned. Keep defaults in case // the function call fail. UINT32 height = 100; @@ -1662,11 +1400,15 @@ namespace SSC { // Make sure controller exists, and the call to get window bounds succeeds. if (controller != nullptr && controller->get_Bounds(&rect) >= 0) { - height = rect.bottom - rect.top; - width = rect.right - rect.left; + this->size.height = rect.bottom - rect.top; + this->size.width = rect.right - rect.left; } - return { static_cast<int>(height), static_cast<int>(width) }; + return this->size; + } + + const Window::Size Window::getSize () const { + return this->size; } void Window::setSize (int width, int height, int hints) { @@ -1681,11 +1423,11 @@ namespace SSC { SetWindowLong(window, GWL_STYLE, style); if (hints == WINDOW_HINT_MAX) { - m_maxsz.x = width; - m_maxsz.y = height; + maximumSize.x = width; + maximumSize.y = height; } else if (hints == WINDOW_HINT_MIN) { - m_minsz.x = width; - m_minsz.y = height; + minimumSize.x = width; + minimumSize.y = height; } else { RECT r; r.left = r.top = 0; @@ -1693,38 +1435,65 @@ namespace SSC { r.bottom = height; AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0); - SetWindowPos( window, - NULL, - r.left, r.top, r.right - r.left, r.bottom - r.top, + nullptr, + r.left, + r.top, + r.right - r.left, + r.bottom - r.top, SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED ); resize(window); } - this->width = width; - this->height = height; + this->size.width = width; + this->size.height = height; + } + + void Window::setPosition (float x, float y) { + RECT r; + r.left = x; + r.top = y; + r.right = this->size.width; + r.bottom = this->size.height; + + AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0); + SetWindowPos( + window, + nullptr, + r.left, + r.top, + r.right - r.left, + r.bottom - r.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED + ); + + resize(window); + this->position.x = x; + this->position.y = y; } - void Window::setTrayMenu (const SSC::String& seq, const SSC::String& value) { - setMenu(seq, value, true); + void Window::setTrayMenu (const String& value) { + return this->setMenu(value, true); } - void Window::setSystemMenu (const SSC::String& seq, const SSC::String& value) { - setMenu(seq, value, false); + void Window::setSystemMenu (const String& value) { + return this->setMenu(value, false); } - void Window::setMenu (const SSC::String& seq, const SSC::String& source, const bool& isTrayMenu) { - static auto userConfig = SSC::getUserConfig(); - if (source.empty()) return void(0); - auto menuSource = replace(SSC::String(source), "%%", "\n"); + void Window::setMenu (const String& menuSource, const bool& isTrayMenu) { + auto app = App::sharedApplication(); + auto userConfig = this->options.userConfig; + + if (menuSource.empty()) { + return; + } NOTIFYICONDATA nid; if (isTrayMenu) { - static auto app = App::instance(); auto cwd = app->getcwd(); auto trayIconPath = String("application_tray_icon"); @@ -1744,7 +1513,7 @@ namespace SSC { HICON icon; if (trayIconPath.size() > 0) { icon = (HICON) LoadImageA( - NULL, + nullptr, trayIconPath.c_str(), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), @@ -1752,7 +1521,10 @@ namespace SSC { LR_LOADFROMFILE ); } else { - icon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_APPLICATION)); + icon = LoadIcon( + GetModuleHandle(nullptr), + reinterpret_cast<LPCSTR>(MAKEINTRESOURCE(IDI_APPLICATION)) + ); } menutray = CreatePopupMenu(); @@ -1797,9 +1569,9 @@ namespace SSC { if (line.find("---") != -1) { if (isTrayMenu) { - AppendMenuW(menutray, MF_SEPARATOR, 0, NULL); + AppendMenuW(menutray, MF_SEPARATOR, 0, nullptr); } else { - AppendMenuW(subMenu, MF_SEPARATOR, 0, NULL); + AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr); } continue; } @@ -1807,19 +1579,19 @@ namespace SSC { auto parts = split(line, ':'); auto title = parts[0]; int mask = 0; - SSC::String key = ""; + String key = ""; auto accelerators = split(parts[1], '+'); - auto accl = SSC::String(""); + auto accl = String(""); key = trim(parts[1]) == "_" ? "" : trim(accelerators[0]); if (key.size() > 0) { - bool isShift = SSC::String("ABCDEFGHIJKLMNOPQRSTUVWXYZ").find(key) != -1; + bool isShift = String("ABCDEFGHIJKLMNOPQRSTUVWXYZ").find(key) != -1; accl = key; if (accelerators.size() > 1) { - accl = SSC::String(trim(accelerators[1]) + "+" + key); + accl = String(trim(accelerators[1]) + "+" + key); accl = replace(accl, "CommandOrControl", "Ctrl"); accl = replace(accl, "Command", "Ctrl"); accl = replace(accl, "Control", "Ctrl"); @@ -1827,18 +1599,18 @@ namespace SSC { } if (isShift) { - accl = SSC::String("Shift+" + accl); + accl = String("Shift+" + accl); } } - auto display = SSC::String(title + "\t" + accl); + auto display = String(title + "\t" + accl); if (isTrayMenu) { AppendMenuA(menutray, MF_STRING, itemId, display.c_str()); - menuTrayMap[itemId] = SSC::String(title + ":" +(parts.size() > 1 ? parts[1] : "")); + menuTrayMap[itemId] = String(title + ":" +(parts.size() > 1 ? parts[1] : "")); } else { AppendMenuA(subMenu, MF_STRING, itemId, display.c_str()); - menuMap[itemId] = SSC::String(title + "\t" + menuTitle); + menuMap[itemId] = String(title + "\t" + menuTitle); } itemId++; @@ -1865,219 +1637,119 @@ namespace SSC { rc.right = 0; InvalidateRect(this->window, &rc, true); DrawMenuBar(this->window); - RedrawWindow(this->window, NULL, NULL, RDW_INVALIDATE | RDW_ERASE); - } - - if (seq.size() > 0) { - auto index = std::to_string(this->opts.index); - this->resolvePromise(seq, "0", index); + RedrawWindow(this->window, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE); } } void Window::setSystemMenuItemEnabled (bool enabled, int barPos, int menuPos) { - // @TODO(): provide impl + // TODO } void Window::closeContextMenu() { - // @TODO(jwerle) + // TODO } - void Window::closeContextMenu(const SSC::String &seq) { - // @TODO(jwerle) + void Window::closeContextMenu (const String &seq) { + // TODO } - void Window::setContextMenu (const SSC::String& seq, const SSC::String& value) { - HMENU hPopupMenu = CreatePopupMenu(); + void Window::setContextMenu (const String& seq, const String& menuSource) { + if (menuSource.empty()) { + return; + } - auto menuItems = split(value, '_'); + const auto menuItems = split(menuSource, '\n'); + auto menu = CreatePopupMenu(); + Vector<String> lookup; int index = 1; - std::vector<SSC::String> lookup; + lookup.push_back(""); - for (auto item : menuItems) { - auto pair = split(trim(item), ':'); - auto key = SSC::String(""); - - if (pair.size() > 1) { - key = pair[1]; - } + for (const auto& item : menuItems) { + const auto pair = split(trim(item), ':'); if (pair[0].find("---") != -1) { - InsertMenu(hPopupMenu, 0, MF_SEPARATOR, 0, NULL); + InsertMenu(menu, 0, MF_SEPARATOR, 0, nullptr); } else { lookup.push_back(pair[0]); - InsertMenu(hPopupMenu, 0, MF_BYPOSITION | MF_STRING, index++, pair[0].c_str()); + InsertMenu(menu, 0, MF_BYPOSITION | MF_STRING, index++, pair[0].c_str()); } } - SetForegroundWindow(window); + SetForegroundWindow(this->window); - POINT p; - GetCursorPos(&p); + POINT point; + GetCursorPos(&point); - auto selection = TrackPopupMenu( - hPopupMenu, + const auto selection = TrackPopupMenu( + menu, TPM_RETURNCMD | TPM_NONOTIFY, - p.x, - p.y, + point.x, + point.y, 0, - window, + this->window, nullptr ); - DestroyMenu(hPopupMenu); - if (selection == 0) return; - this->eval(getResolveMenuSelectionJavaScript(seq, lookup.at(selection), "contextMenu", "context")); - } - - int Window::openExternal (const SSC::String& url) { - ShellExecute(nullptr, "Open", url .c_str(), nullptr, nullptr, SW_SHOWNORMAL); - // TODO how to detect success here. do we care? - return 0; - } - - void Window::setBackgroundColor(int r, int g, int b, float a) { - SetBkColor(GetDC(window), RGB(r, g, b)); - app.wcex.hbrBackground = CreateSolidBrush(RGB(r, g, b)); - } + DestroyMenu(menu); - // message is defined in WinUser.h - // https://raw.githubusercontent.com/tpn/winsdk-10/master/Include/10.0.10240.0/um/WinUser.h - LRESULT CALLBACK Window::WndProc( - HWND hWnd, - UINT message, - WPARAM wParam, - LPARAM lParam - ) { - static auto app = SSC::App::instance(); - Window* w = reinterpret_cast<Window*>(GetWindowLongPtr(hWnd, GWLP_USERDATA)); - - if (message == WM_COPYDATA) { - auto copyData = reinterpret_cast<PCOPYDATASTRUCT>(lParam); - message = (UINT) copyData->dwData; - wParam = (WPARAM) copyData->cbData; - lParam = (LPARAM) copyData->lpData; + if (selection != 0) { + this->eval(getResolveMenuSelectionJavaScript(seq, lookup.at(selection), "contextMenu", "context")); } + } - switch (message) { - case WM_SIZE: { - if (w == nullptr || w->webview == nullptr) { - break; - } - - RECT bounds; - GetClientRect(hWnd, &bounds); - w->controller->put_Bounds(bounds); - break; - } - - case WM_SOCKET_TRAY: { - static auto userConfig = SSC::getUserConfig(); - auto isAgent = userConfig.count("tray_icon") != 0; - - if (lParam == WM_LBUTTONDOWN) { - SetForegroundWindow(hWnd); - if (isAgent) { - POINT pt; - GetCursorPos(&pt); - TrackPopupMenu(w->menutray, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hWnd, NULL); - } - - PostMessage(hWnd, WM_NULL, 0, 0); - - // broadcast an event to all the windows that the tray icon was clicked - if (app != nullptr && app->windowManager != nullptr) { - for (auto window : app->windowManager->windows) { - if (window != nullptr) { - window->bridge->router.emit("tray", "true"); - } - } - } - } - - // fall through to WM_COMMAND!! - } - - case WM_COMMAND: { - if (w == nullptr) break; - - if (w->menuMap.contains(wParam)) { - String meta(w->menuMap[wParam]); - auto parts = split(meta, '\t'); - - if (parts.size() > 1) { - auto title = parts[0]; - auto parent = parts[1]; + void Window::setBackgroundColor (const String& rgba) { + const auto parts = split(trim(replace(replace(rgba, "rgba(", ""), ")", "")), ','); + int r = 0, g = 0, b = 0; - if (String(title).find("About") == 0) { - w->about(); - break; - } + if (parts.size() >= 3) { + try { r = std::stoi(trim(parts[0])); } + catch (...) {} - if (String(title).find("Quit") == 0) { - w->exit(0); - break; - } + try { g = std::stoi(trim(parts[1])); } + catch (...) {} - w->eval(getResolveMenuSelectionJavaScript("0", title, parent, "system")); - } - } else if (w->menuTrayMap.contains(wParam)) { - String meta(w->menuTrayMap[wParam]); - auto parts = split(meta, ':'); - if (parts.size() > 0) { - auto title = trim(parts[0]); - auto tag = parts.size() > 1 ? trim(parts[1]) : ""; - w->eval(getResolveMenuSelectionJavaScript("0", title, tag, "tray")); - } - } + try { b = std::stoi(trim(parts[2])); } + catch (...) {} - break; - } + return this->setBackgroundColor(r, g, b, 1.0); + } + } - case WM_SETTINGCHANGE: { - // TODO(heapwolf): Dark mode - break; - } + void Window::setBackgroundColor (int r, int g, int b, float a) { + SetBkColor(GetDC(this->window), RGB(r, g, b)); + } - case WM_CREATE: { - // TODO(heapwolf): Dark mode - SetWindowTheme(hWnd, L"Explorer", NULL); - SetMenu(hWnd, CreateMenu()); - break; - } + String Window::getBackgroundColor () { + auto color = GetBkColor(GetDC(this->window)); + if (color == CLR_INVALID) { + return ""; + } - case WM_CLOSE: { - w->close(0); - break; - } + const auto r = GetRValue(color); + const auto g = GetGValue(color); + const auto b = GetBValue(color); - case WM_HOTKEY: { - w->hotkey.onHotKeyBindingCallback((HotKeyBinding::ID) wParam); - break; - } + char string[100]; - case WM_HANDLE_DEEP_LINK: { - auto url = SSC::String((const char*) lParam, wParam); - SSC::JSON::Object json = SSC::JSON::Object::Entries {{ - "url", url - }}; + snprintf( + string, + sizeof(string), + "rgba(%d, %d, %d, %f)", + r, + g, + b, + 1.0f + ); - if (app != nullptr && app->windowManager != nullptr) { - for (auto window : app->windowManager->windows) { - if (window != nullptr) { - window->bridge->router.emit("applicationurl", json.str()); - } - } - } - break; - } + return string; + } - default: - return DefWindowProc(hWnd, message, wParam, lParam); - break; - } + void Window::handleApplicationURL (const String& url) { + JSON::Object json = JSON::Object::Entries {{ + "url", url + }}; - return 0; + this->bridge.emit("applicationurl", json.str()); } - -} // namespace SSC +} diff --git a/src/window/window.hh b/src/window/window.hh index 134872e58d..5c823c54fd 100644 --- a/src/window/window.hh +++ b/src/window/window.hh @@ -1,129 +1,69 @@ -#ifndef SSC_WINDOW_WINDOW_H -#define SSC_WINDOW_WINDOW_H +#ifndef SOCKET_RUNTIME_WINDOW_WINDOW_H +#define SOCKET_RUNTIME_WINDOW_WINDOW_H -#include <iostream> - -#include "../ipc/ipc.hh" -#include "../app/app.hh" #include "../core/env.hh" #include "../core/config.hh" +#include "../core/webview.hh" +#include "../ipc/ipc.hh" + +#include "dialog.hh" #include "hotkey.hh" -#include "options.hh" -#ifndef SSC_MAX_WINDOWS -#define SSC_MAX_WINDOWS 32 +#ifndef SOCKET_RUNTIME_MAX_WINDOWS +#define SOCKET_RUNTIME_MAX_WINDOWS 32 #endif -#ifndef SSC_MAX_WINDOWS_RESERVED -#define SSC_MAX_WINDOWS_RESERVED 16 +#define SOCKET_RUNTIME_SERVICE_WORKER_CONTAINER_WINDOW_INDEX SOCKET_RUNTIME_MAX_WINDOWS + 1 + +#ifndef SOCKET_RUNTIME_MAX_WINDOWS_RESERVED +#define SOCKET_RUNTIME_MAX_WINDOWS_RESERVED 16 #endif -#if defined(_WIN32) +#if SOCKET_RUNTIME_PLATFORM_WINDOWS #define WM_HANDLE_DEEP_LINK WM_APP + 1 #define WM_SOCKET_TRAY WM_APP + 2 #endif namespace SSC { // forward - class Dialog; class Window; } -#if defined(__APPLE__) -#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR -@interface SSCWindowDelegate : NSObject -@property (nonatomic) SSC::Window* window; -@end +#if SOCKET_RUNTIME_PLATFORM_APPLE +@class SSCWindow; -@interface SSCUIPickerDelegate : NSObject< - UIDocumentPickerDelegate, - - // TODO(@jwerle): use 'PHPickerViewControllerDelegate' instead - UIImagePickerControllerDelegate, - UINavigationControllerDelegate -> -@property (nonatomic) SSC::Dialog* dialog; -// UIDocumentPickerDelegate -- (void) documentPicker: (UIDocumentPickerViewController*) controller - didPickDocumentsAtURLs: (NSArray<NSURL*>*) urls; -- (void) documentPickerWasCancelled: (UIDocumentPickerViewController*) controller; - -// UIImagePickerControllerDelegate -- (void) imagePickerController: (UIImagePickerController*) picker - didFinishPickingMediaWithInfo: (NSDictionary<UIImagePickerControllerInfoKey, id>*) info; -- (void) imagePickerControllerDidCancel: (UIImagePickerController*) picker; -@end +@interface SSCWindowDelegate : +#if SOCKET_RUNTIME_PLATFORM_IOS + NSObject< + UIScrollViewDelegate, + WKScriptMessageHandler + > #else -@interface SSCWindowDelegate : NSObject <NSWindowDelegate, WKScriptMessageHandler> -- (void) userContentController: (WKUserContentController*) userContentController - didReceiveScriptMessage: (WKScriptMessage*) scriptMessage; -@end + NSObject < + NSWindowDelegate, + WKScriptMessageHandler + > #endif - -#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR -@interface SSCBridgedWebView : WKWebView<WKUIDelegate> -#else -@interface WKOpenPanelParameters (WKPrivate) -- (NSArray<NSString*>*) _acceptedMIMETypes; -- (NSArray<NSString*>*) _acceptedFileExtensions; -- (NSArray<NSString*>*) _allowedFileExtensions; @end -@interface SSCBridgedWebView : WKWebView< - WKUIDelegate, - NSDraggingDestination, - NSFilePromiseProviderDelegate, - NSDraggingSource -> -- (NSDragOperation) draggingSession: (NSDraggingSession *) session -sourceOperationMaskForDraggingContext: (NSDraggingContext) context; - -- (void) webView: (WKWebView*) webView - runOpenPanelWithParameters: (WKOpenPanelParameters*) parameters - initiatedByFrame: (WKFrameInfo*) frame - completionHandler: (void (^)(NSArray<NSURL*>*)) completionHandler; +@interface SSCWindow : +#if SOCKET_RUNTIME_PLATFORM_IOS + UIWindow +#else + NSWindow #endif -#if (!TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) || (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_15) - -- (void) webView: (WKWebView*) webView - requestDeviceOrientationAndMotionPermissionForOrigin: (WKSecurityOrigin*) origin - initiatedByFrame: (WKFrameInfo*) frame - decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler; - -- (void) webView: (WKWebView*) webView - requestMediaCapturePermissionForOrigin: (WKSecurityOrigin*) origin - initiatedByFrame: (WKFrameInfo*) frame - type: (WKMediaCaptureType) type - decisionHandler: (void (^)(WKPermissionDecision decision)) decisionHandler; +#if SOCKET_RUNTIME_PLATFORM_MACOS + @property (nonatomic, strong) NSView *titleBarView; + @property (nonatomic) NSPoint windowControlOffsets; #endif - -- (void) webView: (WKWebView*) webView - runJavaScriptAlertPanelWithMessage: (NSString*) message - initiatedByFrame: (WKFrameInfo*) frame - completionHandler: (void (^)(void)) completionHandler; - -- (void) webView: (WKWebView*) webView - runJavaScriptConfirmPanelWithMessage: (NSString*) message - initiatedByFrame: (WKFrameInfo*) frame - completionHandler: (void (^)(BOOL result)) completionHandler; -@end - -@interface SSCNavigationDelegate : NSObject<WKNavigationDelegate> -@property (nonatomic) SSC::IPC::Bridge* bridge; -- (void) webView: (WKWebView*) webview - decidePolicyForNavigationAction: (WKNavigationAction*) navigationAction - decisionHandler: (void (^)(WKNavigationActionPolicy)) decisionHandler; - -- (void) webView: (WKWebView*) webView - decidePolicyForNavigationResponse: (WKNavigationResponse*) navigationResponse - decisionHandler: (void (^)(WKNavigationResponsePolicy)) decisionHandler; + @property (nonatomic, strong) SSCWebView* webview; @end #endif namespace SSC { -#if defined(_WIN32) +#if SOCKET_RUNTIME_PLATFORM_WINDOWS class DragDrop; #endif @@ -134,102 +74,400 @@ namespace SSC { WINDOW_HINT_FIXED = 3 // Window size can not be changed by a user }; + /** + * A container for holding an application's screen size + */ struct ScreenSize { - int height = 0; int width = 0; + int height = 0; }; + /** + * `Window` is a base class that implements a variety of APIs for a + * window on host platforms. Windows contain a WebView that is connected + * to the core runtime through a window's "IPC Bridge". + */ class Window { public: - App& app; - WindowOptions opts; + using EvalCallback = Function<void(const JSON::Any)>; + + /** + * A container for representing the window position in a + * Cartesian coordinate system (screen coordinates) + */ + struct Position { + float x = 0.0f; + float y = 0.0f; + }; + + /** + * A container for representing the size of a window. + */ + struct Size { + int width = 0; + int height = 0; + }; + + /** + * An enumeration of the "ready state" of a window. + * These values closel relate to the `globalThis.document.readyState` + * possible values (loading, interactive, complete) + */ + enum class ReadyState { + None, + Loading, + Interactive, + Complete + }; + + /** + * `Window::Options` is an extended `IPC::Preload::Options` container for + * configuring a new `Window`. + */ + struct Options : public IPC::Preload::Options { + /** + * If `true`, the window can be minimized. + * This option value is only supported on desktop. + */ + bool minimizable = true; + + /** + * If `true`, the window can be maximized. + * This option value is only supported on desktop. + */ + bool maximizable = true; + + /** + * If `true`, the window can be resized. + * This option value is only supported on desktop. + */ + bool resizable = true; + + /** + * If `true`, the window can be closed. + * This option value is only supported on desktop. + */ + bool closable = true; + + /** + * If `true`, the window can be "frameless". + * This option value is only supported on desktop. + */ + bool frameless = false; + + /** + * If `true`, the window is considered a "utility" window. + * This option value is only supported on desktop. + */ + bool utility = false; + + /** + * If `true`, the window, when the window is "closed", it can + * exit the application. + * This option value is only supported on desktop. + */ + bool shouldExitApplicationOnClose = false; + + /** + * The maximum height in screen pixels the window can be. + */ + float maxHeight = 0.0; + + /** + * The minimum height in screen pixels the window can be. + */ + float minHeight = 0.0; + + /** + * The absolute height in screen pixels the window can be. + */ + float height = 0.0; + + /** + * The maximum width in screen pixels the window can be. + */ + float maxWidth = 0.0; + + /** + * The minimum width in screen pixels the window can be. + */ + float minWidth = 0.0; + + /** + * The absolute width in screen pixels the window can be. + */ + float width = 0.0; + + /** + * The window border/corner radius. + * This option value is only supported on macOS. + */ + float radius = 0.0; + + /** + * Thw window frame margin. + * This option value is only supported on macOS. + */ + float margin = 0.0; + + /** + * A string (split on ':') provides two float values which will + * set the window's aspect ratio. + * This option value is only supported on desktop. + */ + String aspectRatio = ""; + + /** + * A string that describes a style for the window title bar. + * Valid values are: + * - hidden + * - hiddenInset + * This option value is only supported on macOS and Windows. + */ + String titlebarStyle = ""; + + /** + * A string value (split on 'x') in the form of `XxY` where + * - `X` is the value in screen pixels offset from the left of the + * window frame + * - `Y` is the value in screen pixels offset from the top of the + * window frame + * The values control the offset of the "close", "minimize", and "maximize" + * button controls for a window. + * This option value is only supported on macOS. + */ + String windowControlOffsets = ""; + + /** + * A string value in the form of `rgba(r, g, b, a)` where + * - `r` is the "red" channel value, an integer between `0` and `255` + * - `g` is the "green" channel value, an integer between `0` and `255` + * - `b` is the "blue" channel value, an integer between `0` and `255` + * - `a` is the "alpha" channel value, a float between `0` and `1` + * The values represent the background color of a window when the platform + * system theme is in "light mode". This also be the "default" theme. + */ + String backgroundColorLight = ""; + + /** + * A string value in the form of `rgba(r, g, b, a)` where + * - `r` is the "red" channel value, an integer between `0` and `255` + * - `g` is the "green" channel value, an integer between `0` and `255` + * - `b` is the "blue" channel value, an integer between `0` and `255` + * - `a` is the "alpha" channel value, a float between `0` and `1` + * The values represent the background color of a window when the platform + * system theme is in "dark mode". + */ + String backgroundColorDark = ""; + + /** + * A callback function that is called when a "script message" is received + * from the WebVew. + */ + MessageCallback onMessage = [](const String) {}; + + /** + * A callback function that is called when the window wants to exit the + * application. This function is called _only_ when the + * `shouldExitApplicationOnClose` option is `true`. + */ + ExitCallback onExit = nullptr; + }; + + /** + * The current "ready state" of the window + */ + ReadyState readyState = ReadyState::None; + + /** + * The options used to create this window. + */ + const Window::Options options; + + /** + * The "hot key" context for this window. + * The "hot key" features are only available on desktop platforms. + */ HotKeyContext hotkey; + /** + * The IPC bridge that connects the application window's WebView to + * the runtime and various core modules and functions. + */ + IPC::Bridge bridge; + + /** + * The (x, y) screen coordinate position of the window. + */ + Position position; + + /** + * The current mouse (x, y) position when dragging started. + */ + Position dragStart; + + /** + * The current mouse (x, y) position while dragging. + */ + Position dragging; + + /** + * The size (width, height) of the window. + */ + Size size; + + /** + * A shared pointer the application `Core` instance. + */ + SharedPointer<Core> core = nullptr; + + /** + * A callback function that is called when a "script message" is received + * from the WebVew. + */ MessageCallback onMessage = [](const String) {}; + + /** + * A callback function that is called when the window wants to exit the + * application. This function is called _only_ when + * `options.shouldExitApplicationOnClose` is `true`. + */ ExitCallback onExit = nullptr; - IPC::Bridge *bridge = nullptr; + + /** + * The unique index of the window instance. This value is used by the + * `WindowManager` and various standard libary IPC functions for + * addressing a window a unique manner. + */ int index = 0; - int width = 0; - int height = 0; - bool exiting = false; - #if !defined(__APPLE__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) - fs::path modulePath; + /** + * This value is `true` when the window has closed an is indicating + * that the application is exiting + */ + Atomic<bool> isExiting = false; + + /** + * A pointer to the platform WebView. + */ + WebView* webview = nullptr; + + /** + * A controller for showing system dialogs such as a "file picker" + */ + Dialog dialog; + + /** + * The current background color of the window + */ + Color backgroundColor; + + #if SOCKET_RUNTIME_PLATFORM_IOS + SSCWebViewController* viewController = nullptr; #endif - #if defined(__APPLE__) - #if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR - NSWindow* window; - #endif - SSCBridgedWebView* webview; + #if SOCKET_RUNTIME_PLATFORM_APPLE SSCWindowDelegate* windowDelegate = nullptr; - SSCNavigationDelegate *navigationDelegate = nullptr; - #elif defined(__linux__) && !defined(__ANDROID__) + SSCWindow* window = nullptr; + WKProcessPool* processPool = nullptr; + #elif SOCKET_RUNTIME_PLATFORM_LINUX GtkSelectionData *selectionData = nullptr; GtkAccelGroup *accelGroup = nullptr; - GtkWidget *webview = nullptr; - GtkWidget *window = nullptr; - GtkWidget *menubar = nullptr; - GtkWidget *menutray = nullptr; - GtkWidget *vbox = nullptr; - GtkWidget *popup = nullptr; - std::vector<String> draggablePayload; + + GtkWidget* vbox = nullptr; + GtkWidget* window = nullptr; + GtkWidget* menubar = nullptr; + GtkWidget* menutray = nullptr; + GtkWidget* contextMenu = nullptr; + + bool isDarkMode = false; + + #if SOCKET_RUNTIME_DESKTOP_EXTENSION + void* userContentManager; + void* policies; + void* settings; + #else + WebKitUserContentManager* userContentManager; + WebKitWebsitePolicies* policies; + WebKitSettings* settings; + #endif + + int contextMenuID; double dragLastX = 0; double dragLastY = 0; + + bool shouldDrag; + Vector<String> draggablePayload; bool isDragInvokedInsideWindow; - int popupId; - #elif defined(_WIN32) - static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - bool usingCustomEdgeRuntimeDirectory = false; + GdkPoint initialLocation; + #elif SOCKET_RUNTIME_PLATFORM_WINDOWS ICoreWebView2Controller *controller = nullptr; - ICoreWebView2 *webview = nullptr; HMENU menubar; HMENU menutray; DWORD mainThread = GetCurrentThreadId(); - POINT m_minsz = POINT {0, 0}; - POINT m_maxsz = POINT {0, 0}; - DragDrop* drop; + + double dragLastX = 0; + double dragLastY = 0; + bool shouldDrag; + SharedPointer<DragDrop> drop; + + POINT minimumSize = POINT {0, 0}; + POINT maximumSize = POINT {0, 0}; + + POINT initialCursorPos = POINT {0, 0}; + RECT initialWindowPos = RECT {0, 0, 0, 0}; + HWND window; - std::map<int, std::string> menuMap; - std::map<int, std::string> menuTrayMap; + std::map<int, String> menuMap; + std::map<int, String> menuTrayMap; + void resize (HWND window); + #elif SOCKET_RUNTIME_PLATFORM_ANDROID + String pendingNavigationLocation; + jobject androidWindowRef; + std::map<String, EvalCallback> evaluateJavaScriptCallbacks; #endif - Window (App&, WindowOptions); - #if defined(__APPLE__) + Window (SharedPointer<Core> core, const Window::Options&); ~Window (); - #endif static ScreenSize getScreenSize (); void about (); - void eval (const String&); + void eval (const String&, const EvalCallback& callback = nullptr); void show (); void hide (); void kill (); - void exit (int code); - void close (int code); - void minimize(); - void maximize(); - void restore(); - void navigate (const String&, const String&); - String getTitle (); + void exit (int code = 0); + void close (int code = 0); + void minimize (); + void maximize (); + void restore (); + void navigate (const String&); + const String getTitle () const; void setTitle (const String&); - ScreenSize getSize (); - void setSize (int, int, int); + Size getSize (); + const Size getSize () const; + void setSize (int width, int height, int hints = 0); + void setPosition (float, float); void setContextMenu (const String&, const String&); void closeContextMenu (const String&); void closeContextMenu (); - #if defined(__linux__) && !defined(__ANDROID__) + #if SOCKET_RUNTIME_PLATFORM_LINUX void closeContextMenu (GtkWidget *, const String&); #endif void setBackgroundColor (int r, int g, int b, float a); + void setBackgroundColor (const String& rgba); + void setBackgroundColor (const Color& color); + String getBackgroundColor (); void setSystemMenuItemEnabled (bool enabled, int barPos, int menuPos); - void setSystemMenu (const String& seq, const String& dsl); - void setMenu (const String& seq, const String& dsl, const bool& isTrayMenu); - void setTrayMenu (const String& seq, const String& dsl); + void setSystemMenu (const String& dsl); + void setMenu (const String& dsl, const bool& isTrayMenu); + void setTrayMenu (const String& dsl); void showInspector (); - int openExternal (const String& s); + + void handleApplicationURL (const String& url); + void onReadyStateChange (const ReadyState readyState) {} void resolvePromise ( const String& seq, @@ -239,8 +477,6 @@ namespace SSC { if (seq.find("R") == 0) { this->eval(getResolveToRenderProcessJavaScript(seq, state, value)); } - - this->onMessage(IPC::getResolveToMainProcessMessage(seq, state, value)); } void resolvePromise ( @@ -264,21 +500,13 @@ namespace SSC { } }; - struct WindowManagerOptions { + struct WindowManagerOptions : Window::Options { String defaultHeight = "0"; String defaultWidth = "0"; String defaultMinWidth = "0"; String defaultMinHeight = "0"; String defaultMaxWidth = "100%"; String defaultMaxHeight = "100%"; - bool headless = false; - bool isTest = false; - bool canExit = false; - String argv = ""; - String cwd = ""; - Map appData; - MessageCallback onMessage = [](const String) {}; - ExitCallback onExit = nullptr; }; struct WindowPropertiesFlags { @@ -310,447 +538,63 @@ namespace SSC { public: WindowStatus status; WindowManager &manager; + Vector<String> pendingApplicationURLs; + Mutex mutex; + int index = 0; ManagedWindow ( WindowManager &manager, - App &app, - WindowOptions opts - ) : Window(app, opts) , manager(manager) { } - - ~ManagedWindow () {} - - void show (const String &seq) { - auto index = std::to_string(this->opts.index); - manager.log("Showing Window#" + index + " (seq=" + seq + ")"); - status = WindowStatus::WINDOW_SHOWING; - Window::show(); - status = WindowStatus::WINDOW_SHOWN; - } - - void hide (const String &seq) { - if ( - status > WindowStatus::WINDOW_HIDDEN && - status < WindowStatus::WINDOW_EXITING - ) { - auto index = std::to_string(this->opts.index); - manager.log("Hiding Window#" + index + " (seq=" + seq + ")"); - status = WindowStatus::WINDOW_HIDING; - Window::hide(); - status = WindowStatus::WINDOW_HIDDEN; - } - } - - void close (int code) { - if (status < WindowStatus::WINDOW_CLOSING) { - auto index = std::to_string(this->opts.index); - manager.log("Closing Window#" + index + " (code=" + std::to_string(code) + ")"); - status = WindowStatus::WINDOW_CLOSING; - Window::close(code); - if (this->opts.canExit) { - status = WindowStatus::WINDOW_EXITED; - } else { - status = WindowStatus::WINDOW_CLOSED; - } - } - } - - void exit (int code) { - if (status < WindowStatus::WINDOW_EXITING) { - auto index = std::to_string(this->opts.index); - manager.log("Exiting Window#" + index + " (code=" + std::to_string(code) + ")"); - status = WindowStatus::WINDOW_EXITING; - Window::exit(code); - status = WindowStatus::WINDOW_EXITED; - gc(); - } - } - - void kill () { - if (status < WindowStatus::WINDOW_KILLING) { - auto index = std::to_string(this->opts.index); - manager.log("Killing Window#" + index); - status = WindowStatus::WINDOW_KILLING; - Window::kill(); - status = WindowStatus::WINDOW_KILLED; - gc(); - } - } - - void gc () { - manager.destroyWindow(reinterpret_cast<Window*>(this)); - } - - JSON::Object json () { - auto index = this->opts.index; - auto size = this->getSize(); - - return JSON::Object::Entries { - { "index", index }, - { "title", this->getTitle() }, - { "width", size.width }, - { "height", size.height }, - { "status", this->status } - }; - } + SharedPointer<Core> core, + const Window::Options& options + ); + + ~ManagedWindow (); + + void show (); + void hide (); + void close (int code = 0); + void exit (int code = 0); + void kill (); + void gc (); + JSON::Object json () const; + void handleApplicationURL (const String& url); + void onReadyStateChange (const ReadyState& readyState); + bool emit (const String& event, const JSON::Any& json = {}); + void setBackgroundColor (int r, int g, int b, float a); + void setBackgroundColor (const String& rgba); + void setBackgroundColor (const Color& color); + String getBackgroundColor (); }; - std::chrono::system_clock::time_point lastDebugLogLine; - - App &app; - bool destroyed = false; - std::vector<bool> inits; - std::vector<ManagedWindow*> windows; - std::recursive_mutex mutex; + Vector<SharedPointer<ManagedWindow>> windows; WindowManagerOptions options; - - WindowManager (App &app) : - app(app), - inits(SSC_MAX_WINDOWS + SSC_MAX_WINDOWS_RESERVED), - windows(SSC_MAX_WINDOWS + SSC_MAX_WINDOWS_RESERVED) - { - if (isDebugEnabled()) { - lastDebugLogLine = std::chrono::system_clock::now(); - } - } - - ~WindowManager () { - destroy(); - } - - void destroy () { - if (this->destroyed) return; - for (auto window : windows) { - destroyWindow(window); - } - - this->destroyed = true; - - windows.clear(); - inits.clear(); - } - - void configure (WindowManagerOptions configuration) { - if (destroyed) return; - this->options.defaultHeight = configuration.defaultHeight; - this->options.defaultWidth = configuration.defaultWidth; - this->options.defaultMinWidth = configuration.defaultMinWidth; - this->options.defaultMinHeight = configuration.defaultMinHeight; - this->options.defaultMaxWidth = configuration.defaultMaxWidth; - this->options.defaultMaxHeight = configuration.defaultMaxHeight; - this->options.onMessage = configuration.onMessage; - this->options.appData = configuration.appData; - this->options.onExit = configuration.onExit; - this->options.headless = configuration.headless; - this->options.isTest = configuration.isTest; - this->options.argv = configuration.argv; - this->options.cwd = configuration.cwd; - } - - void inline log (const String line) { - if (destroyed || !isDebugEnabled()) return; - using namespace std::chrono; - - auto now = system_clock::now(); - auto delta = duration_cast<milliseconds>(now - lastDebugLogLine).count(); - - std::cout << "• " << line; - std::cout << " \033[0;32m+" << delta << "ms\033[0m"; - std::cout << std::endl; - - lastDebugLogLine = now; - } - - ManagedWindow* getWindow (int index, WindowStatus status) { - std::lock_guard<std::recursive_mutex> guard(this->mutex); - if (this->destroyed) return nullptr; - if ( - getWindowStatus(index) > WindowStatus::WINDOW_NONE && - getWindowStatus(index) < status - ) { - return windows[index]; - } - - return nullptr; - } - - ManagedWindow* getWindow (int index) { - return getWindow(index, WindowStatus::WINDOW_EXITING); - } - - ManagedWindow* getOrCreateWindow (int index) { - return getOrCreateWindow(index, WindowOptions {}); - } - - ManagedWindow* getOrCreateWindow (int index, WindowOptions opts) { - if (this->destroyed) return nullptr; - if (index < 0) return nullptr; - if (getWindowStatus(index) == WindowStatus::WINDOW_NONE) { - opts.index = index; - return createWindow(opts); - } - - return getWindow(index); - } - - WindowStatus getWindowStatus (int index) { - std::lock_guard<std::recursive_mutex> guard(this->mutex); - if (this->destroyed) return WindowStatus::WINDOW_NONE; - if (index >= 0 && inits[index] && windows[index] != nullptr) { - return windows[index]->status; - } - - return WindowStatus::WINDOW_NONE; - } - - void destroyWindow (int index) { - std::lock_guard<std::recursive_mutex> guard(this->mutex); - if (destroyed) return; - if (index >= 0 && inits[index] && windows[index] != nullptr) { - return destroyWindow(windows[index]); - } - } - - void destroyWindow (ManagedWindow* window) { - if (destroyed) return; - if (window != nullptr) { - return destroyWindow(reinterpret_cast<Window*>(window)); - } - } - - void destroyWindow (Window* window) { - std::lock_guard<std::recursive_mutex> guard(this->mutex); - if (destroyed) return; - if (window != nullptr && windows[window->index] != nullptr) { - auto metadata = reinterpret_cast<ManagedWindow*>(window); - inits[window->index] = false; - windows[window->index] = nullptr; - - if (metadata->status < WINDOW_CLOSING) { - window->close(0); - } - - if (metadata->status < WINDOW_KILLING) { - window->kill(); - } - - delete window; - } - } - - ManagedWindow* createWindow (WindowOptions opts) { - std::lock_guard<std::recursive_mutex> guard(this->mutex); - if (destroyed) return nullptr; - StringStream env; - - if (inits[opts.index] && windows[opts.index] != nullptr) { - return windows[opts.index]; - } - - if (opts.appData.size() > 0) { - for (auto const &envKey : parseStringList(opts.appData["build_env"])) { - auto cleanKey = trim(envKey); - - if (!Env::has(cleanKey)) { - continue; - } - - auto envValue = Env::get(cleanKey.c_str()); - - env << String( - cleanKey + "=" + encodeURIComponent(envValue) + "&" - ); - } - } else { - for (auto const &envKey : parseStringList(this->options.appData["build_env"])) { - auto cleanKey = trim(envKey); - - if (!Env::has(cleanKey)) { - continue; - } - - auto envValue = Env::get(cleanKey); - - env << String( - cleanKey + "=" + encodeURIComponent(envValue) + "&" - ); - } - } - - auto screen = Window::getScreenSize(); - - float width = opts.width <= 0 - ? Window::getSizeInPixels(this->options.defaultWidth, screen.width) - : opts.width; - float height = opts.height <= 0 - ? Window::getSizeInPixels(this->options.defaultHeight, screen.height) - : opts.height; - float minWidth = opts.minWidth <= 0 - ? Window::getSizeInPixels(this->options.defaultMinWidth, screen.width) - : opts.minWidth; - float minHeight = opts.minHeight <= 0 - ? Window::getSizeInPixels(this->options.defaultMinHeight, screen.height) - : opts.minHeight; - float maxWidth = opts.maxWidth <= 0 - ? Window::getSizeInPixels(this->options.defaultMaxWidth, screen.width) - : opts.maxWidth; - float maxHeight = opts.maxHeight <= 0 - ? Window::getSizeInPixels(this->options.defaultMaxHeight, screen.height) - : opts.maxHeight; - - WindowOptions windowOptions = { - .resizable = opts.resizable, - .frameless = opts.frameless, - .utility = opts.utility, - .canExit = opts.canExit, - .width = width, - .height = height, - .minWidth = minWidth, - .minHeight = minHeight, - .maxWidth = maxWidth, - .maxHeight = maxHeight, - .index = opts.index, - .debug = isDebugEnabled() || opts.debug, - .isTest = this->options.isTest, - .headless = this->options.headless || opts.headless || opts.appData["build_headless"] == "true", - - .cwd = this->options.cwd, - .title = opts.title.size() > 0 ? opts.title : "", - .url = opts.url.size() > 0 ? opts.url : "data:text/html,<html>", - .argv = this->options.argv, - .preload = opts.preload.size() > 0 ? opts.preload : "", - .env = env.str(), - .appData = this->options.appData - }; - - if (isDebugEnabled()) { - this->log("Creating Window#" + std::to_string(opts.index)); - } - - auto window = new ManagedWindow(*this, app, windowOptions); - - window->status = WindowStatus::WINDOW_CREATED; - window->onExit = this->options.onExit; - window->onMessage = this->options.onMessage; - - windows[opts.index] = window; - inits[opts.index] = true; - - return window; - } - - ManagedWindow* createDefaultWindow (WindowOptions opts) { - return createWindow(WindowOptions { - .resizable = opts.resizable, - .frameless = opts.frameless, - .utility = opts.utility, - .canExit = true, - .width = opts.width, - .height = opts.height, - .index = 0, - #ifdef PORT - .port = PORT, - #endif - .appData = opts.appData - }); - } - - JSON::Array json (std::vector<int> indices) { - auto i = 0; - JSON::Array result; - for (auto index : indices) { - auto window = getWindow(index); - if (window != nullptr) { - result[i++] = window->json(); - } - } - return result; - } - }; - - class Dialog { - public: - struct FileSystemPickerOptions { - enum class Type { Open, Save }; - bool directories = false; - bool multiple = false; - bool files = false; - Type type = Type::Open; - String contentTypes; - String defaultName; - String defaultPath; - String title; - }; - - - #if defined(__APPLE__) && TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - SSCUIPickerDelegate* uiPickerDelegate = nullptr; - Vector<String> delegatedResults; - std::mutex delegateMutex; - #endif - - Dialog (); - ~Dialog (); - - String showSaveFilePicker ( - const FileSystemPickerOptions& options - ); - - Vector<String> showOpenFilePicker ( - const FileSystemPickerOptions& options - ); - - Vector<String> showDirectoryPicker ( - const FileSystemPickerOptions& options - ); - - Vector<String> showFileSystemPicker ( - const FileSystemPickerOptions& options - ); + SharedPointer<Core> core = nullptr; + Atomic<bool> destroyed = false; + Mutex mutex; + + WindowManager (SharedPointer<Core> core); + WindowManager () = delete; + WindowManager (const WindowManager&) = delete; + ~WindowManager (); + + void destroy (); + void configure (const WindowManagerOptions& configuration); + + SharedPointer<ManagedWindow> getWindow (int index, const WindowStatus status); + SharedPointer<ManagedWindow> getWindow (int index); + SharedPointer<ManagedWindow> getWindowForClient (const IPC::Client& client); + SharedPointer<ManagedWindow> getWindowForBridge (const IPC::Bridge* bridge); + SharedPointer<ManagedWindow> getWindowForWebView (WebView* webview);; + SharedPointer<ManagedWindow> getOrCreateWindow (int index); + SharedPointer<ManagedWindow> getOrCreateWindow (int index, const Window::Options& options); + WindowStatus getWindowStatus (int index); + + SharedPointer<ManagedWindow> createWindow (const Window::Options& options); + SharedPointer<ManagedWindow> createDefaultWindow (const Window::Options& options); + + void destroyWindow (int index); + JSON::Array json (const Vector<int>& indices); + bool emit (const String& event, const JSON::Any& json = {}); }; - -#if defined(_WIN32) - using IEnvHandler = ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler; - using IConHandler = ICoreWebView2CreateCoreWebView2ControllerCompletedHandler; - using INavHandler = ICoreWebView2NavigationCompletedEventHandler; - using IRecHandler = ICoreWebView2WebMessageReceivedEventHandler; - using IArgs = ICoreWebView2WebMessageReceivedEventArgs; - - enum WINDOWCOMPOSITIONATTRIB { - WCA_UNDEFINED = 0, - WCA_NCRENDERING_ENABLED = 1, - WCA_NCRENDERING_POLICY = 2, - WCA_TRANSITIONS_FORCEDISABLED = 3, - WCA_ALLOW_NCPAINT = 4, - WCA_CAPTION_BUTTON_BOUNDS = 5, - WCA_NONCLIENT_RTL_LAYOUT = 6, - WCA_FORCE_ICONIC_REPRESENTATION = 7, - WCA_EXTENDED_FRAME_BOUNDS = 8, - WCA_HAS_ICONIC_BITMAP = 9, - WCA_THEME_ATTRIBUTES = 10, - WCA_NCRENDERING_EXILED = 11, - WCA_NCADORNMENTINFO = 12, - WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, - WCA_VIDEO_OVERLAY_ACTIVE = 14, - WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, - WCA_DISALLOW_PEEK = 16, - WCA_CLOAK = 17, - WCA_CLOAKED = 18, - WCA_ACCENT_POLICY = 19, - WCA_FREEZE_REPRESENTATION = 20, - WCA_EVER_UNCLOAKED = 21, - WCA_VISUAL_OWNER = 22, - WCA_HOLOGRAPHIC = 23, - WCA_EXCLUDED_FROM_DDA = 24, - WCA_PASSIVEUPDATEMODE = 25, - WCA_USEDARKMODECOLORS = 26, - WCA_LAST = 27 - }; - - struct WINDOWCOMPOSITIONATTRIBDATA { - WINDOWCOMPOSITIONATTRIB Attrib; - PVOID pvData; - SIZE_T cbData; - }; -#endif } #endif diff --git a/src/window/window.kt b/src/window/window.kt new file mode 100644 index 0000000000..fbf46b02d9 --- /dev/null +++ b/src/window/window.kt @@ -0,0 +1,394 @@ +// vim: set sw=2: +package socket.runtime.window + +import java.lang.ref.WeakReference + +import kotlin.concurrent.thread + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebSettings +import android.webkit.WebView +import android.widget.FrameLayout +import android.webkit.GeolocationPermissions +import android.webkit.PermissionRequest + +import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit + +import socket.runtime.app.App +import socket.runtime.core.console +import socket.runtime.core.WebChromeClient +import socket.runtime.ipc.Bridge +import socket.runtime.ipc.Message +import socket.runtime.window.WindowManagerActivity +import socket.runtime.window.Dialog + +import __BUNDLE_IDENTIFIER__.R + +/** + */ +data class Size (val width: Int, val height: Int); + +/** + */ +data class ScreenSize (val width: Int, val height: Int); + +/** + * A container for configuring a `Window` + */ +data class WindowOptions ( + var index: Int = 0, + var shouldExitApplicationOnClose: Boolean = false, + var headless: Boolean = false +) + +/** + * External JavaScript interface attached to the webview at + * `window.external` + */ +open class WindowWebViewUserMessageHandler (window: Window) { + val TAG = "WindowWebViewUserMessageHandler" + + val namespace = "external" + val window = window + + @android.webkit.JavascriptInterface + open fun postMessage (value: String): Boolean { + return this.postMessage(value, null) + } + + /** + * Low level external message handler + */ + @android.webkit.JavascriptInterface + open fun postMessage (value: String, bytes: ByteArray? = null): Boolean { + val message = Message(value) + + if (message.seq.length > 0 && bytes != null) { + this.window.bridge.buffers[message.seq] = bytes + } + + this.window.onMessage(value) + return true + } +} + +/** + */ +open class WindowWebChromeClient (val window: Window) : WebChromeClient() { + override fun onGeolocationPermissionsShowPrompt ( + origin: String, + callback: GeolocationPermissions.Callback + ) { + val app = App.getInstance() + val allowed = app.hasRuntimePermission("geolocation") + callback(origin, allowed, allowed) + } + + override fun onPermissionRequest (request: PermissionRequest) { + val resources = request.resources + var grants = mutableListOf<String>() + val app = App.getInstance() + + for (resource in resources) { + when (resource) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + if (app.hasRuntimePermission("microphone") || app.hasRuntimePermission("user_media")) { + grants.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE) + } + } + + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { + if (app.hasRuntimePermission("camera") || app.hasRuntimePermission("user_media")) { + grants.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE) + } + } + + // auto grant EME + PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> { + grants.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID) + } + } + } + + if (grants.size > 0) { + request.grant(grants.toTypedArray()) + } else { + request.deny() + } + } + + override fun onShowFileChooser ( + webview: android.webkit.WebView, + callback: android.webkit.ValueCallback<Array<android.net.Uri>>, + params: android.webkit.WebChromeClient.FileChooserParams + ): Boolean { + if (!super.onShowFileChooser(webview, callback, params)) { + return false + } + + val fragment = this.window.fragment + val activity = fragment.requireActivity() as WindowManagerActivity + val options = Dialog.FileSystemPickerOptions(params) + + activity.dialog.showFileSystemPicker(options, fun (uris: Array<Uri>) { + callback.onReceiveValue(uris) + }) + + return true + } +} + +/** + */ +open class Window (val fragment: WindowFragment) { + val userMessageHandler = WindowWebViewUserMessageHandler(this) + val activity = fragment.requireActivity() as WindowManagerActivity + val bridge = Bridge(fragment.index, activity, this) + val client = WindowWebChromeClient(this) + val index = fragment.index + var title = "" + + init { + val userMessageHandler = this.userMessageHandler + val bridge = this.bridge + val client = this.client + + fragment.webview.apply { + // clients + webViewClient = bridge + webChromeClient = client + + // external interface + addJavascriptInterface(userMessageHandler, "external") + } + + thread { + val pendingNavigationLocation = this.getPendingNavigationLocation(this.index) + if (pendingNavigationLocation.length > 0) { + this.navigate(pendingNavigationLocation) + } + } + } + + fun show () { + val fragment = this.fragment + val activity = fragment.activity + val manager = activity?.supportFragmentManager + activity?.runOnUiThread { + manager?.commit { + show(fragment) + } + } + } + + fun hide () { + val fragment = this.fragment + val activity = fragment.activity + val manager = activity?.supportFragmentManager + activity?.runOnUiThread { + manager?.commit { + hide(fragment) + } + } + } + + fun close () { + // TODO + } + + fun navigate (url: String) { + val fragment = this.fragment + val activity = fragment.activity + val webview = fragment.webview + activity?.runOnUiThread { + if (url.startsWith("socket://__BUNDLE_IDENTIFIER__")) { + webview.loadUrl(url.replace("socket:", "https:")) + } else { + webview.loadUrl(url) + } + } + } + + fun getPreloadUserScript (): String { + return this.getPreloadUserScript(this.index) + } + + fun getSize (): Size { + val webview = this.fragment.webview + return Size(webview.measuredWidth, webview.measuredHeight) + } + + fun setSize (width: Int, height: Int) { + return this.setSize(Size(width, height)) + } + + fun setSize (size: Size) { + val webview = this.fragment.webview + val layout = FrameLayout.LayoutParams(size.width, size.height) + webview.setLayoutParams(layout) + } + + fun handleApplicationURL (url: String) { + return this.handleApplicationURL(this.index, url) + } + + fun onReady () { + this.bridge.activity.runOnUiThread { + this.onReady(this.index) + } + } + + fun onMessage (value: String, bytes: ByteArray? = null) { + this.onMessage(this.index, value, bytes) + } + + @Throws(Exception::class) + external fun onReady (index: Int): Unit + + @Throws(Exception::class) + external fun onMessage (index: Int, value: String, bytes: ByteArray? = null): Unit + + @Throws(Exception::class) + external fun onEvaluateJavascriptResult (index: Int, token: String, result: String): Unit + + @Throws(Exception::class) + external fun getPendingNavigationLocation (index: Int): String + + @Throws(Exception::class) + external fun getPreloadUserScript (index: Int): String + + @Throws(Exception::class) + external fun handleApplicationURL (index: Int, url: String): Unit +} + +/** + * A fragment that contains a single `WebView`. A `WindowFragment` is managed + * by a `WindowFragmentManager` and holds a reference to a `SSC::ManagedWindow` + * instance managed by `SSC::WindowManager` in the native runtime. + */ +open class WindowFragment : Fragment(R.layout.web_view) { + companion object { + /** + * Factory for creating new `WindowFragment` instances from a `Bundle` + */ + fun newInstance (bundle: Bundle): WindowFragment { + return WindowFragment().apply { + arguments = bundle + } + } + + /** + * Factory for creating new `WindowFragment` instances from a `WindowOptions` + */ + fun newInstance (options: WindowOptions): WindowFragment { + return WindowFragment().apply { + arguments = bundleOf( + "index" to options.index, + "shouldExitApplicationOnClose" to options.shouldExitApplicationOnClose, + "headless" to options.headless + ) + } + } + } + + /** + * The options used to create this `WindowFragment`. + */ + open val options = WindowOptions() + + /** + * XXX(@jwerle) + */ + open var window: Window? = null + + /** + * XXX(@jwerle) + */ + open val index: Int get () = this.options.index + + /** + * A reference to the child `WebView` owned by this `WindowFragment` + */ + open lateinit var webview: WebView + + override fun onAttach (context: Context) { + super.onAttach(context) + } + + override fun onCreate (savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateView ( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onStart () { + super.onStart() + } + + override fun onStop () { + super.onStop() + } + + override fun onResume () { + super.onResume() + } + + override fun onPause () { + super.onPause() + } + + /** + * Called when the view was created and when the `WindowFragment` should + * assign the `WebView` instance. + */ + override fun onViewCreated (view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val arguments = this.arguments + val app = App.getInstance() + + if (arguments != null) { + this.options.index = arguments.getInt("index", this.options.index) + this.options.headless = arguments.getBoolean("headless", this.options.headless) + this.options.shouldExitApplicationOnClose = arguments.getBoolean( + "shouldExitApplicationOnClose", + this.options.shouldExitApplicationOnClose + ) + } + + this.webview = view.findViewById<WebView>(R.id.webview) + this.webview.apply { + // features + settings.setGeolocationEnabled(app.hasRuntimePermission("geolocation")) + settings.javaScriptCanOpenWindowsAutomatically = true + settings.javaScriptEnabled = true + settings.domStorageEnabled = app.hasRuntimePermission("data_access") + settings.databaseEnabled = app.hasRuntimePermission("data_access") + + settings.allowContentAccess = true + settings.allowFileAccess = true + + settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + } + + this.window = Window(this).apply { + onReady() + } + } +} diff --git a/test/socket.ini b/test/socket.ini index 8f81fae163..96e02c9592 100644 --- a/test/socket.ini +++ b/test/socket.ini @@ -15,6 +15,7 @@ env[] = HOME env[] = DEBUG env[] = SSC_ANDROID_CI env[] = SOCKET_DEBUG_IPC +env[] = SOCKET_RUNTIME_VM_DEBUG env[] = SOCKET_MODULE_PATH_PREFIX env[] = TEST_INJECTED_VARIABLE diff --git a/test/src/application-url-event.js b/test/src/application-url-event.js index 99c309d5ab..6b27df6beb 100644 --- a/test/src/application-url-event.js +++ b/test/src/application-url-event.js @@ -17,12 +17,12 @@ test('trigger from openExternal', async (t) => { } }) - const result = await window.openExternal({ value: expected }) - - if (result.err) { - return t.fail(result.err) + try { + const result = await window.openExternal({ value: expected }) + t.equal(result.url, expected, 'result.url === expected') + } catch (err) { + return t.fail(err) } - t.equal(result?.data.url, expected, 'result.data.url === expected') await pending }) diff --git a/test/src/application.js b/test/src/application.js index e20dd1ec49..66b16b88cd 100644 --- a/test/src/application.js +++ b/test/src/application.js @@ -9,6 +9,8 @@ import process from 'socket:process' // const DELTA = 28 let title = 'Socket Runtime JavaScript Tests' +// TODO(@chicoxyzzy): neither kill nor exit work so I use the counter workaround +let counter = 1 test('window.document.title', async (t) => { t.equal(window.document.title, title, 'window.document.title is correct') @@ -44,66 +46,64 @@ if (!['android'].includes(process.platform)) { } // FIXME: make it work on iOS/Android -if (!['android', 'ios'].includes(process.platform)) { - test('application.config', async (t) => { - const rawConfig = await readFile('socket.ini', 'utf8') - let prefix = '' - const lines = rawConfig.split('\n') - const config = [] - for (let line of lines) { - line = line.trim() - let [key, value] = line.split('=') - - if (line.length === 0 || line.startsWith(';') || line.startsWith('#')) { - continue - } +test.desktop('application.config', async (t) => { + const rawConfig = await readFile('socket.ini', 'utf8') + let prefix = '' + const lines = rawConfig.split('\n') + const config = [] + for (let line of lines) { + line = line.trim() + let [key, value] = line.split('=') + + if (line.length === 0 || line.startsWith(';') || line.startsWith('#')) { + continue + } - if (line.startsWith('[') && line.endsWith(']')) { - prefix = line.slice(1, -1) - continue - } + if (line.startsWith('[') && line.endsWith(']')) { + prefix = line.slice(1, -1) + continue + } - key = key.trim() - value = value.trim().replace(/^"/, '').replace(/"$/, '').replace('.') - config.push([prefix.length === 0 ? key : prefix + '_' + key, value]) + key = key.trim() + value = value.trim().replace(/^"/, '').replace(/"$/, '').replace('.') + config.push([prefix.length === 0 ? key : prefix + '_' + key, value]) + } + config.forEach(([key, value]) => { + switch (key) { + // boolean values + case 'build_headless': + case 'window_max_width': + case 'window_max_height': + case 'window_min_width': + case 'window_min_height': + case 'window_resizable': + case 'window_frameless': + case 'window_utility': + t.equal(application.config[key].toString(), value, `application.config.${key} is correct`) + break + case 'build_name': + t.ok(application.config[key].startsWith(value), `application.config.${key} is correct`) + break + case 'test-section_array[]': + t.ok([1, 2, 3].map(String).includes(value), 'test-section_array values are correct') + break + case 'test-section_subsection_key': + t.equal(application.config[key], 'value', 'test-section_subsection_key values are correct') + break + case '.subsection_key': // FIXME(@jwerle): INI parser above + t.ok(value, 'value', 'test-section.subsection_key == value') + break + default: + // skip various values that we cannot test until we have a valid INI parser in stdlib } - config.forEach(([key, value]) => { - switch (key) { - // boolean values - case 'build_headless': - case 'window_max_width': - case 'window_max_height': - case 'window_min_width': - case 'window_min_height': - case 'window_resizable': - case 'window_frameless': - case 'window_utility': - t.equal(application.config[key].toString(), value, `application.config.${key} is correct`) - break - case 'build_name': - t.ok(application.config[key].startsWith(value), `application.config.${key} is correct`) - break - case 'test-section_array[]': - t.ok([1, 2, 3].map(String).includes(value), 'test-section_array values are correct') - break - case 'test-section_subsection_key': - t.equal(application.config[key], 'value', 'test-section_subsection_key values are correct') - break - case '.subsection_key': // FIXME(@jwerle): INI parser above - t.ok(value, 'value', 'test-section.subsection_key == value') - break - default: - // skip various values that we cannot test until we have a valid INI parser in stdlib - } - t.throws( - () => { application.config[key] = 0 }, - // eslint-disable-next-line prefer-regex-literals - /(read\s?only property)|(not extensible)/, - `application.config.${key} is read-only` - ) - }) + t.throws( + () => { application.config[key] = 0 }, + // eslint-disable-next-line prefer-regex-literals + /(read\s?only property)|(not extensible)/, + `application.config.${key} is read-only` + ) }) -} +}) // FIXME: make it work on iOS/Windows if (!['android', 'ios', 'win32'].includes(process.platform)) { @@ -115,17 +115,15 @@ if (!['android', 'ios', 'win32'].includes(process.platform)) { } // FIXME: make it work on iOS/Android -if (!['android', 'ios'].includes(process.platform)) { - test('openExternal', async (t) => { - const currentWindow = await application.getCurrentWindow() - t.equal(typeof currentWindow.openExternal, 'function', 'openExternal is a function') - if (process.platform !== 'linux') { - const result = await currentWindow.openExternal('https://1.1.1.1') - // can't test results without browser - t.ok(result?.data, 'succesfully completes') - } - }) -} +test.desktop('openExternal', async (t) => { + const currentWindow = await application.getCurrentWindow() + t.equal(typeof currentWindow.openExternal, 'function', 'openExternal is a function') + if (process.platform !== 'linux') { + const result = await currentWindow.openExternal('https://1.1.1.1') + // can't test results without browser + t.ok(result.url, 'succesfully completes') + } +}) test('apllication.exit', async (t) => { t.equal(typeof application.exit, 'function', 'exit is a function') @@ -290,7 +288,7 @@ if (!['android', 'ios', 'win32'].includes(process.platform)) { t.ok(Object.values(windows).every((window) => window instanceof ApplicationWindow), 'values are all ApplicationWindow instances') t.equal(windows[0].index, 0, 'window index is correct') t.equal(windows[newWindow.index].index, newWindow.index, 'window index is correct') - newWindow.close() + await newWindow.close() }) test('application.getWindows without params', async (t) => { @@ -303,7 +301,7 @@ if (!['android', 'ios', 'win32'].includes(process.platform)) { t.ok(Object.values(windows).every((window) => window instanceof ApplicationWindow), 'values are all ApplicationWindow instances') t.equal(windows[0].index, 0, 'window index is correct') t.equal(windows[newWindow.index].index, newWindow.index, 'window index is correct') - newWindow.close() + await newWindow.close() }) test('application.getCurrentWindow', async (t) => { @@ -316,9 +314,6 @@ if (!['android', 'ios', 'win32'].includes(process.platform)) { t.equal(mainWindowStatus, ApplicationWindow.constants.WINDOW_SHOWN, 'status is correct') }) - // TODO(@chicoxyzzy): neither kill nor exit work so I use the counter workaround - let counter = 1 - test('window.close', async (t) => { const newWindow = await application.createWindow({ index: counter, path: 'frontend/index_no_js.html' }) counter++ @@ -413,7 +408,7 @@ if (!['android', 'ios', 'win32'].includes(process.platform)) { const { index, status } = await newWindow.navigate('frontend/index_no_js2.html') t.equal(index, newWindow.index, 'correct index is returned') t.equal(status, ApplicationWindow.constants.WINDOW_SHOWN, 'correct status is returned') - newWindow.close() + await newWindow.close() }) test('window.setBackgroundColor', async (t) => { @@ -421,7 +416,7 @@ if (!['android', 'ios', 'win32'].includes(process.platform)) { counter++ const { index } = await newWindow.setBackgroundColor({ red: 0, green: 0, blue: 0, alpha: 0 }) t.equal(index, newWindow.index, 'correct index is returned') - newWindow.close() + await newWindow.close() }) test('window.setContextMenu', async (t) => { diff --git a/test/src/child_process/index.js b/test/src/child_process/index.js new file mode 100644 index 0000000000..643667d098 --- /dev/null +++ b/test/src/child_process/index.js @@ -0,0 +1,78 @@ +import { spawn, exec } from 'socket:child_process' +import process from 'socket:process' +import test from 'socket:test' +import os from 'socket:os' + +test('child_process.spawn(command[,args[,options]])', async (t) => { + const command = 'ls' + const args = ['-la'] + const options = {} + + let hasDir = false + + const pending = [] + const child = spawn(command, args, options) + + if (/linux|darwin/i.test(os.platform())) { + pending.push(new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error('Timed out aiting for SIGCHLD signal')), + 1000 + ) + + process.once('SIGCHLD', (signal, code, message) => { + resolve() + clearTimeout(timeout) + }) + })) + } + + pending.push(new Promise((resolve, reject) => { + child.stdout.on('data', data => { + if (Buffer.from(data).toString().includes('child_process')) { + hasDir = true + } + }) + + child.on('exit', resolve) + child.on('error', reject) + })) + + await Promise.all(pending) + + t.ok(hasDir, 'the ls command ran and discovered the child_process directory') +}) + +test('child_process.exec(command[,options],callback)', async (t) => { + const pending = [] + + pending.push(new Promise((resolve, reject) => { + exec('ls -la', (err, stdout, stderr) => { + if (err) { + return reject(err) + } + + t.ok(stdout, 'there is stdout') + resolve() + }) + })) + + pending.push(new Promise((resolve, reject) => { + exec('ls /not/a/directory', (err, stdout, stderr) => { + if (err) { + return reject(err) + } + + t.ok(!stdout, 'there is no stdout') + t.ok(stderr, 'there is no stdout') + resolve() + }) + })) + + await Promise.all(pending) +}) + +test('await child_process.exec(command)', async (t) => { + const { stdout } = await exec('ls -la') + t.ok(stdout && stdout.length, 'stdout from await exec() has output') +}) diff --git a/test/src/commonjs.js b/test/src/commonjs.js index 988d7436da..1b6ecde92a 100644 --- a/test/src/commonjs.js +++ b/test/src/commonjs.js @@ -1,10 +1,10 @@ import { createRequire } from 'socket:module' const require = createRequire(import.meta.url) + require('./commonjs/') require('./commonjs/scope') require('./commonjs/globals') require('./commonjs/builtins') require('./commonjs/resolvers') require('./commonjs/node-modules') -require('./commonjs/special-top-level-await') diff --git a/test/src/commonjs/builtins.js b/test/src/commonjs/builtins.js index 11205c76e0..960ff2ed75 100644 --- a/test/src/commonjs/builtins.js +++ b/test/src/commonjs/builtins.js @@ -1,4 +1,4 @@ -const Buffer = require('buffer') +const { Buffer } = require('buffer') const console = require('console') const dgram = require('dgram') const dns = require('dns') @@ -17,51 +17,51 @@ test('builtins - buffer', (t) => { }) test('builtins - console', (t) => { - t.equal(typeof console, 'object', 'console is function') + t.equal(typeof console, 'object', 'console is object') t.equal(typeof console?.log, 'function', 'console.log is function') }) test('builtins - dgram', (t) => { - t.equal(typeof dgram, 'object', 'dgram is function') + t.equal(typeof dgram, 'object', 'dgram is object') t.equal(typeof dgram?.createSocket, 'function', 'dgram.createSocket is function') }) test('builtins - dns', (t) => { - t.equal(typeof dns, 'object', 'dns is function') + t.equal(typeof dns, 'object', 'dns is object') t.equal(typeof dns?.lookup, 'function', 'dns.lookup is function') }) test('builtins - events', (t) => { - t.equal(typeof events, 'object', 'events is function') + t.equal(typeof events, 'function', 'events is function') t.equal(typeof events?.EventEmitter, 'function', 'events.EventEmitter is function') }) test('builtins - fs', (t) => { - t.equal(typeof fs, 'object', 'fs is function') + t.equal(typeof fs, 'object', 'fs is object') t.equal(typeof fs?.readdir, 'function', 'fs.readdir is function') }) test('builtins - os', (t) => { - t.equal(typeof os, 'object', 'os is function') + t.equal(typeof os, 'object', 'os is object') t.equal(typeof os?.networkInterfaces, 'function', 'os.networkInterfaces is function') }) test('builtins - path', (t) => { - t.equal(typeof path, 'object', 'path is function') + t.equal(typeof path, 'object', 'path is object') t.equal(typeof path?.normalize, 'function', 'path.normalize is function') }) test('builtins - process', (t) => { - t.equal(typeof process, 'object', 'process is function') + t.equal(typeof process, 'object', 'process is object') t.equal(typeof process?.cwd, 'function', 'process.cwd is function') }) test('builtins - stream', (t) => { - t.equal(typeof stream, 'object', 'stream is function') + t.equal(typeof stream, 'function', 'stream is function') t.equal(typeof stream?.PassThrough, 'function', 'stream.PassThrough is function') }) test('builtins - util', (t) => { - t.equal(typeof util, 'object', 'util is function') + t.equal(typeof util, 'object', 'util is object') t.equal(typeof util?.format, 'function', 'util.format is function') }) diff --git a/test/src/commonjs/globals.js b/test/src/commonjs/globals.js index b8fede3e1d..52c93b42e2 100644 --- a/test/src/commonjs/globals.js +++ b/test/src/commonjs/globals.js @@ -2,9 +2,9 @@ const test = require('socket:test') test('commonjs - globals', (t) => { t.ok(process === require('socket:process'), 'process is global') - t.ok(typeof global === 'object' && global.console && global.process, 'global is global') - t.ok(module, 'module is global') - t.ok(exports, 'exports is global') - t.ok(__filename, '__filename is global') - t.ok(__dirname, '__filename is global') + t.ok(typeof global === 'object', 'global is global') + t.ok(typeof module === 'object', 'module is global') + t.ok(typeof exports === 'object' && exports === module.exports, 'exports is global') + t.ok(typeof __filename === 'string', '__filename is global') + t.ok(typeof __dirname === 'string', '__filename is global') }) diff --git a/test/src/commonjs/special-top-level-await.js b/test/src/commonjs/special-top-level-await.js deleted file mode 100644 index c96efe8564..0000000000 --- a/test/src/commonjs/special-top-level-await.js +++ /dev/null @@ -1,5 +0,0 @@ -const { test } = await import('socket:test') - -test('commonjs - special top level await', (t) => { - t.ok(true, 'works') -}) diff --git a/test/src/fs/index.js b/test/src/fs/index.js index dab1fad50e..a110e0623f 100644 --- a/test/src/fs/index.js +++ b/test/src/fs/index.js @@ -422,28 +422,32 @@ test('fs.readFile', async (t) => { t.ok(results.every(Boolean), 'fs.readFile(\'fixtures/file.json\')') }) +/* // TODO: ensure this is working as expected. Its not working like node @bcomnes // resolving to "/Users/userHomeDir/socket/test/fixtures/file.txt" on macos test('fs.readlink', async (t) => { await new Promise((resolve, reject) => { const link = path.join(FIXTURES, 'link.txt') - fs.readlink(link, (resolvedPath) => { + fs.readlink(link, (_, resolvedPath) => { t.ok(resolvedPath.endsWith('/file.txt'), 'link path matches the actual path') return resolve() }) }) }) +*/ +/* // TODO: ensure this is working as expected. Its not working like node @bcomnes test('fs.realpath', async (t) => { await new Promise((resolve, reject) => { const link = path.join(FIXTURES, 'link.txt') - fs.realpath(link, (resolvedPath) => { + fs.realpath(link, (_, resolvedPath) => { t.ok(resolvedPath.endsWith('/file.txt'), 'link path matches the actual path') return resolve() }) }) }) +*/ test('fs.rename', async (t) => { await new Promise((resolve, reject) => { diff --git a/test/src/fs/promises.js b/test/src/fs/promises.js index 2492ee3725..299b118da6 100644 --- a/test/src/fs/promises.js +++ b/test/src/fs/promises.js @@ -90,7 +90,7 @@ test('fs.promises.opendir', async (t) => { }) test('fs.promises.readdir', async (t) => { - const files = await fs.readdir(FIXTURES + 'directory') + const files = await fs.readdir(FIXTURES + 'directory', { withFileTypes: true }) t.ok(Array.isArray(files), 'array is returned') t.equal(files.length, 6, 'array contains 2 items') t.deepEqual(files.map(file => file.name), ['0', '1', '2', 'a', 'b', 'c'].map(name => `${name}.txt`), 'array contains files') diff --git a/test/src/hooks.js b/test/src/hooks.js index cc4aea35ca..f2c7f7c0d8 100644 --- a/test/src/hooks.js +++ b/test/src/hooks.js @@ -16,7 +16,7 @@ const callbacks = { } class TestIgnoredError extends Error { - [Symbol.for('socket.test.error.ignore')] = true + [Symbol.for('socket.runtime.test.error.ignore')] = true } hooks.onReady(callbacks.onReady) @@ -24,27 +24,22 @@ hooks.onLoad(callbacks.onLoad) hooks.onInit(callbacks.onInit) test('hooks - initial state', async (t) => { - t.ok(initial.isDocumentReady === false, 'isDocumentReady === false') - t.ok(initial.isRuntimeReady === false, 'isRuntimeReady === false') - t.ok(initial.isGlobalReady === false, 'isGlobalReady === false') - t.ok(initial.isReady === false, 'isReady === false') + t.ok(initial.isDocumentReady === true, 'isDocumentReady === false') + t.ok(initial.isRuntimeReady === true, 'isRuntimeReady === false') + t.ok(initial.isGlobalReady === true, 'isGlobalReady === false') + t.ok(initial.isReady === true, 'isReady === false') }) -test('hooks - properties after load', async (t) => { +test('hooks - properties', async (t) => { t.ok(hooks.global === globalThis, 'hooks.global') t.ok(hooks.document === globalThis.document, 'hooks.document') t.ok(hooks.window === globalThis.window, 'hooks.window') - t.ok(hooks.isDocumentReady === true, 'hooks.isDocumentReady === true') - t.ok(hooks.isGlobalReady === true, 'hooks.isGlobalReady === true') - t.ok(hooks.isRuntimeReady === true, 'hooks.isRuntimeReady === true') - t.ok(hooks.isReady === true, 'hooks.isReady === true') t.ok(typeof hooks.isOnline === 'boolean', 'typeof hooks.isOnline === boolean') t.ok(typeof hooks.isWorkerContext === 'boolean', 'typeof hooks.isWorkerContext === boolean') t.ok(typeof hooks.isWindowContext === 'boolean', 'typeof hooks.isWindowContext === boolean') }) test('hooks - callbacks called during load', async (t) => { - t.ok(callbacks.onReady.called === true, 'onReady called') t.ok(callbacks.onLoad.called === true, 'onLoad called') t.ok(callbacks.onInit.called === true, 'onInit called') }) diff --git a/test/src/index.html b/test/src/index.html index 7e0da1ac06..ac7c795c00 100644 --- a/test/src/index.html +++ b/test/src/index.html @@ -7,6 +7,7 @@ content=" default-src http://* https://* ipc://* file://* socket://* data://*; script-src socket: https: 'unsafe-eval'; + worker-src socket: blob: 'unsafe-inline' 'unsafe-eval'; " > <title>Socket Runtime JavaScript Tests diff --git a/test/src/index.js b/test/src/index.js index 275a8a628c..001a8d148f 100644 --- a/test/src/index.js +++ b/test/src/index.js @@ -25,3 +25,4 @@ import './router-resolution.js' import './mime.js' import './application-url-event.js' import './webassembly.js' +import './vm.js' diff --git a/test/src/ipc.js b/test/src/ipc.js index 7eff13da68..e9999b17aa 100644 --- a/test/src/ipc.js +++ b/test/src/ipc.js @@ -16,6 +16,7 @@ test('ipc exports', async (t) => { 'debug', 'default', 'emit', + 'findMessageTransfers', 'ERROR', 'kDebugEnabled', 'primordials', @@ -61,7 +62,7 @@ test('ipc constants', (t) => { t.equal(ipc.OK, 0) t.equal(ipc.ERROR, 1) t.equal(ipc.TIMEOUT, 32000) - t.equal(ipc.kDebugEnabled, Symbol.for('ipc.debug.enabled')) + t.equal(ipc.kDebugEnabled, Symbol.for('socket.runtime.ipc.debug.enabled')) }) test('ipc.debug', (t) => { @@ -110,12 +111,7 @@ if (process.platform !== 'ios' && process.platform !== 'android') { t.equal(err?.name, 'NotFoundError') // Make lower case to adjust for implementation differences. t.equal(err?.message.toLowerCase(), 'not found') - // win32 adds on the trailing slash in the URL. - if (process.platform === 'win32') { - t.ok(err?.url.startsWith('ipc://test/?foo=bar&index=0&seq=R')) - } else { - t.ok(err?.url.startsWith('ipc://test?foo=bar&index=0&seq=R')) - } + t.ok(err?.url.startsWith('ipc://test/?foo=bar&index=0&seq=R')) t.equal(err?.code, 'NOT_FOUND_ERR') }) } diff --git a/test/src/mime.js b/test/src/mime.js index 12302e3ce2..a68be15d0c 100644 --- a/test/src/mime.js +++ b/test/src/mime.js @@ -99,7 +99,7 @@ test('mime.lookup', async (t) => { const results = await mime.lookup(ext) const mimes = results.map((result) => result.mime) const i = intersection(mimes, expect) - t.ok(i.length > 0, `mime.lookup returns resuls for ${ext}`) + t.ok(i.length > 0, `mime.lookup returns results for ${ext}`) for (const result of results) { expect.includes(result) } @@ -113,6 +113,9 @@ test('verify internal database content type prefix', async (t) => { for (const entry of database.entries()) { allContentTypesStartWithDatabaseName = entry[1].startsWith(database.name + '/') + if (!allContentTypesStartWithDatabaseName) { + break + } } t.ok( diff --git a/test/src/network/index.js b/test/src/network/index.js new file mode 100644 index 0000000000..0001108404 --- /dev/null +++ b/test/src/network/index.js @@ -0,0 +1,29 @@ +import test from 'socket:test' +import { isIPv4 } from 'socket:ip' +import { network, Encryption } from 'socket:network' + +test('basic network constructor', async t => { + // eslint-disable-next-line + const sharedKey = await Encryption.createSharedKey('TEST') + const clusterId = await Encryption.createClusterId('TEST') + const peerId = await Encryption.createId() + const signingKeys = await Encryption.createKeyPair() + + const options = { + clusterId, + peerId, + signingKeys + } + + const socket = await network(options) + + await new Promise((resolve, reject) => { + socket.on('#ready', info => { + t.ok(isIPv4(info.address), 'got an ipv4 address') + t.ok(!isNaN(info.port), 'got a valid port') + t.equal(Buffer.from(info.peerId, 'hex').length, 32, 'valid peerid') + resolve() + }) + socket.on('#error', reject) + }) +}) diff --git a/test/src/os.js b/test/src/os.js index ae7f3e43dc..f4e0be48db 100644 --- a/test/src/os.js +++ b/test/src/os.js @@ -95,3 +95,7 @@ test('os.EOL', (t) => { t.equal(os.EOL, '\n') } }) + +test('os.homedir()', (t) => { + t.ok(typeof os.homedir() === 'string', 'os.homedir() returns a string') +}) diff --git a/test/src/path.js b/test/src/path.js index f993025181..06b29a6db9 100644 --- a/test/src/path.js +++ b/test/src/path.js @@ -27,6 +27,7 @@ test('path.posix.resolve', (t) => { t.equal(abd, cwd + ['a', 'b', 'd'].join('/'), 'path.posix.resolve() resolves path 4 components') t.equal(a___, cwd + 'a', 'path.posix.resolve() resolves path with 5 component') }) + test('path.posix.join', (t) => { t.equal(path.posix.join('a', 'b', 'c'), 'a/b/c', 'join(a, b, c)') t.equal(path.posix.join('a', 'b', 'c', '../d'), 'a/b/d', 'join(a, b, c, ../d)') @@ -261,3 +262,18 @@ test('path.relative', (t) => { t.equal(path.win32.relative('\\a\\b\\c', '\\a\\b\\c\\d'), 'd', 'd') t.equal(path.win32.relative('\\a\\b\\c', '\\a\\b\\c\\d\\e'), 'd\\e', 'd\\e') }) + +test('path - well known', (t) => { + t.ok(path.DOWNLOADS && typeof path.DOWNLOADS === 'string', 'path.DOWNLOADS') + t.ok(path.DOCUMENTS && typeof path.DOCUMENTS === 'string', 'path.DOCUMENTS') + t.ok(path.RESOURCES && typeof path.RESOURCES === 'string', 'path.RESOURCES') + t.ok(path.PICTURES && typeof path.PICTURES === 'string', 'path.PICTURES') + t.ok(path.DESKTOP && typeof path.DESKTOP === 'string', 'path.DESKTOP') + t.ok(path.VIDEOS && typeof path.VIDEOS === 'string', 'path.VIDEOS') + t.ok(path.CONFIG && typeof path.CONFIG === 'string', 'path.CONFIG') + t.ok(path.MUSIC && typeof path.MUSIC === 'string', 'path.MUSIC') + t.ok(path.HOME && typeof path.HOME === 'string', 'path.HOME') + t.ok(path.DATA && typeof path.DATA === 'string', 'path.DATA') + t.ok(path.LOG && typeof path.LOG === 'string', 'path.LOG') + t.ok(path.TMP && typeof path.TMP === 'string', 'path.TMP') +}) diff --git a/test/src/process.js b/test/src/process.js index a47b9293cd..12f51478fc 100644 --- a/test/src/process.js +++ b/test/src/process.js @@ -7,10 +7,6 @@ test('process', (t) => { t.ok(typeof process.addListener === 'function', 'process is an EventEmitter') }) -test('process.homedir()', (t) => { - t.ok(typeof process.homedir() === 'string', 'process.homedir() returns a string') -}) - test('process.exit()', (t) => { t.ok(typeof process.exit === 'function', 'process.exit() is a function') }) @@ -49,7 +45,9 @@ test('process.platform', (t) => { }) test('process.env', (t) => { - t.deepEqual(process.env, globalThis.__args.env, 'process.env is equal to globalThis.__args.env') + for (const key in globalThis.__args.env) { + t.equal(globalThis.__args.env[key], process.env[key], `globalThis.__args.env.${key} === process.env.${key}`) + } }) test('process.argv', (t) => { diff --git a/test/src/router-resolution.js b/test/src/router-resolution.js index 080ec34a56..94b5f6bfd9 100644 --- a/test/src/router-resolution.js +++ b/test/src/router-resolution.js @@ -79,10 +79,10 @@ test('router-resolution', async (t) => { const extractedRedirectURL = extractUrl(response, responseBody) const redirectResponse = await fetch(extractedRedirectURL) const redirectResponseBody = (await redirectResponse.text()).trim() - t.equal(redirectResponseBody, testCase.bodyTest, `Redirect response body matches ${testCase.bodyTest}`) + t.ok(redirectResponseBody.includes(testCase.bodyTest), `Redirect response body includes ${testCase.bodyTest}`) } } else { - t.equal(responseBody, testCase.bodyTest, `response body matches ${testCase.bodyTest}`) + t.ok(responseBody.includes(testCase.bodyTest), `response body includes ${testCase.bodyTest}`) } } }) diff --git a/test/src/util.js b/test/src/util.js index 7e80030242..d53ffbcff5 100644 --- a/test/src/util.js +++ b/test/src/util.js @@ -140,8 +140,6 @@ test('util.splitBuffer', (t) => { t.equal(c.toString(), 'ar', 'util.splitBuffer returns an array of buffers') }) -test('util.InvertedPromise', (t) => {}) - test('util.clamp', (t) => { t.equal(util.clamp(0, 0, 1), 0, 'util.clamp returns the lower bound if the value is less than the lower bound') t.equal(util.clamp(1, 0, 1), 1, 'util.clamp returns the upper bound if the value is greater than the upper bound') diff --git a/test/src/vm.js b/test/src/vm.js new file mode 100644 index 0000000000..3cdf6baa9d --- /dev/null +++ b/test/src/vm.js @@ -0,0 +1,96 @@ +import test from 'socket:test' +import vm from 'socket:vm' + +test('vm.runInContext(source) - simple', async (t) => { + t.equal(await vm.runInContext('1 + 2 + 3'), 6, 'vm.runInContext("1 + 2 + 3")') + t.deepEqual( + await vm.runInContext('{ number: 123 }'), + { number: 123 }, + 'vm.runInContext("{ number: 123 }")' + ) +}) + +test('vm.runInContext(source) - script refererences', async (t) => { + async function identity (...args) { + return args + } + + const result = await vm.runInContext(identity) + t.deepEqual(await result(1, 2, 3), [1, 2, 3], 'function reference returns value') + + const FunctionReference = await vm.runInContext('Function') + const fn = new FunctionReference('return 123') + + t.equal(123, await fn(), 'Function constructor with new call') + + const Class = await vm.runInContext('class Class { method (...args) { return args } }') + t.equal('function', typeof Class, 'Class constructor') + + const instance = new Class() + + t.deepEqual([1, 2, 3], await instance.method(1, 2, 3), 'class instance method') +}) + +test('vm.runInContext(source, context) - context', async (t) => { + const context = { + key: 'value', + functions: {} + } + + const value = await vm.runInContext('key', { context }) + t.equal(context.key, value, 'context.key === value') + + await vm.runInContext('key = "other value"', { context }) + t.equal(context.key, 'other value', 'context.key === "other value"') + + await vm.runInContext('functions.hello = () => "hello world"', { context }) + t.equal('function', typeof context.functions.hello, 'typeof context.function.hello === "function"') + t.equal('hello world', await context.functions.hello(), 'await context.function.hello() === "hello world"') +}) + +test('vm.runInContext(source, context) - transferables', async (t) => { + const channel = new MessageChannel() + const context = { + buffer: new TextEncoder().encode('hello world'), + scope: {} + } + + await vm.runInContext(` + let port = null + + scope.setMessagePort = (value) => { + port = value + port.onmessage = (event) => port.postMessage(event.data) + } + + scope.decoded = new TextDecoder().decode(buffer) + `, { context }) + + await context.scope.setMessagePort(channel.port2) + t.equal('hello world', context.scope.decoded, 'context.scope.decoded === "hello world"') + channel.port1.postMessage(context.scope.decoded) + await new Promise((resolve) => { + channel.port1.onmessage = (event) => { + t.equal('hello world', event.data, 'port1 message is "hello world"') + resolve() + } + }) +}) + +test('vm.runInContext(source, context) - ESM', async (t) => { + const module = await vm.runInContext(` + const storage = {} + export function set (key, value) { + storage[key] = value + } + + export function get (key) { + return storage[key] + } + `) + + t.equal('function', typeof module.set, 'typeof module.set === "function"') + t.equal('function', typeof module.get, 'typeof module.set === "function"') + await module.set('key', 'value') + t.equal('value', await module.get('key'), 'module.get("key") === "value"') +}) diff --git a/test/src/webview.js b/test/src/webview.js index b65f29f452..a85397e173 100644 --- a/test/src/webview.js +++ b/test/src/webview.js @@ -20,16 +20,6 @@ import { showSaveFilePicker } from 'socket:internal/pickers' -const loaded = new Promise((resolve) => { - globalThis.addEventListener('load', () => { - resolve(true) - }, { once: true }) -}) - -test('globalThis.onload', async (t) => { - t.ok(await loaded, 'globalThis.onload called') -}) - test('globalThis.isSocketRuntime', (t) => { t.ok(globalThis.isSocketRuntime === true, 'globalThis.isSocketRuntime') }) diff --git a/tsconfig.json b/tsconfig.json index 68d363d7da..731bf90f62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,9 @@ { - "include": ["api/*.js", "api/**/*.js", "api/**/**/*.js"], + "include": [ + "api/*.js", + "api/**/*.js", + "api/**/**/*.js" + ], "compilerOptions": { "baseUrl": "socket:", "target": "ES2022", @@ -14,6 +18,7 @@ "checkJs": false, "allowJs": true, "paths": { + "npm:*": ["node_modules/*"], "socket:*": ["api/*"] } }