Skip to content

Commit 7154af6

Browse files
committed
Use platform/ convention
1 parent 647eb21 commit 7154af6

File tree

9 files changed

+122
-104
lines changed

9 files changed

+122
-104
lines changed

packages/ai/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"test:ci": "yarn testsetup && node ../../scripts/run_tests_in_ci.js -s test",
4040
"test:skip-clone": "karma start",
4141
"test:browser": "yarn testsetup && karma start",
42-
"test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require src/index.node.ts src/**/*.test.ts --config ../../config/mocharc.node.js",
42+
"test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require src/index.node.ts 'src/**/*.test.ts' --config ../../config/mocharc.node.js",
4343
"test:integration": "karma start --integration",
4444
"api-report": "api-extractor run --local --verbose",
4545
"typings:public": "node ../../scripts/build/use_typings.js ./dist/ai-public.d.ts",

packages/ai/rollup.config.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
* limitations under the License.
1616
*/
1717

18+
import alias from '@rollup/plugin-alias';
1819
import json from '@rollup/plugin-json';
1920
import typescriptPlugin from 'rollup-plugin-typescript2';
2021
import replace from 'rollup-plugin-replace';
2122
import typescript from 'typescript';
2223
import pkg from './package.json';
2324
import tsconfig from './tsconfig.json';
2425
import { generateBuildTargetReplaceConfig } from '../../scripts/build/rollup_replace_build_target';
25-
import { getEnvironmentReplacements } from '../../scripts/build/rollup_get_environment_replacements';
2626
import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file';
27+
import { generateAliasConfig } from '../../scripts/build/rollup_generate_alias_config';
2728

2829
const deps = Object.keys(
2930
Object.assign({}, pkg.peerDependencies, pkg.dependencies)
@@ -56,9 +57,9 @@ const browserBuilds = [
5657
sourcemap: true
5758
},
5859
plugins: [
60+
alias(generateAliasConfig('browser')),
5961
...buildPlugins,
6062
replace({
61-
...getEnvironmentReplacements('browser'),
6263
...generateBuildTargetReplaceConfig('esm', 2020),
6364
'__PACKAGE_VERSION__': pkg.version
6465
}),
@@ -75,9 +76,9 @@ const browserBuilds = [
7576
sourcemap: true
7677
},
7778
plugins: [
79+
alias(generateAliasConfig('browser')),
7880
...buildPlugins,
7981
replace({
80-
...getEnvironmentReplacements('browser'),
8182
...generateBuildTargetReplaceConfig('cjs', 2020),
8283
'__PACKAGE_VERSION__': pkg.version
8384
})
@@ -96,9 +97,9 @@ const nodeBuilds = [
9697
sourcemap: true
9798
},
9899
plugins: [
100+
alias(generateAliasConfig('node')),
99101
...buildPlugins,
100102
replace({
101-
...getEnvironmentReplacements('node'),
102103
...generateBuildTargetReplaceConfig('esm', 2020)
103104
})
104105
],
@@ -113,9 +114,9 @@ const nodeBuilds = [
113114
sourcemap: true
114115
},
115116
plugins: [
117+
alias(generateAliasConfig('node')),
116118
...buildPlugins,
117119
replace({
118-
...getEnvironmentReplacements('node'),
119120
...generateBuildTargetReplaceConfig('cjs', 2020)
120121
})
121122
],

packages/ai/src/ws/browser-websocket-handler.test.ts renamed to packages/ai/src/platform/browser/websocket.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import { expect, use } from 'chai';
1919
import sinon, { SinonFakeTimers, SinonStub } from 'sinon';
2020
import sinonChai from 'sinon-chai';
2121
import chaiAsPromised from 'chai-as-promised';
22-
import { BrowserWebSocketHandler } from './browser-websocket-handler';
23-
import { AIError } from '../errors';
2422
import { isBrowser } from '@firebase/util';
23+
import { BrowserWebSocketHandler } from './websocket';
24+
import { AIError } from '../../errors';
2525

2626
use(sinonChai);
2727
use(chaiAsPromised);
@@ -270,4 +270,4 @@ describe('BrowserWebSocketHandler', () => {
270270
expect(mockWebSocket.readyState).to.equal(MockBrowserWebSocket.CLOSED);
271271
});
272272
});
273-
});
273+
});

packages/ai/src/ws/browser-websocket-handler.ts renamed to packages/ai/src/platform/browser/websocket.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,27 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { AIError } from '../errors';
19-
import { AIErrorCode } from '../types';
20-
import { WebSocketHandler } from './websocket-handler';
18+
import { AIError } from '../../errors';
19+
import { AIErrorCode } from '../../types';
20+
import { WebSocketHandler } from '../websocket';
21+
22+
export function createWebSocketHandler(): WebSocketHandler {
23+
if (typeof WebSocket !== 'undefined') {
24+
return new BrowserWebSocketHandler();
25+
} else {
26+
throw new AIError(
27+
AIErrorCode.UNSUPPORTED,
28+
'The WebSocket API is not available in this browser-like environment. ' +
29+
'The "Live" feature is not supported here. It is supported in ' +
30+
'standard browser windows, Web Workers with WebSocket support, and Node >= 22.'
31+
);
32+
}
33+
}
2134

