From 6f2fb68b2da20596cdd806aee62cab54404a3bfb Mon Sep 17 00:00:00 2001 From: shum Date: Sat, 10 Oct 2026 12:27:31 +0000 Subject: [PATCH] nodejs: download prebuilt add-on at runtime --- packages/simplex-chat-nodejs/README.md | 8 +- .../docs/core.Class.ChatAPIError.md | 8 +- .../docs/core.Class.ChatInitError.md | 8 +- ...MigrationError.Interface.ErrorMigration.md | 8 +- ...rationError.Interface.ErrorNotADatabase.md | 6 +- ...ore.DBMigrationError.Interface.ErrorSQL.md | 8 +- ...tionError.Interface.InvalidConfirmation.md | 4 +- ...grationError.Interface.InvalidQueueSize.md | 4 +- .../core.DBMigrationError.TypeAlias.Tag.md | 2 +- .../core.Enumeration.MigrationConfirmation.md | 10 +-- .../docs/core.Function.chatCloseStore.md | 2 +- .../docs/core.Function.chatDecryptFile.md | 2 +- .../docs/core.Function.chatEncryptFile.md | 2 +- .../docs/core.Function.chatMigrateInit.md | 2 +- .../docs/core.Function.chatReadFile.md | 2 +- .../docs/core.Function.chatRecvMsgWait.md | 2 +- .../docs/core.Function.chatSendCmd.md | 2 +- .../docs/core.Function.chatWriteFile.md | 2 +- .../docs/core.Interface.APIResult.md | 6 +- .../docs/core.Interface.CryptoArgs.md | 6 +- .../docs/core.Interface.UpMigration.md | 6 +- .../core.MTRError.Interface.MTREDifferent.md | 6 +- .../core.MTRError.Interface.MTRENoDown.md | 6 +- .../docs/core.MTRError.TypeAlias.Tag.md | 2 +- ...re.MigrationError.Interface.MEDowngrade.md | 6 +- ...core.MigrationError.Interface.MEUpgrade.md | 6 +- ...MigrationError.Interface.MigrationError.md | 6 +- .../docs/core.MigrationError.TypeAlias.Tag.md | 2 +- .../docs/core.TypeAlias.DBMigrationError.md | 2 +- .../docs/core.TypeAlias.MTRError.md | 2 +- .../docs/core.TypeAlias.MigrationError.md | 2 +- packages/simplex-chat-nodejs/jest.config.js | 5 ++ packages/simplex-chat-nodejs/package.json | 7 +- packages/simplex-chat-nodejs/src/cli.ts | 5 +- packages/simplex-chat-nodejs/src/core.ts | 3 +- packages/simplex-chat-nodejs/src/libs.ts | 84 ++++++++++++++---- packages/simplex-chat-nodejs/src/simplex.d.ts | 2 +- packages/simplex-chat-nodejs/src/simplex.js | 23 ++++- .../simplex-chat-nodejs/tests/core.test.ts | 9 +- .../simplex-chat-nodejs/tests/libs.test.ts | 85 +++++++++++++++++-- .../simplex-chat-nodejs/tests/loader.test.ts | 16 +++- 41 files changed, 268 insertions(+), 111 deletions(-) diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md index 9faf3b9c0d..6ccd26fc05 100644 --- a/packages/simplex-chat-nodejs/README.md +++ b/packages/simplex-chat-nodejs/README.md @@ -55,9 +55,11 @@ If you cloned this repository, you can: cd ./packages/simplex-chat-nodejs npm install npm run build -node ./examples/squaring-bot-readme.js +SIMPLEX_ADDON_PATH=build/Release/simplex.node node ./examples/squaring-bot-readme.js ``` +Building the Node.js add-on from source needs [node-gyp](https://github.com/nodejs/node-gyp) build tools. + There is an example with more options in [./examples/squaring-bot.ts](./examples/squaring-bot.ts). You can run it with: `npx ts-node ./examples/squaring-bot.ts` @@ -66,14 +68,14 @@ A larger example, with a business address and commands as keys: [SimpleX Calcula ## Native library -`libsimplex` is downloaded on first use into the user cache (`$XDG_CACHE_HOME/simplex-chat` or `~/.cache/simplex-chat` on Linux, `~/Library/Caches/simplex-chat` on macOS, `%LOCALAPPDATA%\simplex-chat` on Windows), shared with the Python library. To download it ahead of time, for example in a Dockerfile, run as the user that runs the app: +`libsimplex` and the prebuilt Node.js add-on are downloaded on first use into the user cache (`$XDG_CACHE_HOME/simplex-chat` or `~/.cache/simplex-chat` on Linux, `~/Library/Caches/simplex-chat` on macOS, `%LOCALAPPDATA%\simplex-chat` on Windows), shared with the Python library. To download them ahead of time, for example in a Dockerfile, run as the user that runs the app: ```bash npx simplex-chat install # sqlite (default) npx simplex-chat install --backend postgres # Linux x86_64 only ``` -Set `SIMPLEX_LIBS_DIR` to a directory with a local build of `libsimplex` and its dependencies to use it instead; it must be built for the backend in use. +Set `SIMPLEX_LIBS_DIR` to a directory with a local build of `libsimplex` and its dependencies to use it instead; it must be built for the backend in use. Set `SIMPLEX_ADDON_PATH` to a locally built add-on (`build/Release/simplex.node`) to use it instead of the downloaded one. `ChatApi.init` loads the library for `DbConfig.type`; the low-level `core` functions require `core.loadLibrary(backend)` first. diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md index 690898c69a..64e3e764df 100644 --- a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md @@ -6,7 +6,7 @@ # Class: ChatAPIError -Defined in: [src/core.ts:116](../src/core.ts#L116) +Defined in: [src/core.ts:117](../src/core.ts#L117) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:116](../src/core.ts#L116) > **new ChatAPIError**(`message`, `chatError?`): `ChatAPIError` -Defined in: [src/core.ts:117](../src/core.ts#L117) +Defined in: [src/core.ts:118](../src/core.ts#L118) #### Parameters @@ -44,7 +44,7 @@ Defined in: [src/core.ts:117](../src/core.ts#L117) > **chatError**: `ChatError` \| `undefined` = `undefined` -Defined in: [src/core.ts:117](../src/core.ts#L117) +Defined in: [src/core.ts:118](../src/core.ts#L118) *** @@ -52,7 +52,7 @@ Defined in: [src/core.ts:117](../src/core.ts#L117) > **message**: `string` -Defined in: [src/core.ts:117](../src/core.ts#L117) +Defined in: [src/core.ts:118](../src/core.ts#L118) #### Inherited from diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md index bf62db5cc4..aefc4b9073 100644 --- a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md @@ -6,7 +6,7 @@ # Class: ChatInitError -Defined in: [src/core.ts:140](../src/core.ts#L140) +Defined in: [src/core.ts:141](../src/core.ts#L141) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:140](../src/core.ts#L140) > **new ChatInitError**(`message`, `dbMigrationError`): `ChatInitError` -Defined in: [src/core.ts:141](../src/core.ts#L141) +Defined in: [src/core.ts:142](../src/core.ts#L142) #### Parameters @@ -44,7 +44,7 @@ Defined in: [src/core.ts:141](../src/core.ts#L141) > **dbMigrationError**: [`DBMigrationError`](core.TypeAlias.DBMigrationError.md) -Defined in: [src/core.ts:141](../src/core.ts#L141) +Defined in: [src/core.ts:142](../src/core.ts#L142) *** @@ -52,7 +52,7 @@ Defined in: [src/core.ts:141](../src/core.ts#L141) > **message**: `string` -Defined in: [src/core.ts:141](../src/core.ts#L141) +Defined in: [src/core.ts:142](../src/core.ts#L142) #### Inherited from diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md index 7bc93312bf..9b6ad3c099 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md @@ -6,7 +6,7 @@ # Interface: ErrorMigration -Defined in: [src/core.ts:173](../src/core.ts#L173) +Defined in: [src/core.ts:174](../src/core.ts#L174) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:173](../src/core.ts#L173) > **dbFile**: `string` -Defined in: [src/core.ts:175](../src/core.ts#L175) +Defined in: [src/core.ts:176](../src/core.ts#L176) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:175](../src/core.ts#L175) > **migrationError**: [`MigrationError`](core.TypeAlias.MigrationError.md) -Defined in: [src/core.ts:176](../src/core.ts#L176) +Defined in: [src/core.ts:177](../src/core.ts#L177) *** @@ -34,7 +34,7 @@ Defined in: [src/core.ts:176](../src/core.ts#L176) > **type**: `"errorMigration"` -Defined in: [src/core.ts:174](../src/core.ts#L174) +Defined in: [src/core.ts:175](../src/core.ts#L175) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md index dfe51b323d..d54f59003f 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md @@ -6,7 +6,7 @@ # Interface: ErrorNotADatabase -Defined in: [src/core.ts:168](../src/core.ts#L168) +Defined in: [src/core.ts:169](../src/core.ts#L169) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:168](../src/core.ts#L168) > **dbFile**: `string` -Defined in: [src/core.ts:170](../src/core.ts#L170) +Defined in: [src/core.ts:171](../src/core.ts#L171) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:170](../src/core.ts#L170) > **type**: `"errorNotADatabase"` -Defined in: [src/core.ts:169](../src/core.ts#L169) +Defined in: [src/core.ts:170](../src/core.ts#L170) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md index 0e6a987f27..e76de932f6 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md @@ -6,7 +6,7 @@ # Interface: ErrorSQL -Defined in: [src/core.ts:179](../src/core.ts#L179) +Defined in: [src/core.ts:180](../src/core.ts#L180) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:179](../src/core.ts#L179) > **dbFile**: `string` -Defined in: [src/core.ts:181](../src/core.ts#L181) +Defined in: [src/core.ts:182](../src/core.ts#L182) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:181](../src/core.ts#L181) > **migrationSQLError**: `string` -Defined in: [src/core.ts:182](../src/core.ts#L182) +Defined in: [src/core.ts:183](../src/core.ts#L183) *** @@ -34,7 +34,7 @@ Defined in: [src/core.ts:182](../src/core.ts#L182) > **type**: `"errorSQL"` -Defined in: [src/core.ts:180](../src/core.ts#L180) +Defined in: [src/core.ts:181](../src/core.ts#L181) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md index 331c54b978..30157c808d 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md @@ -6,7 +6,7 @@ # Interface: InvalidConfirmation -Defined in: [src/core.ts:160](../src/core.ts#L160) +Defined in: [src/core.ts:161](../src/core.ts#L161) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:160](../src/core.ts#L160) > **type**: `"invalidConfirmation"` -Defined in: [src/core.ts:161](../src/core.ts#L161) +Defined in: [src/core.ts:162](../src/core.ts#L162) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md index a4bc17e92a..5264e4977f 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md @@ -6,7 +6,7 @@ # Interface: InvalidQueueSize -Defined in: [src/core.ts:164](../src/core.ts#L164) +Defined in: [src/core.ts:165](../src/core.ts#L165) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:164](../src/core.ts#L164) > **type**: `"invalidQueueSize"` -Defined in: [src/core.ts:165](../src/core.ts#L165) +Defined in: [src/core.ts:166](../src/core.ts#L166) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md index 590625f42d..d94d5893ad 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md @@ -8,4 +8,4 @@ > **Tag** = `"invalidConfirmation"` \| `"invalidQueueSize"` \| `"errorNotADatabase"` \| `"errorMigration"` \| `"errorSQL"` -Defined in: [src/core.ts:154](../src/core.ts#L154) +Defined in: [src/core.ts:155](../src/core.ts#L155) diff --git a/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md index a5a4897aed..cc2dea3901 100644 --- a/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md +++ b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md @@ -6,7 +6,7 @@ # Enumeration: MigrationConfirmation -Defined in: [src/core.ts:125](../src/core.ts#L125) +Defined in: [src/core.ts:126](../src/core.ts#L126) Migration confirmation mode @@ -16,7 +16,7 @@ Migration confirmation mode > **Console**: `"console"` -Defined in: [src/core.ts:128](../src/core.ts#L128) +Defined in: [src/core.ts:129](../src/core.ts#L129) *** @@ -24,7 +24,7 @@ Defined in: [src/core.ts:128](../src/core.ts#L128) > **Error**: `"error"` -Defined in: [src/core.ts:129](../src/core.ts#L129) +Defined in: [src/core.ts:130](../src/core.ts#L130) *** @@ -32,7 +32,7 @@ Defined in: [src/core.ts:129](../src/core.ts#L129) > **YesUp**: `"yesUp"` -Defined in: [src/core.ts:126](../src/core.ts#L126) +Defined in: [src/core.ts:127](../src/core.ts#L127) *** @@ -40,4 +40,4 @@ Defined in: [src/core.ts:126](../src/core.ts#L126) > **YesUpDown**: `"yesUpDown"` -Defined in: [src/core.ts:127](../src/core.ts#L127) +Defined in: [src/core.ts:128](../src/core.ts#L128) diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md index f8a10c4276..69be845ed3 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md @@ -8,7 +8,7 @@ > **chatCloseStore**(`ctrl`): `Promise`\<`void`\> -Defined in: [src/core.ts:41](../src/core.ts#L41) +Defined in: [src/core.ts:42](../src/core.ts#L42) Close chat store diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md index 4925e29dd2..a2a901ddc5 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md @@ -8,7 +8,7 @@ > **chatDecryptFile**(`fromPath`, `__namedParameters`, `toPath`): `Promise`\<`void`\> -Defined in: [src/core.ts:97](../src/core.ts#L97) +Defined in: [src/core.ts:98](../src/core.ts#L98) Decrypt file diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md index 79c2ddab26..8f4c7815c4 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md @@ -8,7 +8,7 @@ > **chatEncryptFile**(`ctrl`, `fromPath`, `toPath`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> -Defined in: [src/core.ts:89](../src/core.ts#L89) +Defined in: [src/core.ts:90](../src/core.ts#L90) Encrypt file diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md index 30c73c5c37..c6ad146aa1 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md @@ -8,7 +8,7 @@ > **chatMigrateInit**(`dbPath`, `dbKey`, `confirm`, `queueSize?`): `Promise`\<`bigint`\> -Defined in: [src/core.ts:29](../src/core.ts#L29) +Defined in: [src/core.ts:30](../src/core.ts#L30) Initialize chat controller diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md index 6c88d336b8..f9413c4d74 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md @@ -8,7 +8,7 @@ > **chatReadFile**(`path`, `__namedParameters`): `Promise`\<`Buffer`\<`ArrayBufferLike`\>\> -Defined in: [src/core.ts:82](../src/core.ts#L82) +Defined in: [src/core.ts:83](../src/core.ts#L83) Read buffer from encrypted file diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md index 672e5390a3..8aaa2e1590 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md @@ -8,7 +8,7 @@ > **chatRecvMsgWait**(`ctrl`, `wait`): `Promise`\<`ChatEvent` \| `undefined`\> -Defined in: [src/core.ts:61](../src/core.ts#L61) +Defined in: [src/core.ts:62](../src/core.ts#L62) Receive chat event diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md index 1416eaaec1..9f8c34be0f 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md @@ -8,7 +8,7 @@ > **chatSendCmd**(`ctrl`, `cmd`): `Promise`\<`ChatResponse`\> -Defined in: [src/core.ts:49](../src/core.ts#L49) +Defined in: [src/core.ts:50](../src/core.ts#L50) Send chat command as string diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md index 905f4df0d0..bd22e9d2c6 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md @@ -8,7 +8,7 @@ > **chatWriteFile**(`ctrl`, `path`, `buffer`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> -Defined in: [src/core.ts:74](../src/core.ts#L74) +Defined in: [src/core.ts:75](../src/core.ts#L75) Write buffer to encrypted file diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md index 2079e9f4a5..63b0caba85 100644 --- a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md +++ b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md @@ -6,7 +6,7 @@ # Interface: APIResult\ -Defined in: [src/core.ts:111](../src/core.ts#L111) +Defined in: [src/core.ts:112](../src/core.ts#L112) ## Type Parameters @@ -20,7 +20,7 @@ Defined in: [src/core.ts:111](../src/core.ts#L111) > `optional` **error?**: `ChatError` -Defined in: [src/core.ts:113](../src/core.ts#L113) +Defined in: [src/core.ts:114](../src/core.ts#L114) *** @@ -28,4 +28,4 @@ Defined in: [src/core.ts:113](../src/core.ts#L113) > `optional` **result?**: `R` -Defined in: [src/core.ts:112](../src/core.ts#L112) +Defined in: [src/core.ts:113](../src/core.ts#L113) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md index 29e29b416b..939b3f7430 100644 --- a/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md +++ b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md @@ -6,7 +6,7 @@ # Interface: CryptoArgs -Defined in: [src/core.ts:135](../src/core.ts#L135) +Defined in: [src/core.ts:136](../src/core.ts#L136) File encryption key and nonce @@ -16,7 +16,7 @@ File encryption key and nonce > **fileKey**: `string` -Defined in: [src/core.ts:136](../src/core.ts#L136) +Defined in: [src/core.ts:137](../src/core.ts#L137) *** @@ -24,4 +24,4 @@ Defined in: [src/core.ts:136](../src/core.ts#L136) > **fileNonce**: `string` -Defined in: [src/core.ts:137](../src/core.ts#L137) +Defined in: [src/core.ts:138](../src/core.ts#L138) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md index d7ef4e236b..30a7f0b060 100644 --- a/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md +++ b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md @@ -6,7 +6,7 @@ # Interface: UpMigration -Defined in: [src/core.ts:214](../src/core.ts#L214) +Defined in: [src/core.ts:215](../src/core.ts#L215) ## Properties @@ -14,7 +14,7 @@ Defined in: [src/core.ts:214](../src/core.ts#L214) > **upName**: `string` -Defined in: [src/core.ts:215](../src/core.ts#L215) +Defined in: [src/core.ts:216](../src/core.ts#L216) *** @@ -22,4 +22,4 @@ Defined in: [src/core.ts:215](../src/core.ts#L215) > **withDown**: `boolean` -Defined in: [src/core.ts:216](../src/core.ts#L216) +Defined in: [src/core.ts:217](../src/core.ts#L217) diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md index 2acfea33d6..aaaa506f1e 100644 --- a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md @@ -6,7 +6,7 @@ # Interface: MTREDifferent -Defined in: [src/core.ts:235](../src/core.ts#L235) +Defined in: [src/core.ts:236](../src/core.ts#L236) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:235](../src/core.ts#L235) > **downMigrations**: `string`[] -Defined in: [src/core.ts:237](../src/core.ts#L237) +Defined in: [src/core.ts:238](../src/core.ts#L238) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:237](../src/core.ts#L237) > **type**: `"different"` -Defined in: [src/core.ts:236](../src/core.ts#L236) +Defined in: [src/core.ts:237](../src/core.ts#L237) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md index 2c614c7662..e088536be9 100644 --- a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md @@ -6,7 +6,7 @@ # Interface: MTRENoDown -Defined in: [src/core.ts:230](../src/core.ts#L230) +Defined in: [src/core.ts:231](../src/core.ts#L231) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:230](../src/core.ts#L230) > **dbMigrations**: `string`[] -Defined in: [src/core.ts:232](../src/core.ts#L232) +Defined in: [src/core.ts:233](../src/core.ts#L233) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:232](../src/core.ts#L232) > **type**: `"noDown"` -Defined in: [src/core.ts:231](../src/core.ts#L231) +Defined in: [src/core.ts:232](../src/core.ts#L232) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md index a818dcb97a..058c64d396 100644 --- a/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md @@ -8,4 +8,4 @@ > **Tag** = `"noDown"` \| `"different"` -Defined in: [src/core.ts:224](../src/core.ts#L224) +Defined in: [src/core.ts:225](../src/core.ts#L225) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md index dbc4ce2363..523fe8fe1e 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md @@ -6,7 +6,7 @@ # Interface: MEDowngrade -Defined in: [src/core.ts:203](../src/core.ts#L203) +Defined in: [src/core.ts:204](../src/core.ts#L204) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:203](../src/core.ts#L203) > **downMigrations**: `string`[] -Defined in: [src/core.ts:205](../src/core.ts#L205) +Defined in: [src/core.ts:206](../src/core.ts#L206) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:205](../src/core.ts#L205) > **type**: `"downgrade"` -Defined in: [src/core.ts:204](../src/core.ts#L204) +Defined in: [src/core.ts:205](../src/core.ts#L205) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md index e73c0421f8..c5856f1943 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md @@ -6,7 +6,7 @@ # Interface: MEUpgrade -Defined in: [src/core.ts:198](../src/core.ts#L198) +Defined in: [src/core.ts:199](../src/core.ts#L199) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:198](../src/core.ts#L198) > **type**: `"upgrade"` -Defined in: [src/core.ts:199](../src/core.ts#L199) +Defined in: [src/core.ts:200](../src/core.ts#L200) #### Overrides @@ -30,4 +30,4 @@ Defined in: [src/core.ts:199](../src/core.ts#L199) > **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md)[] -Defined in: [src/core.ts:200](../src/core.ts#L200) +Defined in: [src/core.ts:201](../src/core.ts#L201) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md index f8effcbce9..f3f7d9bc1f 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md @@ -6,7 +6,7 @@ # Interface: MigrationError -Defined in: [src/core.ts:208](../src/core.ts#L208) +Defined in: [src/core.ts:209](../src/core.ts#L209) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:208](../src/core.ts#L208) > **mtrError**: [`MTRError`](core.TypeAlias.MTRError.md) -Defined in: [src/core.ts:210](../src/core.ts#L210) +Defined in: [src/core.ts:211](../src/core.ts#L211) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:210](../src/core.ts#L210) > **type**: `"migrationError"` -Defined in: [src/core.ts:209](../src/core.ts#L209) +Defined in: [src/core.ts:210](../src/core.ts#L210) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md index f434bbfb12..92f649e615 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md @@ -8,4 +8,4 @@ > **Tag** = `"upgrade"` \| `"downgrade"` \| `"migrationError"` -Defined in: [src/core.ts:192](../src/core.ts#L192) +Defined in: [src/core.ts:193](../src/core.ts#L193) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md index 9394337fe0..159ce87391 100644 --- a/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md @@ -8,4 +8,4 @@ > **DBMigrationError** = [`InvalidConfirmation`](core.DBMigrationError.Interface.InvalidConfirmation.md) \| [`InvalidQueueSize`](core.DBMigrationError.Interface.InvalidQueueSize.md) \| [`ErrorNotADatabase`](core.DBMigrationError.Interface.ErrorNotADatabase.md) \| [`ErrorMigration`](core.DBMigrationError.Interface.ErrorMigration.md) \| [`ErrorSQL`](core.DBMigrationError.Interface.ErrorSQL.md) -Defined in: [src/core.ts:146](../src/core.ts#L146) +Defined in: [src/core.ts:147](../src/core.ts#L147) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md index 447c32eaef..1c4ceb16e9 100644 --- a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md @@ -8,4 +8,4 @@ > **MTRError** = [`MTRENoDown`](core.MTRError.Interface.MTRENoDown.md) \| [`MTREDifferent`](core.MTRError.Interface.MTREDifferent.md) -Defined in: [src/core.ts:219](../src/core.ts#L219) +Defined in: [src/core.ts:220](../src/core.ts#L220) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md index b59c335af3..485311f0de 100644 --- a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md @@ -8,4 +8,4 @@ > **MigrationError** = [`MEUpgrade`](core.MigrationError.Interface.MEUpgrade.md) \| [`MEDowngrade`](core.MigrationError.Interface.MEDowngrade.md) \| [`MigrationError`](core.MigrationError.Interface.MigrationError.md) -Defined in: [src/core.ts:186](../src/core.ts#L186) +Defined in: [src/core.ts:187](../src/core.ts#L187) diff --git a/packages/simplex-chat-nodejs/jest.config.js b/packages/simplex-chat-nodejs/jest.config.js index 18c5ddf0e5..8f37dfe843 100644 --- a/packages/simplex-chat-nodejs/jest.config.js +++ b/packages/simplex-chat-nodejs/jest.config.js @@ -1,3 +1,8 @@ +const path = require("path") + +// Tests load the locally built add-on instead of downloading the released one. +process.env.SIMPLEX_ADDON_PATH ??= path.join(__dirname, "build", "Release", "simplex.node") + module.exports = { preset: "ts-jest", maxWorkers: 1, diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 7e07bb0849..ac4161eccf 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -8,15 +8,12 @@ }, "files": [ "src", - "cpp", "dist", - "binding.gyp", "tsconfig.json", "docs", "examples" ], "scripts": { - "install": "node-gyp configure; node-gyp rebuild --release", "install-tools": "npm install -g node-gyp", "configure": "node-gyp configure", "build": "node-gyp rebuild && tsc && cp ./src/simplex.* ./dist", @@ -28,13 +25,13 @@ "dependencies": { "@simplex-chat/types": "^0.11.4", "extract-zip": "^2.0.1", - "fast-deep-equal": "^3.1.3", - "node-addon-api": "^8.5.0" + "fast-deep-equal": "^3.1.3" }, "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^25.0.5", "jest": "^30.2.0", + "node-addon-api": "^8.5.0", "ts-jest": "^29.4.6", "typedoc": "^0.28.15", "typedoc-plugin-markdown": "^4.9.0", diff --git a/packages/simplex-chat-nodejs/src/cli.ts b/packages/simplex-chat-nodejs/src/cli.ts index 369c0dd106..8f24e5a89d 100644 --- a/packages/simplex-chat-nodejs/src/cli.ts +++ b/packages/simplex-chat-nodejs/src/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import {parseArgs} from "util" import type {Backend} from "./core" -import {resolveLibsDir} from "./libs" +import {resolveAddonPath, resolveLibsDir} from "./libs" const USAGE = "usage: simplex-chat install [--backend sqlite|postgres]" const EXIT_INSTALL_FAILED = 1 @@ -27,8 +27,9 @@ export async function main(argv: string[]): Promise { return EXIT_USAGE } try { - const dir = await resolveLibsDir(backend) + const [dir, addon] = await Promise.all([resolveLibsDir(backend), resolveAddonPath()]) console.log(`libsimplex installed at: ${dir}`) + console.log(`Node.js add-on installed at: ${addon}`) return 0 } catch (e) { console.error(`install failed: ${(e as Error).message}`) diff --git a/packages/simplex-chat-nodejs/src/core.ts b/packages/simplex-chat-nodejs/src/core.ts index c9ac3642f0..5c246cf5c1 100644 --- a/packages/simplex-chat-nodejs/src/core.ts +++ b/packages/simplex-chat-nodejs/src/core.ts @@ -15,7 +15,8 @@ export function loadLibrary(backend: Backend): Promise { if (loading.backend === backend) return loading.promise return Promise.reject(new Error(`libsimplex already loaded with backend=${loading.backend}; cannot switch to ${backend} in the same process`)) } - const promise = libs.resolveLibsDir(backend).then(dir => simplex.load(libs.libPath(dir))) + const promise = Promise.all([libs.resolveLibsDir(backend), libs.resolveAddonPath()]) + .then(([dir, addon]) => simplex.load(addon, libs.libPath(dir))) loading = {backend, promise} // a failed download or load can be retried promise.catch(() => { loading = undefined }) diff --git a/packages/simplex-chat-nodejs/src/libs.ts b/packages/simplex-chat-nodejs/src/libs.ts index ad83b514a4..beeba394fc 100644 --- a/packages/simplex-chat-nodejs/src/libs.ts +++ b/packages/simplex-chat-nodejs/src/libs.ts @@ -45,9 +45,21 @@ export function cacheRoot(platform: string, env: NodeJS.ProcessEnv, home: string return path.join(env.XDG_CACHE_HOME || path.join(home, ".cache"), "simplex-chat") } +function releaseUrl(asset: string): string { + return `https://github.com/${GITHUB_REPO}/releases/download/v${LIBS_VERSION}/${asset}` +} + export function libsUrl(backend: Backend, tag: string): string { const suffix = backend === "postgres" ? "-postgres" : "" - return `https://github.com/${GITHUB_REPO}/releases/download/v${LIBS_VERSION}/simplex-chat-libs-${tag}${suffix}.zip` + return releaseUrl(`simplex-chat-libs-${tag}${suffix}.zip`) +} + +function addonName(tag: string): string { + return `simplex-chat-nodejs-${tag}.node` +} + +export function addonUrl(tag: string): string { + return releaseUrl(addonName(tag)) } export function libPath(dir: string): string { @@ -75,34 +87,70 @@ export async function resolveLibsDir( return target } -export async function installLibs(url: string, target: string, lib: string, timeoutMs: number = REQUEST_TIMEOUT_MS): Promise { - const parent = path.dirname(target) - await fs.promises.mkdir(parent, {recursive: true}) - const tmp = await fs.promises.mkdtemp(path.join(parent, ".download-")) - try { +export async function resolveAddonPath( + env: NodeJS.ProcessEnv = process.env, + platform: string = process.platform, + arch: string = process.arch +): Promise { + if (env.SIMPLEX_ADDON_PATH) { + const file = path.resolve(env.SIMPLEX_ADDON_PATH) + if (!fs.existsSync(file)) throw new Error(`SIMPLEX_ADDON_PATH does not exist: ${file}`) + return file + } + const tag = platformTag(platform, arch) + const target = path.resolve(cacheRoot(platform, env), `v${LIBS_VERSION}`, addonName(tag)) + if (!fs.existsSync(target)) await installAddon(addonUrl(tag), target) + return target +} + +export function installLibs(url: string, target: string, lib: string, timeoutMs: number = REQUEST_TIMEOUT_MS): Promise { + return inTempDir(target, async tmp => { console.error(`Downloading libsimplex from ${url} ...`) const zipPath = path.join(tmp, "libs.zip") await download(url, zipPath, timeoutMs) await extract(zipPath, {dir: tmp}) const extracted = path.join(tmp, "libs") if (!fs.existsSync(path.join(extracted, lib))) throw new Error(`libs/${lib} missing from ${url}`) - try { - await fs.promises.rename(extracted, target) - } catch (e) { - // Another process installed the same version first; its files are identical. - // Windows reports renaming onto an existing directory as EPERM. - const code = (e as NodeJS.ErrnoException).code - const lost = (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM") && fs.existsSync(target) - if (!lost) throw e - if (!fs.existsSync(path.join(target, lib))) { - throw new Error(`another process partially populated ${target} but libsimplex is missing; remove the directory manually and retry`) - } - } + await renameInto(extracted, target, path.join(target, lib)) + }) +} + +export function installAddon(url: string, target: string, timeoutMs: number = REQUEST_TIMEOUT_MS): Promise { + return inTempDir(target, async tmp => { + console.error(`Downloading the Node.js add-on from ${url} ...`) + const file = path.join(tmp, path.basename(target)) + await download(url, file, timeoutMs) + await renameInto(file, target, target) + }) +} + +// The temp dir is next to the target, so the final rename stays on one filesystem. +async function inTempDir(target: string, install: (tmp: string) => Promise): Promise { + const parent = path.dirname(target) + await fs.promises.mkdir(parent, {recursive: true}) + const tmp = await fs.promises.mkdtemp(path.join(parent, ".download-")) + try { + await install(tmp) } finally { await fs.promises.rm(tmp, {recursive: true, force: true}) } } +async function renameInto(from: string, target: string, installed: string): Promise { + try { + await fs.promises.rename(from, target) + } catch (e) { + // Another process installed the same version first; its files are identical. + // Windows reports renaming onto an existing directory or a loaded file as EPERM. + const code = (e as NodeJS.ErrnoException).code + const lost = (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM") && fs.existsSync(target) + if (!lost) throw e + if (!fs.existsSync(installed)) { + throw new Error(`another process partially populated ${target} but ${path.basename(installed)} is missing; remove it manually and retry`) + } + } +} + function download(url: string, dest: string, timeoutMs: number, redirects = 0): Promise { return new Promise((resolve, reject) => { const get = url.startsWith("https:") ? https.get : http.get diff --git a/packages/simplex-chat-nodejs/src/simplex.d.ts b/packages/simplex-chat-nodejs/src/simplex.d.ts index d7575a8677..0187556afd 100644 --- a/packages/simplex-chat-nodejs/src/simplex.d.ts +++ b/packages/simplex-chat-nodejs/src/simplex.d.ts @@ -1,6 +1,6 @@ // These functions are defined in CPP add-on ../cpp/simplex.cc -export function load(libPath: string): void +export function load(addonPath: string, libPath: string): void export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]> export function chat_migrate_init_queue(dbPath: string, dbKey: string, confirm: string, queueSize: number): Promise<[bigint, string]> export function chat_close_store(ctrl: bigint): Promise diff --git a/packages/simplex-chat-nodejs/src/simplex.js b/packages/simplex-chat-nodejs/src/simplex.js index fd26c67438..3bdfb528df 100644 --- a/packages/simplex-chat-nodejs/src/simplex.js +++ b/packages/simplex-chat-nodejs/src/simplex.js @@ -1 +1,22 @@ -module.exports = require("../build/Release/simplex") +// The add-on is downloaded from the libsimplex release, so it is loaded at runtime by core.loadLibrary. +const NOT_LOADED = "libsimplex is not loaded, call core.loadLibrary(backend) first" +const BINDINGS = ["chat_migrate_init", "chat_migrate_init_queue", "chat_close_store", "chat_send_cmd", "chat_recv_msg_wait", + "chat_write_file", "chat_read_file", "chat_encrypt_file", "chat_decrypt_file"] + +let addon + +exports.load = (addonPath, libPath) => { + if (!addon) { + const addonModule = {exports: {}} + process.dlopen(addonModule, addonPath) + addon = addonModule.exports + } + addon.load(libPath) +} + +for (const name of BINDINGS) { + exports[name] = (...args) => { + if (!addon) throw new Error(NOT_LOADED) + return addon[name](...args) + } +} diff --git a/packages/simplex-chat-nodejs/tests/core.test.ts b/packages/simplex-chat-nodejs/tests/core.test.ts index 0a1bcc08df..6f0d7ad21f 100644 --- a/packages/simplex-chat-nodejs/tests/core.test.ts +++ b/packages/simplex-chat-nodejs/tests/core.test.ts @@ -7,9 +7,12 @@ import * as simplex from "../src/simplex"; describe("Core tests", () => { let libPath: string; + let addonPath: string; // the first run downloads libsimplex beforeAll(async () => { - libPath = libs.libPath(await libs.resolveLibsDir("sqlite")); + const libsDir = await libs.resolveLibsDir("sqlite"); + libPath = libs.libPath(libsDir); + addonPath = await libs.resolveAddonPath(); await core.loadLibrary("sqlite"); }, 300000); const tmpDir = "./tests/tmp"; @@ -200,11 +203,11 @@ describe("Core tests", () => { }, 10000); it("should accept loading libsimplex again from the same path", () => { - expect(() => simplex.load(libPath)).not.toThrow(); + expect(() => simplex.load(addonPath, libPath)).not.toThrow(); }); it("should refuse to load libsimplex from another path", () => { - expect(() => simplex.load(path.resolve("other", "libsimplex.so"))).toThrow(`libsimplex already loaded from ${libPath}`); + expect(() => simplex.load(addonPath, path.resolve("other", "libsimplex.so"))).toThrow(`libsimplex already loaded from ${libPath}`); }); it("should load libsimplex after a failed load and in several workers", () => { diff --git a/packages/simplex-chat-nodejs/tests/libs.test.ts b/packages/simplex-chat-nodejs/tests/libs.test.ts index 2e0615f6c1..a31cb6a2f2 100644 --- a/packages/simplex-chat-nodejs/tests/libs.test.ts +++ b/packages/simplex-chat-nodejs/tests/libs.test.ts @@ -4,14 +4,14 @@ import * as https from "https" import * as os from "os" import * as path from "path" import {AddressInfo} from "net" -import {cacheRoot, installLibs, LIBS_VERSION, libPath, libsUrl, MAX_REDIRECTS, platformTag, resolveLibsDir} from "../src/libs" +import {addonUrl, cacheRoot, installAddon, installLibs, LIBS_VERSION, libPath, libsUrl, MAX_REDIRECTS, platformTag, resolveAddonPath, resolveLibsDir} from "../src/libs" import {main, parseInstallArgs} from "../src/cli" import * as libs from "../src/libs" import {storedZip} from "./zip" const LIB = "libtest.so" const LINUX_LIB = "libsimplex.so" -const {XDG_CACHE_HOME, LOCALAPPDATA, SIMPLEX_LIBS_DIR} = process.env +const {XDG_CACHE_HOME, LOCALAPPDATA, SIMPLEX_LIBS_DIR, SIMPLEX_ADDON_PATH} = process.env function setEnv(name: string, value: string | undefined): void { if (value === undefined) delete process.env[name] @@ -27,12 +27,14 @@ beforeEach(() => { setEnv("XDG_CACHE_HOME", home) setEnv("LOCALAPPDATA", home) setEnv("SIMPLEX_LIBS_DIR", undefined) + setEnv("SIMPLEX_ADDON_PATH", undefined) }) afterEach(() => { jest.restoreAllMocks() setEnv("XDG_CACHE_HOME", XDG_CACHE_HOME) setEnv("LOCALAPPDATA", LOCALAPPDATA) setEnv("SIMPLEX_LIBS_DIR", SIMPLEX_LIBS_DIR) + setEnv("SIMPLEX_ADDON_PATH", SIMPLEX_ADDON_PATH) fs.rmSync(home, {recursive: true, force: true}) }) @@ -75,6 +77,7 @@ describe("paths", () => { const release = `https://github.com/simplex-chat/simplex-chat-libs/releases/download/v${LIBS_VERSION}` expect(libsUrl("sqlite", "macos-aarch64")).toBe(`${release}/simplex-chat-libs-macos-aarch64.zip`) expect(libsUrl("postgres", "linux-x86_64")).toBe(`${release}/simplex-chat-libs-linux-x86_64-postgres.zip`) + expect(addonUrl("windows-x86_64")).toBe(`${release}/simplex-chat-nodejs-windows-x86_64.node`) }) it("resolves an absolute SIMPLEX_LIBS_DIR", async () => { @@ -104,6 +107,17 @@ describe("paths", () => { await expect(resolveLibsDir("sqlite", {SIMPLEX_LIBS_DIR: libsDir}, platform, arch)).rejects.toThrow(`SIMPLEX_LIBS_DIR has no ${lib}: `) }) + it("returns a relative SIMPLEX_ADDON_PATH as absolute", async () => { + const addon = path.join(libsDir, "simplex.node") + fs.writeFileSync(addon, "addon") + await expect(resolveAddonPath({SIMPLEX_ADDON_PATH: path.relative(process.cwd(), addon)})).resolves.toBe(addon) + }) + + it("rejects a missing SIMPLEX_ADDON_PATH", async () => { + const addon = path.join(libsDir, "simplex.node") + await expect(resolveAddonPath({SIMPLEX_ADDON_PATH: addon})).rejects.toThrow(`SIMPLEX_ADDON_PATH does not exist: ${addon}`) + }) + it("rejects a SIMPLEX_LIBS_DIR without libsimplex", async () => { fs.rmSync(libPath(libsDir)) await expect(resolveLibsDir("sqlite", {SIMPLEX_LIBS_DIR: libsDir})).rejects.toThrow(`SIMPLEX_LIBS_DIR has no ${path.basename(libPath(libsDir))}: ${libsDir}`) @@ -246,6 +260,41 @@ describe("installLibs", () => { expect(fs.readFileSync(path.join(dir, LINUX_LIB), "utf8")).toBe("lib") }) + it("downloads the add-on into the cache", async () => { + routes["/addon.node"] = res => res.writeHead(200).end("addon") + const requested = mockHttps("/addon.node") + const addon = await resolveAddonPath({XDG_CACHE_HOME: tmp}, "linux", "x64") + const version = path.join(tmp, "simplex-chat", `v${LIBS_VERSION}`) + expect(requested).toEqual([addonUrl("linux-x86_64")]) + expect(addon).toBe(path.join(version, "simplex-chat-nodejs-linux-x86_64.node")) + expect(fs.readFileSync(addon, "utf8")).toBe("addon") + expect(fs.readdirSync(version)).toEqual(["simplex-chat-nodejs-linux-x86_64.node"]) + }) + + it("does not download a cached add-on", async () => { + const requested = mockHttps("/addon.node") + const cached = path.join(cacheRoot("darwin", {}), `v${LIBS_VERSION}`, "simplex-chat-nodejs-macos-aarch64.node") + fs.mkdirSync(path.dirname(cached), {recursive: true}) + fs.writeFileSync(cached, "addon") + await expect(resolveAddonPath({}, "darwin", "arm64")).resolves.toBe(cached) + expect(requested).toEqual([]) + }) + + it("rejects a missing add-on and cleans up", async () => { + const target = path.join(tmp, "simplex.node") + await expect(installAddon(`${base}/missing.node`, target)).rejects.toThrow("HTTP 404") + expect(fs.readdirSync(tmp)).toEqual([]) + }) + + it("accepts EPERM from rename onto an installed add-on", async () => { + routes["/addon.node"] = res => res.writeHead(200).end("addon") + const target = path.join(tmp, "simplex.node") + fs.writeFileSync(target, "addon") + renameFails("EPERM") + await expect(installAddon(`${base}/addon.node`, target)).resolves.toBeUndefined() + expect(leftovers()).toEqual([]) + }) + it("does not download a cached lib", async () => { const requested = mockHttps("/good.zip") const cached = path.join(tmp, "simplex-chat", `v${LIBS_VERSION}`, "postgres") @@ -296,8 +345,9 @@ describe("installLibs", () => { }) describe("cli", () => { - function installWithLibsDir(dir: string): Promise { - setEnv("SIMPLEX_LIBS_DIR", dir) + function installWith(libsDir: string, addon: string): Promise { + setEnv("SIMPLEX_LIBS_DIR", libsDir) + setEnv("SIMPLEX_ADDON_PATH", addon) return main(["install"]) } @@ -313,13 +363,29 @@ describe("cli", () => { expect(() => parseInstallArgs([])).toThrow("expected command: install") }) - it("prints the libs directory", async () => { + it("prints the libs directory and the add-on", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "libs-cli-")) + const addon = path.join(dir, "simplex.node") + fs.writeFileSync(libPath(dir), "lib") + fs.writeFileSync(addon, "addon") + const log = jest.spyOn(console, "log").mockImplementation(() => {}) + try { + await expect(installWith(dir, addon)).resolves.toBe(0) + expect(log.mock.calls).toEqual([[`libsimplex installed at: ${dir}`], [`Node.js add-on installed at: ${addon}`]]) + } finally { + fs.rmSync(dir, {recursive: true, force: true}) + } + }) + + it("returns 1 without the add-on", async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "libs-cli-")) fs.writeFileSync(libPath(dir), "lib") const log = jest.spyOn(console, "log").mockImplementation(() => {}) + const error = jest.spyOn(console, "error").mockImplementation(() => {}) try { - await expect(installWithLibsDir(dir)).resolves.toBe(0) - expect(log).toHaveBeenCalledWith(`libsimplex installed at: ${dir}`) + await expect(installWith(dir, path.join(dir, "simplex.node"))).resolves.toBe(1) + expect(log).not.toHaveBeenCalled() + expect(error).toHaveBeenCalledWith(expect.stringContaining("install failed: SIMPLEX_ADDON_PATH does not exist")) } finally { fs.rmSync(dir, {recursive: true, force: true}) } @@ -327,9 +393,11 @@ describe("cli", () => { it("returns 1 when install fails", async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "libs-cli-")) + const addon = path.join(dir, "simplex.node") + fs.writeFileSync(addon, "addon") const error = jest.spyOn(console, "error").mockImplementation(() => {}) try { - await expect(installWithLibsDir(dir)).resolves.toBe(1) + await expect(installWith(dir, addon)).resolves.toBe(1) expect(error).toHaveBeenCalledWith(expect.stringContaining("install failed: SIMPLEX_LIBS_DIR has no")) } finally { fs.rmSync(dir, {recursive: true, force: true}) @@ -338,6 +406,7 @@ describe("cli", () => { it("installs the requested backend", async () => { const resolve = jest.spyOn(libs, "resolveLibsDir").mockResolvedValue("/libs") + jest.spyOn(libs, "resolveAddonPath").mockResolvedValue("/addon.node") jest.spyOn(console, "log").mockImplementation(() => {}) await expect(main(["install", "--backend", "postgres"])).resolves.toBe(0) expect(resolve).toHaveBeenCalledWith("postgres") diff --git a/packages/simplex-chat-nodejs/tests/loader.test.ts b/packages/simplex-chat-nodejs/tests/loader.test.ts index 167d65258f..6a03fe8feb 100644 --- a/packages/simplex-chat-nodejs/tests/loader.test.ts +++ b/packages/simplex-chat-nodejs/tests/loader.test.ts @@ -2,13 +2,14 @@ import {spawnSync} from "child_process" import * as path from "path" jest.mock("../src/simplex", () => ({load: jest.fn()})) -jest.mock("../src/libs", () => ({libPath: jest.requireActual("../src/libs").libPath, resolveLibsDir: jest.fn()})) +jest.mock("../src/libs", () => ({libPath: jest.requireActual("../src/libs").libPath, resolveLibsDir: jest.fn(), resolveAddonPath: jest.fn()})) const PACKAGE_DIR = path.join(__dirname, "..") const ADDON = path.join(PACKAGE_DIR, "build", "Release", "simplex.node") const CHILD_TIMEOUT_MS = 10000 const LIBS_DIR = path.resolve("libs") const SWITCH_ERROR = "libsimplex already loaded with backend=sqlite; cannot switch to postgres in the same process" +const ADDON_PATH = path.resolve("simplex.node") const {libPath} = jest.requireActual("../src/libs") function runAddon(script: string): string { @@ -19,9 +20,11 @@ function runAddon(script: string): string { function freshCore(): {core: typeof import("../src/core"), resolveLibsDir: jest.Mock, load: jest.Mock} { let modules!: ReturnType jest.isolateModules(() => { + const libs = require("../src/libs") + libs.resolveAddonPath.mockResolvedValue(ADDON_PATH) modules = { core: require("../src/core"), - resolveLibsDir: require("../src/libs").resolveLibsDir, + resolveLibsDir: libs.resolveLibsDir, load: require("../src/simplex").load, } }) @@ -56,6 +59,13 @@ describe("native load", () => { }) }) +describe("add-on module", () => { + it("rejects FFI calls before the add-on is loaded", () => { + const simplex = jest.requireActual("../src/simplex") + expect(() => simplex.chat_send_cmd(BigInt(1), "/v")).toThrow("libsimplex is not loaded, call core.loadLibrary(backend) first") + }) +}) + describe("loadLibrary", () => { it("shares one load between concurrent calls", async () => { const {core, resolveLibsDir, load} = freshCore() @@ -65,7 +75,7 @@ describe("loadLibrary", () => { expect(second).toBe(first) await Promise.all([first, second]) expect(resolveLibsDir).toHaveBeenCalledTimes(1) - expect(load.mock.calls).toEqual([[libPath(LIBS_DIR)]]) + expect(load.mock.calls).toEqual([[ADDON_PATH, libPath(LIBS_DIR)]]) }) it("refuses to switch backend while the first load is in progress", async () => {