diff --git a/docker-compose.yml b/docker-compose.yml index 560bd9c..e3c07a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: typesense: - image: typesense/typesense:27.0 + image: typesense/typesense:29.0 restart: on-failure ports: - "8108:8108" diff --git a/extension.yaml b/extension.yaml index 7cdaad6..99c4b3c 100644 --- a/extension.yaml +++ b/extension.yaml @@ -348,3 +348,17 @@ params: value: true default: false required: false + - param: TYPESENSE_CONNECTION_TIMEOUT_SECONDS + label: Typesense Connection Timeout (seconds) + description: >- + The timeout in seconds for connections to Typesense. This applies to both connection timeout. + type: string + example: "60" + required: false + - param: TYPESENSE_RETRY_INTERVAL_SECONDS + label: Typesense Retry Interval (seconds) + description: >- + The retry interval in seconds for connections to Typesense. This applies to both connection timeout. + type: string + example: "60" + required: false diff --git a/functions/src/config.js b/functions/src/config.js index 71a7b5f..91041b1 100644 --- a/functions/src/config.js +++ b/functions/src/config.js @@ -11,6 +11,8 @@ module.exports = { typesenseProtocol: process.env.TYPESENSE_PROTOCOL || "https", typesenseCollectionName: process.env.TYPESENSE_COLLECTION_NAME, typesenseAPIKey: process.env.TYPESENSE_API_KEY, + typesenseConnectionTimeoutSeconds: parseInt(process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS, 10), + typesenseRetryIntervalSeconds: parseInt(process.env.TYPESENSE_RETRY_INTERVAL_SECONDS, 10), typesenseBackfillTriggerDocumentInFirestore: "typesense_sync/backfill", typesenseBackfillBatchSize: 1000, }; diff --git a/functions/src/createTypesenseClient.js b/functions/src/createTypesenseClient.js index 887049c..2530443 100644 --- a/functions/src/createTypesenseClient.js +++ b/functions/src/createTypesenseClient.js @@ -7,6 +7,11 @@ function getRandomNumber(min, max) { } module.exports = function () { + const connectionTimeout = + !process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS || Number.isNaN(config.typesenseConnectionTimeoutSeconds) ? getRandomNumber(60, 90) : config.typesenseConnectionTimeoutSeconds; + + const retryInterval = !process.env.TYPESENSE_RETRY_INTERVAL_SECONDS || Number.isNaN(config.typesenseRetryIntervalSeconds) ? getRandomNumber(60, 120) : config.typesenseRetryIntervalSeconds; + return new Typesense.Client({ nodes: config.typesenseHosts.map((h) => { return { @@ -16,7 +21,7 @@ module.exports = function () { }; }), apiKey: config.typesenseAPIKey, - connectionTimeoutSeconds: getRandomNumber(60, 90), - retryIntervalSeconds: getRandomNumber(60, 120), + connectionTimeoutSeconds: connectionTimeout, + retryIntervalSeconds: retryInterval, }); }; diff --git a/package.json b/package.json index ee23fe8..e6e22cc 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "scripts": { "emulator": "cross-env DOTENV_CONFIG=extensions/test-params-flatten-nested-false.local.env firebase emulators:start --import=emulator_data", "export": "firebase emulators:export emulator_data", - "test": "npm run test:flatttened && npm run test:unflattened && npm run test:subcollection", + "test": "npm run test:flatttened && npm run test:unflattened && npm run test:subcollection && npm run test:typesenseClientDefaults", "test:flatttened": "cp -f extensions/test-params-flatten-nested-true.local.env functions/.env && cross-env NODE_OPTIONS=--experimental-vm-modules DOTENV_CONFIG=extensions/test-params-flatten-nested-true.local.env firebase emulators:exec --only functions,firestore,extensions 'jest --testRegex=\"WithFlattening\" --testRegex=\"backfill.spec\"'", "test:unflattened": "cp -f extensions/test-params-flatten-nested-false.local.env functions/.env && cross-env NODE_OPTIONS=--experimental-vm-modules DOTENV_CONFIG=extensions/test-params-flatten-nested-false.local.env firebase emulators:exec --only functions,firestore,extensions 'jest --testRegex=\"WithoutFlattening\"'", "test:subcollection": "jest --testRegex=\"writeLogging\" --testRegex=\"Subcollection\" --detectOpenHandles", + "test:typesenseClientDefaults": "jest --testRegex=\"typesenseClientDefaults\"", "typesenseServer": "docker compose up", "lint:fix": "eslint . --fix", "lint": "eslint .", diff --git a/test/typesenseClientDefaults.spec.js b/test/typesenseClientDefaults.spec.js new file mode 100644 index 0000000..c0e4ec7 --- /dev/null +++ b/test/typesenseClientDefaults.spec.js @@ -0,0 +1,115 @@ +describe("Typesense Client Default Random Configuration", () => { + let originalConnectionTimeout; + let originalRetryInterval; + + beforeEach(() => { + originalConnectionTimeout = process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS; + originalRetryInterval = process.env.TYPESENSE_RETRY_INTERVAL_SECONDS; + + jest.resetModules(); + + process.env.TYPESENSE_HOSTS = "localhost"; + process.env.TYPESENSE_API_KEY = "test-key"; + process.env.TYPESENSE_COLLECTION_NAME = "test-collection"; + }); + + afterEach(() => { + if (originalConnectionTimeout !== undefined) { + process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS = originalConnectionTimeout; + } else { + delete process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS; + } + + if (originalRetryInterval !== undefined) { + process.env.TYPESENSE_RETRY_INTERVAL_SECONDS = originalRetryInterval; + } else { + delete process.env.TYPESENSE_RETRY_INTERVAL_SECONDS; + } + + delete process.env.TYPESENSE_HOSTS; + delete process.env.TYPESENSE_API_KEY; + delete process.env.TYPESENSE_COLLECTION_NAME; + + jest.resetModules(); + }); + + describe("when environment variables are not set", () => { + beforeEach(() => { + delete process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS; + delete process.env.TYPESENSE_RETRY_INTERVAL_SECONDS; + + jest.resetModules(); + }); + + it("should use random connection timeout between 60 and 90 seconds", () => { + const createTypesenseClient = require("../functions/src/createTypesenseClient"); + const client = createTypesenseClient(); + + expect(client.configuration.connectionTimeoutSeconds).toBeGreaterThanOrEqual(60); + expect(client.configuration.connectionTimeoutSeconds).toBeLessThanOrEqual(90); + expect(Number.isInteger(client.configuration.connectionTimeoutSeconds)).toBe(true); + }); + + it("should use random retry interval between 60 and 120 seconds", () => { + const createTypesenseClient = require("../functions/src/createTypesenseClient"); + const client = createTypesenseClient(); + + expect(client.configuration.retryIntervalSeconds).toBeGreaterThanOrEqual(60); + expect(client.configuration.retryIntervalSeconds).toBeLessThanOrEqual(120); + expect(Number.isInteger(client.configuration.retryIntervalSeconds)).toBe(true); + }); + + it("should generate different random values on multiple client creations", () => { + const createTypesenseClient = require("../functions/src/createTypesenseClient"); + const clients = []; + const connectionTimeouts = new Set(); + const retryIntervals = new Set(); + + for (let i = 0; i < 10; i++) { + const client = createTypesenseClient(); + clients.push(client); + connectionTimeouts.add(client.configuration.connectionTimeoutSeconds); + retryIntervals.add(client.configuration.retryIntervalSeconds); + } + + expect(connectionTimeouts.size).toBeGreaterThan(1); + expect(retryIntervals.size).toBeGreaterThan(1); + }); + }); + + describe("when environment variables are set", () => { + beforeEach(() => { + process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS = "45"; + process.env.TYPESENSE_RETRY_INTERVAL_SECONDS = "30"; + + jest.resetModules(); + }); + + it("should use the configured values instead of random ones", () => { + const createTypesenseClient = require("../functions/src/createTypesenseClient"); + const client = createTypesenseClient(); + + expect(client.configuration.connectionTimeoutSeconds).toBe(45); + expect(client.configuration.retryIntervalSeconds).toBe(30); + }); + }); + + describe("when environment variables are set to invalid values", () => { + beforeEach(() => { + process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS = "invalid"; + process.env.TYPESENSE_RETRY_INTERVAL_SECONDS = "also-invalid"; + + jest.resetModules(); + }); + + it("should fall back to random values when parseInt returns NaN", () => { + const createTypesenseClient = require("../functions/src/createTypesenseClient"); + const client = createTypesenseClient(); + + expect(client.configuration.connectionTimeoutSeconds).toBeGreaterThanOrEqual(60); + expect(client.configuration.connectionTimeoutSeconds).toBeLessThanOrEqual(90); + expect(client.configuration.retryIntervalSeconds).toBeGreaterThanOrEqual(60); + expect(client.configuration.retryIntervalSeconds).toBeLessThanOrEqual(120); + }); + }); +});