2235
/**
2336
* A WebSocketHandler implementation for the browser environment.
2437
* It uses the native `WebSocket`.
38+
*
2539
* @internal
2640
*/
2741
export class BrowserWebSocketHandler implements WebSocketHandler {
@@ -63,7 +77,6 @@ export class BrowserWebSocketHandler implements WebSocketHandler {
6377
}
6478

6579
async *listen(): AsyncGenerator<unknown> {
66-
console.log('listener started');
6780
if (!this.ws) {
6881
throw new AIError(
6982
AIErrorCode.REQUEST_ERROR,
@@ -77,16 +90,21 @@ export class BrowserWebSocketHandler implements WebSocketHandler {
7790

7891
const messageListener = async (event: MessageEvent): Promise<void> => {
7992
if (event.data instanceof Blob) {
80-
const obj = JSON.parse(await event.data.text()) as unknown;
81-
messageQueue.push(obj);
82-
if (resolvePromise) {
83-
resolvePromise();
84-
resolvePromise = null;
93+
try {
94+
const obj = JSON.parse(await event.data.text()) as unknown;
95+
messageQueue.push(obj);
96+
if (resolvePromise) {
97+
resolvePromise();
98+
resolvePromise = null;
99+
}
100+
} catch (e) {
101+
console.warn('Failed to parse WebSocket message to JSON:', e);
85102
}
86103
} else {
87104
throw new AIError(
88105
AIErrorCode.PARSE_FAILED,
89-
'Failed to parse WebSocket response to JSON, response was not a Blob'
106+
`Failed to parse WebSocket response to JSON. ` +
107+
`Expected data to be a Blob, but was ${typeof event.data}.`
90108
);
91109
}
92110
};

packages/ai/src/ws/node-websocket-handler.test.ts renamed to packages/ai/src/platform/node/websocket.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import { expect, use } from 'chai';
1919
import sinonChai from 'sinon-chai';
2020
import chaiAsPromised from 'chai-as-promised';
2121
import { isNode } from '@firebase/util';
22-
import { NodeWebSocketHandler } from './node-websocket-handler';
23-
import { WebSocketHandler } from './websocket-handler';
24-
import { MockWebSocketServer } from '../../test-utils/mock-websocket-server';
25-
import { AIError } from '../errors';
2622
import { TextEncoder } from 'util';
23+
import { MockWebSocketServer } from '../../../test-utils/mock-websocket-server';
24+
import { WebSocketHandler } from '../websocket';
25+
import { NodeWebSocketHandler } from './websocket';
26+
import { AIError } from '../../errors';
2727

2828
use(sinonChai);
2929
use(chaiAsPromised);
@@ -144,4 +144,4 @@ describe('NodeWebSocketHandler (Integration Tests)', () => {
144144
await expect(consumerPromise).to.be.fulfilled;
145145
});
146146
});
147-
});
147+
});

packages/ai/src/ws/node-websocket-handler.ts renamed to packages/ai/src/platform/node/websocket.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,53 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { AIError } from '../errors';
19-
import { AIErrorCode } from '../public-types';
20-
import { WebSocketHandler } from './websocket-handler';
18+
// import { WebSocket, MessageEvent } from 'ws'; // External dependency on native Node module
19+
import { AIError } from '../../errors';
20+
import { AIErrorCode } from '../../types';
21+
import { WebSocketHandler } from '../websocket';
22+
23+
export function createWebSocketHandler(): WebSocketHandler {
24+
if (typeof process === 'object' && process.versions?.node) {
25+
const [major] = process.versions.node.split('.').map(Number);
26+
if (major < 22) {
27+
throw new AIError(
28+
AIErrorCode.UNSUPPORTED,
29+
`The "Live" feature is being used in a Node environment, but the ` +
30+
`runtime version is ${process.versions.node}. This feature requires Node >= 22` +
31+
`for native WebSocket support.`
32+
);
33+
}
34+
return new NodeWebSocketHandler();
35+
} else {
36+
throw new AIError(
37+
AIErrorCode.UNSUPPORTED,
38+
'The "Live" feature is not supported in this Node-like environment. It is supported in ' +
39+
'standard browser windows, Web Workers with WebSocket support, and Node >= 22.'
40+
);
41+
}
42+
}
2143

