diff --git a/package-lock.json b/package-lock.json index 05c5a566..1b7d54ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "nice-grpc": "^2.1.14", "nice-grpc-client-middleware-retry": "^3.1.13", "nice-grpc-common": "^2.0.2", + "nice-grpc-web": "^3.3.10", "uuid": "^14.0.0" }, "devDependencies": { @@ -34,6 +35,7 @@ "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "babel-jest": "^29.4.3", + "buffer": "^6.0.3", "eslint": "^8.35.0", "eslint-config-prettier": "^8.7.0", "eslint-plugin-prettier": "^4.2.1", @@ -6488,6 +6490,15 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "dev": true, @@ -7176,6 +7187,12 @@ "node": ">=10" } }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -8052,13 +8069,26 @@ "license": "MIT" }, "node_modules/nice-grpc-common": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz", - "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.3.tgz", + "integrity": "sha512-MEhnD3JMah0mgyivpb9hpRDbOBuXBxI/TVO+OK1h6rC97WM42HsPMR+zzRNQ0C5BqYJTw1nyWiQRD0DucO+pjQ==", + "license": "MIT", "dependencies": { "ts-error": "^1.0.6" } }, + "node_modules/nice-grpc-web": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/nice-grpc-web/-/nice-grpc-web-3.3.10.tgz", + "integrity": "sha512-8XyCtbs7uL0mWQEjpkjZy57bnLbtheRbIWgj8p98XPbjFqXOBkTLu+ebj5H4fUu/yxqt+6ULPPDHT/icUsyieA==", + "license": "MIT", + "dependencies": { + "abort-controller-x": "^0.5.0", + "isomorphic-ws": "^5.0.0", + "js-base64": "^3.7.2", + "nice-grpc-common": "^2.0.3" + } + }, "node_modules/nice-grpc/node_modules/abort-controller-x": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz", @@ -11548,7 +11578,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index be07ff32..d13832c9 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,19 @@ "require": "./dist/node/cjs/index.js", "default": "./dist/node/esm/index.js" } + }, + "./web": { + "types": { + "require": "./dist/web/index.web.d.cts", + "default": "./dist/web/index.web.d.ts" + }, + "default": { + "require": "./dist/web/index.web.cjs", + "default": "./dist/web/index.web.js" + } } }, + "browser": "./dist/web/index.web.js", "engines": { "node": ">=22.0.0" }, @@ -61,6 +72,7 @@ "nice-grpc": "^2.1.14", "nice-grpc-client-middleware-retry": "^3.1.13", "nice-grpc-common": "^2.0.2", + "nice-grpc-web": "^3.3.10", "uuid": "^14.0.0" }, "devDependencies": { @@ -78,6 +90,7 @@ "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "babel-jest": "^29.4.3", + "buffer": "^6.0.3", "eslint": "^8.35.0", "eslint-config-prettier": "^8.7.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/src/alias/index.ts b/src/alias/index.ts index e910d8cf..93da9358 100644 --- a/src/alias/index.ts +++ b/src/alias/index.ts @@ -1,4 +1,4 @@ -import { ConnectionREST } from '../index.js'; +import { ConnectionREST } from '../connection/index.js'; import { WeaviateAlias, WeaviateAliasResponse } from '../openapi/types.js'; import { Alias, AliasListAllOptions, CreateAliasArgs, UpdateAliasArgs } from './types.js'; diff --git a/src/collections/aggregate/index.ts b/src/collections/aggregate/index.ts index 9060ad8d..4ecbaf48 100644 --- a/src/collections/aggregate/index.ts +++ b/src/collections/aggregate/index.ts @@ -7,7 +7,8 @@ import { FilterValue } from '../filters/index.js'; import { WeaviateInvalidInputError, WeaviateQueryError } from '../../errors.js'; import { Aggregator } from '../../graphql/index.js'; -import { PrimitiveKeys, toBase64FromMedia } from '../../index.js'; +import type { PrimitiveKeys } from '../../index.js'; +import { toBase64FromMedia } from '../../utils/base64.js'; import { Deserialize } from '../deserialize/index.js'; import { Bm25OperatorOptions, Bm25QueryProperty, NearVectorInputType, TargetVector } from '../query/types.js'; import { NearVectorInputGuards } from '../query/utils.js'; diff --git a/src/collections/data/batch.ts b/src/collections/data/batch.ts index 26baea63..66afde06 100644 --- a/src/collections/data/batch.ts +++ b/src/collections/data/batch.ts @@ -1,6 +1,7 @@ import { Deque } from '@datastructures-js/deque'; import { v4 as uuidv4 } from 'uuid'; import Connection from '../../connection/grpc.js'; +import { WeaviateUnsupportedFeatureError } from '../../errors.js'; import { ConsistencyLevel } from '../../index.js'; import { BatchObject as BatchObjectGRPC, @@ -66,6 +67,11 @@ export interface Batch { export default function (connection: Connection, dbVersionSupport: DbVersionSupport): Batch { return { stream: async (consistencyLevel) => { + if (!connection.supportsStreaming()) { + throw new WeaviateUnsupportedFeatureError( + 'Streaming batch (batch.stream / data.ingest) is not supported over gRPC-Web. Use data.insertMany instead.' + ); + } const { supports, message } = await dbVersionSupport.supportsServerSideBatching(); if (!supports) { throw new Error(message); diff --git a/src/collections/generate/index.ts b/src/collections/generate/index.ts index 0fdcfd6d..58f739ab 100644 --- a/src/collections/generate/index.ts +++ b/src/collections/generate/index.ts @@ -4,9 +4,9 @@ import { ConsistencyLevel } from '../../data/index.js'; import { DbVersionSupport } from '../../utils/dbVersion.js'; import { WeaviateInvalidInputError } from '../../errors.js'; -import { toBase64FromMedia } from '../../index.js'; import { GenerativeSearch } from '../../proto/v1/generative.js'; import { SearchReply } from '../../proto/v1/search_get.js'; +import { toBase64FromMedia } from '../../utils/base64.js'; import { Deserialize } from '../deserialize/index.js'; import { Check } from '../query/check.js'; import { CallOptions } from '../query/index.js'; diff --git a/src/collections/serialize/index.ts b/src/collections/serialize/index.ts index 04413230..026db514 100644 --- a/src/collections/serialize/index.ts +++ b/src/collections/serialize/index.ts @@ -71,7 +71,6 @@ import { SearchNearVectorArgs, SearchNearVideoArgs, } from '../../grpc/searcher.js'; -import { toBase64FromMedia } from '../../index.js'; import { AggregateRequest_Aggregation, AggregateRequest_Aggregation_Boolean, @@ -97,6 +96,7 @@ import { Vectors as VectorsGrpc, Vectors_VectorType, } from '../../proto/v1/base.js'; +import { toBase64FromMedia } from '../../utils/base64.js'; import { yieldToEventLoop } from '../../utils/yield.js'; import { FilterId } from '../filters/classes.js'; import { FilterValue, Filters } from '../filters/index.js'; diff --git a/src/connection/grpc.ts b/src/connection/grpc.ts index bae6c7de..f646027e 100644 --- a/src/connection/grpc.ts +++ b/src/connection/grpc.ts @@ -5,19 +5,9 @@ import { getClientVersionHeader, InternalConnectionParams } from './http.js'; import { ConsistencyLevel } from '../data/index.js'; -import { - ChannelCredentials, - ChannelOptions, - ClientError, - createChannel, - createClientFactory, - Metadata, - Status, -} from 'nice-grpc'; -import { retryMiddleware } from 'nice-grpc-client-middleware-retry'; +import { ClientError, Metadata, Status } from 'nice-grpc-common'; -import { HealthCheckResponse_ServingStatus, HealthDefinition } from '../proto/google/health/v1/health.js'; -import { WeaviateDefinition } from '../proto/v1/weaviate.js'; +import { HealthCheckResponse_ServingStatus } from '../proto/google/health/v1/health.js'; import Batcher, { Batch } from '../grpc/batcher.js'; import Searcher, { Search } from '../grpc/searcher.js'; @@ -27,14 +17,14 @@ import { DbVersionSupport, initDbVersionProvider } from '../utils/dbVersion.js'; import { WeaviateGRPCUnavailableError, WeaviateUnsupportedFeatureError } from '../errors.js'; import Aggregator, { Aggregate } from '../grpc/aggregator.js'; import { Meta } from '../openapi/types.js'; +import { GrpcTransport } from './transports/types.js'; export interface GrpcConnectionParams extends InternalConnectionParams { grpcAddress: string; grpcSecure: boolean; + transport?: GrpcTransport; } -const clientFactory = createClientFactory().use(retryMiddleware); - const MAX_GRPC_MESSAGE_LENGTH = 104858000; // 10mb, needs to be synchronized with GRPC server // Must extend from ConnectionGQL so that it can be passed to all the builder methods, @@ -156,6 +146,8 @@ export default class ConnectionGRPC extends ConnectionGQL { return new Promise((resolve) => resolve(this.grpc.tenants(collection))); }; + public supportsStreaming = (): boolean => this.params.transport?.supportsStreaming ?? true; + close = () => { this.grpc.close(); this.http.close(); @@ -188,23 +180,10 @@ export interface GrpcClient { } export const grpcClient = (config: GrpcConnectionParams & { grpcMaxMessageLength: number }): GrpcClient => { - const channelOptions: ChannelOptions = { - 'grpc.max_send_message_length': config.grpcMaxMessageLength, - 'grpc.max_receive_message_length': config.grpcMaxMessageLength, - }; - if (config.grpcProxyUrl) { - // grpc.http_proxy is not used by grpc.js under-the-hood - // only uses the env var and whether http_proxy is enabled - process.env.grpc_proxy = config.grpcProxyUrl; - channelOptions['grpc.enabled_http_proxy'] = true; + if (!config.transport) { + throw new Error('grpcClient requires a transport to be configured'); } - const channel = createChannel( - config.grpcAddress, - config.grpcSecure ? ChannelCredentials.createSsl() : ChannelCredentials.createInsecure(), - channelOptions - ); - const client = clientFactory.create(WeaviateDefinition, channel); - const health = clientFactory.create(HealthDefinition, channel); + const { client, health, close } = config.transport.create(config); return { aggregate: ( collection: string, @@ -229,7 +208,7 @@ export const grpcClient = (config: GrpcConnectionParams & { grpcMaxMessageLength consistencyLevel, tenant ), - close: () => channel.close(), + close: () => close(), health: () => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), (config.timeout?.init || 2) * 1000); diff --git a/src/connection/helpers.ts b/src/connection/helpers.ts index 28a0a959..8f146149 100644 --- a/src/connection/helpers.ts +++ b/src/connection/helpers.ts @@ -18,6 +18,8 @@ export type ConnectToWeaviateCloudOptions = { timeout?: TimeoutParams; /** Whether to skip the initialization checks */ skipInitChecks?: boolean; + /** Which gRPC transport to use: 'native' (default, Node) or 'grpc-web' (browser-compatible). */ + transport?: 'native' | 'grpc-web'; }; /** @deprecated Use `ConnectToWeaviateCloudOptions` instead. */ @@ -41,6 +43,8 @@ export type ConnectToLocalOptions = { timeout?: TimeoutParams; /** Whether to skip the initialization checks */ skipInitChecks?: boolean; + /** Which gRPC transport to use: 'native' (default, Node) or 'grpc-web' (browser-compatible). */ + transport?: 'native' | 'grpc-web'; }; export type ConnectToCustomOptions = { @@ -68,6 +72,8 @@ export type ConnectToCustomOptions = { timeout?: TimeoutParams; /** Whether to skip the initialization checks */ skipInitChecks?: boolean; + /** Which gRPC transport to use: 'native' (default, Node) or 'grpc-web' (browser-compatible). */ + transport?: 'native' | 'grpc-web'; }; export function connectToWeaviateCloud( diff --git a/src/connection/http.ts b/src/connection/http.ts index 53b4bdfd..25e81e4a 100644 --- a/src/connection/http.ts +++ b/src/connection/http.ts @@ -1,5 +1,5 @@ import { isAbortError } from 'abort-controller-x'; -import { Agent } from 'http'; +import type { Agent } from 'http'; import OpenidConfigurationGetter from '../misc/openidConfigurationGetter.js'; @@ -262,7 +262,7 @@ export const httpClient = (config: InternalConnectionParams): HttpClient => { const url = makeUrl(baseUri); return { - close: () => config.agent?.destroy(), + close: () => config.agent?.destroy?.(), post: ( path: string, payload: B, diff --git a/src/connection/makeClient.ts b/src/connection/makeClient.ts new file mode 100644 index 00000000..85b9cfc3 --- /dev/null +++ b/src/connection/makeClient.ts @@ -0,0 +1,95 @@ +import type { Agent } from 'http'; + +import alias from '../alias/index.js'; +import { backup } from '../collections/backup/client.js'; +import cluster from '../collections/cluster/index.js'; +import batch from '../collections/data/batch.js'; +import collections from '../collections/index.js'; +import groups from '../groups/index.js'; +import type { ClientParams, WeaviateClient } from '../index.js'; +import { LiveChecker, OpenidConfigurationGetter, ReadyChecker } from '../misc/index.js'; +import MetaGetter from '../misc/metaGetter.js'; +import roles from '../roles/index.js'; +import tokenize from '../tokenize/index.js'; +import users from '../users/index.js'; +import { isApiKey, mapApiKey } from './auth.js'; +import { ConnectionGRPC } from './index.js'; +import { GrpcTransport } from './transports/types.js'; + +const cleanHost = (host: string, protocol: 'rest' | 'grpc') => { + if (host.includes('http')) { + console.warn( + `The ${protocol}.host parameter should not include the protocol. Please remove the http:// or https:// from the ${protocol}.host parameter.\ + To specify a secure connection, set the secure parameter to true. The protocol will be inferred from the secure parameter instead.` + ); + return host.replace('http://', '').replace('https://', ''); + } + return host; +}; + +/** + * Assemble a `WeaviateClient` from connection parameters. Shared by the Node + * and browser entry points, which differ only in the gRPC `transport` and how + * (if at all) an HTTP `Agent` is constructed. + * + * @param params The user-supplied client parameters. + * @param transport The gRPC transport to use (native or grpc-web). + * @param makeAgent Builds the optional HTTP(S) agent. Returns `undefined` in + * environments (e.g. the browser) where Node's `http`/`https` agents are + * unavailable. + */ +export async function makeClient( + params: ClientParams, + transport: GrpcTransport, + makeAgent: (secure: boolean) => Agent | undefined +): Promise { + let { host: httpHost } = params.connectionParams.http; + let { host: grpcHost } = params.connectionParams.grpc; + const { port: httpPort, secure: httpSecure, path: httpPath } = params.connectionParams.http; + const { port: grpcPort, secure: grpcSecure, path: grpcPath } = params.connectionParams.grpc; + httpHost = cleanHost(httpHost, 'rest'); + grpcHost = cleanHost(grpcHost, 'grpc'); + + // check if headers are set + if (!params.headers) params.headers = {}; + + const scheme = httpSecure ? 'https' : 'http'; + const agent = makeAgent(httpSecure); + + const { connection, dbVersionProvider, dbVersionSupport } = await ConnectionGRPC.use({ + host: `${scheme}://${httpHost}:${httpPort}${httpPath || ''}`, + scheme: scheme, + headers: params.headers, + grpcAddress: `${grpcHost}:${grpcPort}${grpcPath || ''}`, + grpcSecure: grpcSecure, + grpcProxyUrl: params.proxies?.grpc, + apiKey: isApiKey(params.auth) ? mapApiKey(params.auth) : undefined, + authClientSecret: isApiKey(params.auth) ? undefined : params.auth, + agent, + timeout: params.timeout, + skipInitChecks: params.skipInitChecks, + transport, + }); + + const ifc: WeaviateClient = { + alias: alias(connection), + backup: backup(connection), + batch: batch(connection, dbVersionSupport), + cluster: cluster(connection), + collections: collections(connection, dbVersionSupport), + groups: groups(connection), + roles: roles(connection), + tokenize: tokenize(connection, dbVersionSupport), + users: users(connection), + close: () => Promise.resolve(connection.close()), // hedge against future changes to add I/O to .close() + getMeta: () => new MetaGetter(connection).do(), + getConnectionDetails: connection.getDetails, + getOpenIDConfig: () => new OpenidConfigurationGetter(connection.http).do(), + getWeaviateVersion: () => dbVersionSupport.getVersion(), + isLive: () => new LiveChecker(connection, dbVersionProvider).do(), + isReady: () => new ReadyChecker(connection, dbVersionProvider).do(), + }; + if (connection.oidcAuth) ifc.oidcAuth = connection.oidcAuth; + + return ifc; +} diff --git a/src/connection/transports/index.ts b/src/connection/transports/index.ts new file mode 100644 index 00000000..a0d6b574 --- /dev/null +++ b/src/connection/transports/index.ts @@ -0,0 +1,13 @@ +import { nativeGrpcTransport } from './native.js'; +import { GrpcTransport } from './types.js'; +import { webGrpcTransport } from './web.js'; + +export { nativeGrpcTransport } from './native.js'; +export type { GrpcClients, GrpcTransport } from './types.js'; +export { webGrpcTransport } from './web.js'; + +export type GrpcTransportName = 'native' | 'grpc-web'; + +export function resolveGrpcTransport(name?: GrpcTransportName): GrpcTransport { + return name === 'grpc-web' ? webGrpcTransport : nativeGrpcTransport; +} diff --git a/src/connection/transports/native.ts b/src/connection/transports/native.ts new file mode 100644 index 00000000..9d90eeae --- /dev/null +++ b/src/connection/transports/native.ts @@ -0,0 +1,36 @@ +import { ChannelCredentials, ChannelOptions, createChannel, createClientFactory } from 'nice-grpc'; +import { retryMiddleware } from 'nice-grpc-client-middleware-retry'; + +import { HealthDefinition } from '../../proto/google/health/v1/health.js'; +import { WeaviateDefinition } from '../../proto/v1/weaviate.js'; +import { GrpcClients, GrpcTransport } from './types.js'; + +const clientFactory = createClientFactory().use(retryMiddleware); + +export const nativeGrpcTransport: GrpcTransport = { + supportsStreaming: true, + create: (config): GrpcClients => { + const channelOptions: ChannelOptions = { + 'grpc.max_send_message_length': config.grpcMaxMessageLength, + 'grpc.max_receive_message_length': config.grpcMaxMessageLength, + }; + if (config.grpcProxyUrl) { + // grpc.http_proxy is not used by grpc.js under-the-hood + // only uses the env var and whether http_proxy is enabled + process.env.grpc_proxy = config.grpcProxyUrl; + channelOptions['grpc.enabled_http_proxy'] = true; + } + const channel = createChannel( + config.grpcAddress, + config.grpcSecure ? ChannelCredentials.createSsl() : ChannelCredentials.createInsecure(), + channelOptions + ); + const client = clientFactory.create(WeaviateDefinition, channel); + const health = clientFactory.create(HealthDefinition, channel); + return { + client, + health, + close: () => channel.close(), + }; + }, +}; diff --git a/src/connection/transports/types.ts b/src/connection/transports/types.ts new file mode 100644 index 00000000..e974d6db --- /dev/null +++ b/src/connection/transports/types.ts @@ -0,0 +1,15 @@ +import { RetryOptions } from 'nice-grpc-client-middleware-retry'; +import { HealthClient } from '../../proto/google/health/v1/health.js'; +import { WeaviateClient } from '../../proto/v1/weaviate.js'; +import type { GrpcConnectionParams } from '../grpc.js'; + +export interface GrpcClients { + client: WeaviateClient; + health: HealthClient; + close: () => void; +} + +export interface GrpcTransport { + readonly supportsStreaming: boolean; + create(config: GrpcConnectionParams & { grpcMaxMessageLength: number }): GrpcClients; +} diff --git a/src/connection/transports/web.ts b/src/connection/transports/web.ts new file mode 100644 index 00000000..d398f1ea --- /dev/null +++ b/src/connection/transports/web.ts @@ -0,0 +1,32 @@ +import { retryMiddleware } from 'nice-grpc-client-middleware-retry'; +import { createChannel, createClientFactory, FetchTransport } from 'nice-grpc-web'; + +import { HealthDefinition } from '../../proto/google/health/v1/health.js'; +import { WeaviateDefinition } from '../../proto/v1/weaviate.js'; +import { GrpcClients, GrpcTransport } from './types.js'; + +const clientFactory = createClientFactory().use(retryMiddleware); + +export const webGrpcTransport: GrpcTransport = { + supportsStreaming: false, + create: (config): GrpcClients => { + // nice-grpc-web needs a fully-qualified URL with a scheme, unlike the + // native transport which uses a bare `host:port` address. The configured + // grpcAddress may be a bare `host:port`, a `host:port/path`, or an + // already-fully-qualified URL. + const address = /^https?:\/\//.test(config.grpcAddress) + ? config.grpcAddress + : `${config.grpcSecure ? 'https' : 'http'}://${config.grpcAddress}`; + // eslint-disable-next-line new-cap -- FetchTransport is a factory function, not a constructor + const channel = createChannel(address, FetchTransport()); + const client = clientFactory.create(WeaviateDefinition, channel); + const health = clientFactory.create(HealthDefinition, channel); + return { + client, + health, + // Fetch-based gRPC-Web channels hold no persistent connection, so there + // is nothing to tear down here. + close: () => {}, + }; + }, +}; diff --git a/src/grpc/aggregator.ts b/src/grpc/aggregator.ts index 4262fe5d..5ea69e22 100644 --- a/src/grpc/aggregator.ts +++ b/src/grpc/aggregator.ts @@ -1,6 +1,6 @@ import { ConsistencyLevel } from '../data/index.js'; -import { Metadata } from 'nice-grpc'; +import { Metadata } from 'nice-grpc-common'; import { AggregateReply, AggregateRequest, diff --git a/src/grpc/base.ts b/src/grpc/base.ts index af84976e..9f71d180 100644 --- a/src/grpc/base.ts +++ b/src/grpc/base.ts @@ -1,8 +1,8 @@ import { ConsistencyLevel } from '../data/index.js'; import { isAbortError } from 'abort-controller-x'; -import { Metadata, ServerError, Status } from 'nice-grpc'; import { RetryOptions } from 'nice-grpc-client-middleware-retry'; +import { Metadata, ServerError, Status } from 'nice-grpc-common'; import { WeaviateInsufficientPermissionsError, WeaviateRequestTimeoutError } from '../errors.js'; import { ConsistencyLevel as ConsistencyLevelGRPC } from '../proto/v1/base.js'; import { WeaviateClient } from '../proto/v1/weaviate.js'; diff --git a/src/grpc/batcher.ts b/src/grpc/batcher.ts index c995ddf4..d664040d 100644 --- a/src/grpc/batcher.ts +++ b/src/grpc/batcher.ts @@ -1,4 +1,4 @@ -import { ClientError, Metadata } from 'nice-grpc'; +import { ClientError, Metadata } from 'nice-grpc-common'; import { ConsistencyLevel } from '../data/index.js'; diff --git a/src/grpc/retry.ts b/src/grpc/retry.ts index a2628117..96d379a9 100644 --- a/src/grpc/retry.ts +++ b/src/grpc/retry.ts @@ -1,5 +1,5 @@ -import { ClientError, Status } from 'nice-grpc'; import { RetryOptions } from 'nice-grpc-client-middleware-retry'; +import { ClientError, Status } from 'nice-grpc-common'; export const retryOptions: RetryOptions = { retry: true, diff --git a/src/grpc/searcher.ts b/src/grpc/searcher.ts index 60f38cf4..efca5a76 100644 --- a/src/grpc/searcher.ts +++ b/src/grpc/searcher.ts @@ -1,6 +1,6 @@ import { ConsistencyLevel } from '../data/index.js'; -import { Metadata } from 'nice-grpc'; +import { Metadata } from 'nice-grpc-common'; import { Filters } from '../proto/v1/base.js'; import { BM25, diff --git a/src/grpc/tenantsManager.ts b/src/grpc/tenantsManager.ts index 82473f76..20fea6ff 100644 --- a/src/grpc/tenantsManager.ts +++ b/src/grpc/tenantsManager.ts @@ -1,5 +1,5 @@ -import { Metadata } from 'nice-grpc'; import { RetryOptions } from 'nice-grpc-client-middleware-retry'; +import { Metadata } from 'nice-grpc-common'; import { WeaviateTenantsGetError } from '../errors.js'; import { TenantsGetReply, TenantsGetRequest } from '../proto/v1/tenants.js'; import { WeaviateClient } from '../proto/v1/weaviate.js'; diff --git a/src/index.ts b/src/index.ts index b3fa9e7d..f4a88c82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import { Backend, BackupCompressionLevel, BackupStatus } from './backup/index.js'; -import { Backup, backup } from './collections/backup/client.js'; -import cluster, { Cluster } from './collections/cluster/index.js'; +import { Backup } from './collections/backup/client.js'; +import { Cluster } from './collections/cluster/index.js'; import { configGuards } from './collections/config/index.js'; import { configure, reconfigure } from './collections/configure/index.js'; -import collections, { Collections, queryFactory } from './collections/index.js'; +import { Collections, queryFactory } from './collections/index.js'; import { AccessTokenCredentialsInput, ApiKey, @@ -14,8 +14,6 @@ import { ClientCredentialsInput, OidcAuthenticator, UserPasswordCredentialsInput, - isApiKey, - mapApiKey, } from './connection/auth.js'; import * as helpers from './connection/helpers.js'; import { @@ -26,25 +24,24 @@ import { ConnectToWeaviateCloudOptions, } from './connection/helpers.js'; import { ConnectionDetails, Headers, ProxiesParams, TimeoutParams } from './connection/http.js'; -import { ConnectionGRPC } from './connection/index.js'; -import MetaGetter from './misc/metaGetter.js'; +import { makeClient } from './connection/makeClient.js'; +import { resolveGrpcTransport } from './connection/transports/index.js'; import { Meta } from './openapi/types.js'; -import roles, { Roles, permissions } from './roles/index.js'; +import { Roles, permissions } from './roles/index.js'; import { DbVersion } from './utils/dbVersion.js'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; -import { LiveChecker, OpenidConfigurationGetter, ReadyChecker } from './misc/index.js'; import weaviateV2 from './v2/index.js'; -import alias, { Aliases } from './alias/index.js'; -import batch, { Batch } from './collections/data/batch.js'; +import { Aliases } from './alias/index.js'; +import { Batch } from './collections/data/batch.js'; import filter from './collections/filters/index.js'; import { ConsistencyLevel } from './data/replication.js'; -import groups, { Groups } from './groups/index.js'; -import tokenize, { Tokenize } from './tokenize/index.js'; -import users, { Users } from './users/index.js'; +import { Groups } from './groups/index.js'; +import { Tokenize } from './tokenize/index.js'; +import { Users } from './users/index.js'; export type ProtocolParams = { /** @@ -103,6 +100,8 @@ export type ClientParams = { timeout?: TimeoutParams; /** Whether to skip the initialization checks */ skipInitChecks?: boolean; + /** Which gRPC transport to use: 'native' (default, Node) or 'grpc-web' (browser-compatible). */ + transport?: 'native' | 'grpc-web'; }; export interface WeaviateClient { @@ -126,17 +125,6 @@ export interface WeaviateClient { isReady: () => Promise; } -const cleanHost = (host: string, protocol: 'rest' | 'grpc') => { - if (host.includes('http')) { - console.warn( - `The ${protocol}.host parameter should not include the protocol. Please remove the http:// or https:// from the ${protocol}.host parameter.\ - To specify a secure connection, set the secure parameter to true. The protocol will be inferred from the secure parameter instead.` - ); - return host.replace('http://', '').replace('https://', ''); - } - return host; -}; - /** * Connect to a custom Weaviate deployment, e.g. your own self-hosted Kubernetes cluster. * @@ -203,55 +191,14 @@ export function connectToWeaviateCloud( return helpers.connectToWeaviateCloud(clusterURL, client, options); } -async function client(params: ClientParams): Promise { - let { host: httpHost } = params.connectionParams.http; - let { host: grpcHost } = params.connectionParams.grpc; - const { port: httpPort, secure: httpSecure, path: httpPath } = params.connectionParams.http; - const { port: grpcPort, secure: grpcSecure } = params.connectionParams.grpc; - httpHost = cleanHost(httpHost, 'rest'); - grpcHost = cleanHost(grpcHost, 'grpc'); - - // check if headers are set - if (!params.headers) params.headers = {}; - - const scheme = httpSecure ? 'https' : 'http'; - const agent = httpSecure ? new HttpsAgent({ keepAlive: true }) : new HttpAgent({ keepAlive: true }); - - const { connection, dbVersionProvider, dbVersionSupport } = await ConnectionGRPC.use({ - host: `${scheme}://${httpHost}:${httpPort}${httpPath || ''}`, - scheme: scheme, - headers: params.headers, - grpcAddress: `${grpcHost}:${grpcPort}`, - grpcSecure: grpcSecure, - grpcProxyUrl: params.proxies?.grpc, - apiKey: isApiKey(params.auth) ? mapApiKey(params.auth) : undefined, - authClientSecret: isApiKey(params.auth) ? undefined : params.auth, - agent, - timeout: params.timeout, - skipInitChecks: params.skipInitChecks, - }); - - const ifc: WeaviateClient = { - alias: alias(connection), - backup: backup(connection), - batch: batch(connection, dbVersionSupport), - cluster: cluster(connection), - collections: collections(connection, dbVersionSupport), - groups: groups(connection), - roles: roles(connection), - tokenize: tokenize(connection, dbVersionSupport), - users: users(connection), - close: () => Promise.resolve(connection.close()), // hedge against future changes to add I/O to .close() - getMeta: () => new MetaGetter(connection).do(), - getConnectionDetails: connection.getDetails, - getOpenIDConfig: () => new OpenidConfigurationGetter(connection.http).do(), - getWeaviateVersion: () => dbVersionSupport.getVersion(), - isLive: () => new LiveChecker(connection, dbVersionProvider).do(), - isReady: () => new ReadyChecker(connection, dbVersionProvider).do(), - }; - if (connection.oidcAuth) ifc.oidcAuth = connection.oidcAuth; - - return ifc; +function client(params: ClientParams): Promise { + return makeClient(params, resolveGrpcTransport(params.transport), (secure) => + params.transport === 'grpc-web' + ? undefined + : secure + ? new HttpsAgent({ keepAlive: true }) + : new HttpAgent({ keepAlive: true }) + ); } export default { diff --git a/src/index.web.ts b/src/index.web.ts new file mode 100644 index 00000000..61e54420 --- /dev/null +++ b/src/index.web.ts @@ -0,0 +1,241 @@ +/** + * Browser / gRPC-Web entry point for the Weaviate TypeScript client. + * + * This module wires ONLY the gRPC-Web (`nice-grpc-web`) transport and MUST stay + * free of Node-only imports (`http`, `https`, `@grpc/grpc-js`, `nice-grpc`, + * etc.). In particular it must never import `./connection/transports/native.js` + * or the `resolveGrpcTransport` resolver from `./connection/transports/index.js`, + * both of which transitively load Node's `http2` stack and would break browser + * bundles. Agents are never constructed here — `makeClient` is given a no-op + * agent factory. + */ +import { configGuards } from './collections/config/index.js'; +import { configure, reconfigure } from './collections/configure/index.js'; +import filter from './collections/filters/index.js'; +import { queryFactory } from './collections/index.js'; +import { + ApiKey, + AuthAccessTokenCredentials, + AuthClientCredentials, + AuthCredentials, + AuthUserPasswordCredentials, +} from './connection/auth.js'; +import { Headers, TimeoutParams } from './connection/http.js'; +import { makeClient } from './connection/makeClient.js'; +import { webGrpcTransport } from './connection/transports/web.js'; +import { permissions } from './roles/index.js'; + +import type { ClientParams, WeaviateClient } from './index.js'; + +/** The gRPC-Web path that vanguard serves the gRPC-Web proxy under by default. */ +const DEFAULT_GRPC_WEB_PATH = '/grpc-web'; + +function client(params: ClientParams): Promise { + return makeClient(params, webGrpcTransport, () => undefined); +} + +/** Options for {@link connectToWeaviateCloud} in the browser/gRPC-Web entry. */ +export interface ConnectToWeaviateCloudOptions { + /** The authentication credentials to use when connecting to Weaviate, e.g. API key */ + authCredentials?: AuthCredentials; + /** Additional headers to include in the request */ + headers?: Headers; + /** The timeouts to use when making requests to Weaviate */ + timeout?: TimeoutParams; + /** Whether to skip the initialization checks */ + skipInitChecks?: boolean; + /** Override the gRPC-Web path served on the main host (default `/grpc-web`). */ + grpcWebPath?: string; +} + +/** + * Connect to your own Weaviate Cloud (WCD) instance over gRPC-Web. + * + * Unlike the native helper, gRPC-Web is served on the MAIN host under a path + * (default `/grpc-web`), not on the `grpc-` variant. + * + * @param {string} clusterURL The URL of your WCD instance. E.g., `https://example.weaviate.network`. + * @param {ConnectToWeaviateCloudOptions} [options] Additional options for the connection. + * @returns {Promise} A Promise that resolves to a connected client. + */ +export function connectToWeaviateCloud( + clusterURL: string, + options?: ConnectToWeaviateCloudOptions +): Promise { + if (!clusterURL) { + throw new Error('Missing `clusterURL` parameter'); + } + if (!clusterURL.startsWith('http')) { + clusterURL = `https://${clusterURL}`; + } + const url = new URL(clusterURL); + const { authCredentials: auth, headers, timeout, skipInitChecks, grpcWebPath } = options || {}; + return client({ + connectionParams: { + http: { secure: true, host: url.hostname, port: 443 }, + grpc: { secure: true, host: url.hostname, port: 443, path: grpcWebPath ?? DEFAULT_GRPC_WEB_PATH }, + }, + auth, + headers, + timeout, + skipInitChecks, + }); +} + +/** Options for {@link connectToCustom} in the browser/gRPC-Web entry. */ +export interface ConnectToCustomOptions { + /** The hostname of the HTTP/1.1 server */ + httpHost?: string; + /** The port of the HTTP/1.1 server */ + httpPort?: number; + /** Whether to use a secure connection to the HTTP/1.1 server */ + httpSecure?: boolean; + /** An additional path of the HTTP/1.1 server, e.g. `http://proxy.net/weaviate` */ + httpPath?: string; + /** The hostname of the gRPC-Web server */ + grpcHost?: string; + /** The port of the gRPC-Web server */ + grpcPort?: number; + /** Whether to use a secure connection to the gRPC-Web server */ + grpcSecure?: boolean; + /** The path the gRPC-Web proxy is served under (default `/grpc-web`). */ + grpcPath?: string; + /** The authentication credentials to use when connecting to Weaviate, e.g. API key */ + authCredentials?: AuthCredentials; + /** Additional headers to include in the request */ + headers?: Headers; + /** The timeouts to use when making requests to Weaviate */ + timeout?: TimeoutParams; + /** Whether to skip the initialization checks */ + skipInitChecks?: boolean; +} + +/** + * Connect to a custom Weaviate deployment over gRPC-Web. + * + * @param {ConnectToCustomOptions} [options] Options for the connection. + * @returns {Promise} A Promise that resolves to a connected client. + */ +export function connectToCustom(options?: ConnectToCustomOptions): Promise { + const { + httpHost, + httpPort, + httpSecure, + httpPath, + grpcHost, + grpcPort, + grpcSecure, + grpcPath, + authCredentials: auth, + headers, + timeout, + skipInitChecks, + } = options || {}; + return client({ + connectionParams: { + http: { + secure: httpSecure || false, + host: httpHost || 'localhost', + port: httpPort || 8080, + path: httpPath || '', + }, + grpc: { + secure: grpcSecure || false, + host: grpcHost || 'localhost', + port: grpcPort ?? httpPort ?? 8080, + path: grpcPath ?? DEFAULT_GRPC_WEB_PATH, + }, + }, + auth, + headers, + timeout, + skipInitChecks, + }); +} + +/** Options for {@link connectToLocal} in the browser/gRPC-Web entry. */ +export interface ConnectToLocalOptions { + /** The host where Weaviate is served. Assumes HTTP and gRPC-Web are served on the same host. */ + host?: string; + /** The port of the HTTP/1.1 server */ + port?: number; + /** The port of the gRPC-Web server (defaults to the HTTP port, not 50051). */ + grpcPort?: number; + /** The path the gRPC-Web proxy is served under (default `/grpc-web`). */ + grpcWebPath?: string; + /** The authentication credentials to use when connecting to Weaviate, e.g. API key */ + authCredentials?: AuthCredentials; + /** Additional headers to include in the request */ + headers?: Headers; + /** The timeouts to use when making requests to Weaviate */ + timeout?: TimeoutParams; + /** Whether to skip the initialization checks */ + skipInitChecks?: boolean; +} + +/** + * Connect to a locally-deployed Weaviate instance over gRPC-Web. + * + * Note: for local vanguard the gRPC-Web port is the HTTP port, not 50051. + * + * @param {ConnectToLocalOptions} [options] Options for the connection. + * @returns {Promise} A Promise that resolves to a connected client. + */ +export function connectToLocal(options?: ConnectToLocalOptions): Promise { + const { + host, + port, + grpcPort, + grpcWebPath, + authCredentials: auth, + headers, + timeout, + skipInitChecks, + } = options || {}; + return client({ + connectionParams: { + http: { secure: false, host: host || 'localhost', port: port || 8080 }, + grpc: { + secure: false, + host: host || 'localhost', + port: grpcPort || port || 8080, + path: grpcWebPath || DEFAULT_GRPC_WEB_PATH, + }, + }, + auth, + headers, + timeout, + skipInitChecks, + }); +} + +export default { + connectToCustom, + connectToLocal, + connectToWeaviateCloud, + client, + ApiKey, + AuthUserPasswordCredentials, + AuthAccessTokenCredentials, + AuthClientCredentials, + configure, + configGuards, + filter: filter(), + reconfigure, + permissions, + query: queryFactory, +}; + +export * from './collections/index.js'; +export * from './errors.js'; +export type { ClientParams, ConnectionParams, ProtocolParams, WeaviateClient } from './index.js'; +export * from './roles/types.js'; +export * from './utils/base64.js'; +export * from './utils/uuid.js'; +export { + ApiKey, + AuthAccessTokenCredentials, + AuthClientCredentials, + AuthCredentials, + AuthUserPasswordCredentials, +}; diff --git a/src/roles/index.ts b/src/roles/index.ts index a0d0c88e..a92819d1 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -1,4 +1,4 @@ -import { ConnectionREST } from '../index.js'; +import { ConnectionREST } from '../connection/index.js'; import { WeaviateAssignedUser, WeaviateGroupAssignment, diff --git a/src/users/index.ts b/src/users/index.ts index 79e33a72..d702d50a 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -1,5 +1,5 @@ +import { ConnectionREST } from '../connection/index.js'; import { WeaviateUnexpectedStatusCodeError } from '../errors.js'; -import { ConnectionREST } from '../index.js'; import { WeaviateUserTypeInternal as UserTypeInternal, WeaviateDBUser, diff --git a/src/utils/dbVersion.ts b/src/utils/dbVersion.ts index b37c328f..fbdeac88 100644 --- a/src/utils/dbVersion.ts +++ b/src/utils/dbVersion.ts @@ -1,4 +1,4 @@ -import { ConnectionGQL } from '../index.js'; +import type { ConnectionGQL } from '../connection/index.js'; import MetaGetter from '../misc/metaGetter.js'; export class DbVersionSupport { diff --git a/src/web-shims/buffer.ts b/src/web-shims/buffer.ts new file mode 100644 index 00000000..1c85fc22 --- /dev/null +++ b/src/web-shims/buffer.ts @@ -0,0 +1 @@ +export { Buffer } from 'buffer'; diff --git a/src/web-shims/empty.ts b/src/web-shims/empty.ts new file mode 100644 index 00000000..b36d6f11 --- /dev/null +++ b/src/web-shims/empty.ts @@ -0,0 +1,10 @@ +// Browser stand-in for Node built-in modules (fs/http/https) that are imported +// by isomorphic code paths but not exercised in the browser. Only the file-path +// media helpers in utils/base64 would touch these, which browser code does not use. +export default {}; + +// The isomorphic native entry (`src/index.ts`) imports `{ Agent }` from +// `http`/`https` via named bindings. Provide a stand-in constructor so the +// browser bundle links cleanly; it is tree-shaken away / never instantiated on +// the gRPC-Web code path. +export class Agent {} diff --git a/tsup.config.ts b/tsup.config.ts index f66a2362..986c98a0 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,4 +1,25 @@ -import { defineConfig } from 'tsup'; +import { defineConfig, type Options } from 'tsup'; +import { fileURLToPath } from 'url'; + +// Shared esbuild tweaks that make a `platform: 'browser'` bundle load without Node +// built-ins. Isomorphic code paths (utils/base64, collections/deserialize) statically +// reference `fs`/`http`/`https` and `Buffer`, but the browser path never exercises the +// file-system/agent helpers. We alias those built-ins to an empty stand-in and inject a +// `Buffer` polyfill at the bundler level, leaving the runtime source untouched. +const applyBrowserShims: NonNullable = (options) => { + const shim = fileURLToPath(new URL('./src/web-shims/empty.ts', import.meta.url)); + options.alias = { + ...(options.alias ?? {}), + fs: shim, + http: shim, + https: shim, + }; + options.inject = [ + ...(options.inject ?? []), + fileURLToPath(new URL('./src/web-shims/buffer.ts', import.meta.url)), + ]; + options.define = { ...(options.define ?? {}), 'process.env.NODE_ENV': '"production"' }; +}; export default defineConfig([ { @@ -20,6 +41,19 @@ export default defineConfig([ dts: true, splitting: true, treeshake: true, + esbuildOptions: applyBrowserShims, + }, + { + entry: { 'index.web': 'src/index.web.ts' }, + format: ['esm', 'cjs'], + outDir: 'dist/web', + clean: false, + platform: 'browser', + minify: true, + dts: true, + splitting: false, + treeshake: true, + esbuildOptions: applyBrowserShims, }, // { // entry: {