2244
/**
2345
* A WebSocketHandler implementation for Node >= 22.
24-
* It uses the native, built-in 'ws' module, which must be imported.
2546
*
2647
* @internal
2748
*/
2849
export class NodeWebSocketHandler implements WebSocketHandler {
2950
private ws?: import('ws').WebSocket;
3051

3152
async connect(url: string): Promise<void> {
32-
// This dynamic import is why we need a separate class.
33-
// It is only ever executed in a Node environment, preventing browser
34-
// bundlers from attempting to resolve this Node-specific module.
35-
// eslint-disable-next-line import/no-extraneous-dependencies
36-
const { WebSocket } = await import('ws');
37-
38-
return new Promise((resolve, reject) => {
39-
this.ws = new WebSocket(url);
53+
return new Promise(async (resolve, reject) => {
54+
const { WebSocket } = await import('ws');
55+
try {
56+
this.ws = new WebSocket(url);
57+
} catch (e) {
58+
return reject(
59+
new AIError(
60+
AIErrorCode.ERROR,
61+
`Internal Error: Invalid WebSocket URL: ${url}`
62+
)
63+
);
64+
}
4065
this.ws!.addEventListener('open', () => resolve(), { once: true });
4166
this.ws!.addEventListener(
4267
'error',
@@ -84,8 +109,11 @@ export class NodeWebSocketHandler implements WebSocketHandler {
84109
const decoder = new TextDecoder();
85110
textData = decoder.decode(event.data);
86111
} else {
87-
console.warn('Received unexpected WebSocket message type:', event.data);
88-
return;
112+
throw new AIError(
113+
AIErrorCode.PARSE_FAILED,
114+
`Failed to parse WebSocket response to JSON. ` +
115+
`Expected data to be string, Buffer, ArrayBuffer, or Uint8Array, but was ${typeof event.data}.`
116+
);
89117
}
90118

91119
try {
@@ -106,6 +134,7 @@ export class NodeWebSocketHandler implements WebSocketHandler {
106134
resolvePromise();
107135
resolvePromise = null;
108136
}
137+
// Clean up listeners to prevent memory leaks
109138
this.ws?.removeEventListener('message', messageListener);
110139
this.ws?.removeEventListener('close', closeListener);
111140
};
@@ -126,7 +155,11 @@ export class NodeWebSocketHandler implements WebSocketHandler {
126155

127156
close(code?: number, reason?: string): Promise<void> {
128157
return new Promise(resolve => {
129-
if (!this.ws || this.ws.readyState === this.ws.CLOSED) {
158+
if (
159+
!this.ws ||
160+
this.ws.readyState === this.ws.CLOSED ||
161+
this.ws.readyState === this.ws.CLOSING
162+
) {
130163
return resolve();
131164
}
132165
this.ws.addEventListener('close', () => resolve(), { once: true });

packages/ai/src/ws/websocket-handler.ts renamed to packages/ai/src/platform/websocket.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717

1818
import { isBrowser, isNode } from '@firebase/util';
1919
import { AIError } from '../errors';
20-
import { AIErrorCode } from '../public-types';
21-
import { NodeWebSocketHandler } from './node-websocket-handler';
22-
import { BrowserWebSocketHandler } from './browser-websocket-handler';
20+
import { AIErrorCode } from '../types';
21+
import { NodeWebSocketHandler } from './node/websocket';
22+
import { BrowserWebSocketHandler } from './browser/websocket';
2323

2424
/**
2525
* A standardized interface for interacting with a WebSocket connection.
@@ -71,10 +71,7 @@ export interface WebSocketHandler {
7171
*
7272
* 1. Module Loading: The primary difference is how the `WebSocket` class is
7373
* accessed. In browsers, it's a global (`window.WebSocket`). In Node, it
74-
* must be imported from the built-in `'ws'` module. Isolating the Node
75-
* `import('ws')` call in its own class prevents browser-bundling tools
76-
* (like Webpack, Vite) from trying to resolve a Node-specific module, which
77-
* would either fail the build or include unnecessary polyfills.
74+
* must be imported from the built-in `'ws'` module.
7875
*
7976
* 2. Type Safety: TypeScript's type definitions for the browser's WebSocket
8077
* (from `lib.dom.d.ts`) and Node's WebSocket (from `@types/node`) are
@@ -83,11 +80,7 @@ export interface WebSocketHandler {
8380
* @internal
8481
*/
8582
export function createWebSocketHandler(): WebSocketHandler {
86-
// `isNode()` is replaced with a static boolean during build time so this block will be
87-
// tree-shaken in browser builds.
8883
if (isNode()) {
89-
// At this point we're certain we're in a Node bundle, but we still need to have checks
90-
// to be certain we're in a Node environment, and not something like Deno, Bun, or Edge workers.
9184
if (typeof process === 'object' && process.versions?.node) {
9285
const [major] = process.versions.node.split('.').map(Number);
9386
if (major < 22) {
@@ -102,12 +95,7 @@ export function createWebSocketHandler(): WebSocketHandler {
10295
}
10396
}
10497

105-
// `isBrowser()` is replaced with a static boolean during build time so this block will be
106-
// tree-shaken in Node builds.
10798
if (isBrowser()) {
108-
// At this point we're certain we're in a browser build, but we still need to check for the
109-
// existence of the `WebSocket` API. This check would fail in environments that use a browser
110-
// bundle, but don't support WebSockets (Web workers and SSR).
11199
if (typeof WebSocket !== 'undefined') {
112100
return new BrowserWebSocketHandler();
113101
} else {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
export function generateAliasConfig(platform) {
19+
return {
20+
entries: [
21+
{
22+
find: /^(.*)\/platform\/([^.\/]*)(\.ts)?$/,
23+
replacement: `$1\/platform/${platform}/$2.ts`
24+
}
25+
]
26+
};
27+
}

0 commit comments

Comments
 (0)