Skip to content

Commit 57cef65

Browse files
committed
Use 'EventEmitter' with generics
1 parent b67261c commit 57cef65

File tree

7 files changed

+142
-119
lines changed

7 files changed

+142
-119
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ In next release ...
44
connected when the promise returns. The `Client` symbol is now a
55
type instead of a value.
66

7+
- Add `off` method to disable event listening.
8+
9+
- Remove dependency on
10+
[ts-typed-events](https://www.npmjs.com/package/ts-typed-events),
11+
which has been supplanted by updated typings for the built-in
12+
`EventEmitter` class that's now generic.
13+
714
## v1.9.0 (2024-01-23)
815

916
- Add support for ESM modules.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ $ npm install ts-postgres
2323
- Rows and column names
2424
- Streaming data directly into a socket
2525
- Supports CommonJS and ESM modules
26+
- No dependencies
2627

2728
See the [documentation](https://malthe.github.io/ts-postgres/) for a complete reference.
2829

package.json

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
],
1313
"homepage": "https://github.com/malthe/ts-postgres",
1414
"author": "Malthe Borch <[email protected]>",
15-
"dependencies": {
16-
"ts-typed-events": "^3.0.0"
17-
},
1815
"main": "./dist/module/index.js",
1916
"types": "./dist/module/index.d.ts",
2017
"exports": {
@@ -57,20 +54,23 @@
5754
]
5855
},
5956
"devDependencies": {
60-
"@types/node": "^20.10.6",
61-
"@typescript-eslint/eslint-plugin": "^6.10.0",
62-
"@typescript-eslint/parser": "^6.10.0",
63-
"colors": "^1.4.0",
64-
"eslint": "^8.56.0",
65-
"eslint-config-standard": "^17.1.0",
66-
"eslint-plugin-import": "^2.29.0",
67-
"eslint-plugin-node": "^11.1.0",
68-
"eslint-plugin-prettier": "^5.0.1",
69-
"eslint-plugin-promise": "^6.1.1",
57+
"@types/node": "^20.11.21",
58+
"@typescript-eslint/eslint-plugin": "^6",
59+
"@typescript-eslint/parser": "^6",
60+
"colors": "^1",
61+
"eslint": "^8",
62+
"eslint-config-standard": "^17",
63+
"eslint-plugin-import": "^2",
64+
"eslint-plugin-node": "^11",
65+
"eslint-plugin-prettier": "^5",
66+
"eslint-plugin-promise": "^6",
7067
"lint-staged": "^15.0.2",
71-
"rimraf": "^3.0.2",
72-
"ts-node": "^10.9.2",
73-
"typedoc": "^0.25.4",
74-
"typescript": "^5.2.2"
68+
"rimraf": "~3.0",
69+
"ts-node": "~10.9",
70+
"typedoc": "~0.25",
71+
"typescript": "~5.2"
72+
},
73+
"peerDependencies": {
74+
"@types/node": "^20"
7575
}
7676
}

src/client.ts

Lines changed: 74 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {
1010
connect as tls,
1111
createSecureContext,
1212
} from 'node:tls';
13-
14-
import { Event as TypedEvent } from 'ts-typed-events';
13+
import { EventEmitter } from 'node:events';
1514

1615
import { Defaults, Environment } from './defaults.js';
1716
import * as logger from './logging.js';
@@ -94,7 +93,7 @@ export interface Configuration
9493
export interface Notification {
9594
processId: number;
9695
channel: string;
97-
payload?: string;
96+
payload: string;
9897
}
9998

10099
export interface PreparedStatement<T = ResultRecord> {
@@ -107,17 +106,6 @@ export interface PreparedStatement<T = ResultRecord> {
107106
) => ResultIterator<T>;
108107
}
109108

110-
export type Callback<T> = (data: T) => void;
111-
112-
/* eslint-disable @typescript-eslint/no-explicit-any */
113-
type CallbackOf<U> = U extends any ? Callback<U> : never;
114-
115-
type Event = ClientNotice | DatabaseError | Notification;
116-
117-
type Connect = Error | null;
118-
119-
type End = NodeJS.ErrnoException | null;
120-
121109
type CloseHandler = () => void;
122110

123111
interface RowDataHandler {
@@ -161,20 +149,31 @@ interface PreFlightQueue {
161149

162150
const DEFAULTS = new Defaults(env as unknown as Environment);
163151

152+
export type EventMap<
153+
T = {
154+
error: DatabaseError;
155+
notice: ClientNotice;
156+
notification: Notification;
157+
},
158+
> = {
159+
[K in keyof T]: [T[K]];
160+
};
161+
162+
type Resolve<T> = (value?: T) => void;
163+
164+
export type EventListener<K> = K extends keyof EventMap ? (
165+
(...args: EventMap[K]) => void
166+
) : never;
167+
164168
export class ClientImpl {
165-
private readonly events = {
166-
connect: new TypedEvent<Connect>(),
167-
end: new TypedEvent<End>(),
168-
error: new TypedEvent<DatabaseError>(),
169-
notice: new TypedEvent<ClientNotice>(),
170-
notification: new TypedEvent<Notification>(),
171-
};
172-
173-
private ending = false;
169+
private readonly events = new EventEmitter<EventMap>();
170+
174171
private connected = false;
175-
private connecting = false;
176172
private error = false;
177173

174+
private ending?: Resolve<NodeJS.ErrnoException>;
175+
private connecting?: Resolve<Error>;
176+
178177
private readonly encoding: BufferEncoding;
179178
private readonly writer: Writer;
180179

@@ -217,7 +216,7 @@ export class ClientImpl {
217216

218217
this.stream.on('close', () => {
219218
this.closed = true;
220-
this.events.end.emit(null);
219+
this.ending?.();
221220
});
222221

223222
this.stream.on('connect', () => {
@@ -237,14 +236,14 @@ export class ClientImpl {
237236
/* istanbul ignore next */
238237
this.stream.on('error', (error: NodeJS.ErrnoException) => {
239238
if (this.connecting) {
240-
this.events.connect.emit(error);
239+
this.connecting(error);
241240
} else {
242241
// Don't raise ECONNRESET errors - they can & should be
243242
// ignored during disconnect.
244-
if (this.ending && error.errno === constants.errno.ECONNRESET)
245-
return;
246-
247-
this.events.end.emit(error);
243+
if (this.ending) {
244+
if (error.errno === constants.errno.ECONNRESET) return;
245+
this.ending();
246+
}
248247
}
249248
});
250249

@@ -294,7 +293,7 @@ export class ClientImpl {
294293

295294
const abort = (error: Error) => {
296295
this.handleError(error);
297-
this.events.connect.emit(error);
296+
this.connecting?.(error);
298297
};
299298

300299
const startup = (stream?: Socket) => {
@@ -384,12 +383,14 @@ export class ClientImpl {
384383
const read = this.handle(buffer, offset, size);
385384
offset += read;
386385
remaining = size - read;
387-
} catch (error) {
386+
} catch (error: unknown) {
388387
logger.warn(error);
389388
if (this.connecting) {
390-
this.events.connect.emit(error as Error);
389+
this.connecting(error as Error);
391390
} else {
392391
try {
392+
// In normal operation (including regular handling of errors),
393+
// there's nothing further to clean up at this point.
393394
while (this.handleError(error as Error)) {
394395
logger.info(
395396
'Cancelled query due to an internal error',
@@ -420,20 +421,25 @@ export class ClientImpl {
420421
if (this.error) {
421422
throw new Error("Can't connect in error state");
422423
}
423-
this.connecting = true;
424424

425425
const timeout =
426426
this.config.connectionTimeout ?? DEFAULTS.connectionTimeout;
427427

428-
let p = this.events.connect.once().then((error: Connect) => {
429-
if (error) {
430-
this.connecting = false;
431-
this.stream.destroy();
432-
throw error;
433-
}
434-
return {
435-
encrypted: this.stream instanceof TLSSocket,
436-
parameters: this.parameters as ReadonlyMap<string, string>,
428+
let p = new Promise<ConnectionInfo>((resolve, reject) => {
429+
this.connecting = (error?: Error) => {
430+
this.connecting = undefined;
431+
if (error) {
432+
this.stream.destroy();
433+
reject(error);
434+
} else {
435+
resolve({
436+
encrypted: this.stream instanceof TLSSocket,
437+
parameters: this.parameters as ReadonlyMap<
438+
string,
439+
string
440+
>,
441+
});
442+
}
437443
};
438444
});
439445

@@ -476,8 +482,6 @@ export class ClientImpl {
476482
throw new Error('Connection unexpectedly destroyed');
477483
}
478484

479-
this.ending = true;
480-
481485
if (this.connected) {
482486
this.writer.end();
483487
this.send();
@@ -486,32 +490,21 @@ export class ClientImpl {
486490
} else {
487491
this.stream.destroy();
488492
}
489-
return new Promise<void>((resolve, reject) =>
490-
this.events.end.once().then((value) => {
491-
if (value === null) resolve();
492-
reject(value);
493-
}),
494-
);
493+
return new Promise<void>((resolve, reject) => {
494+
this.ending = (error?: NodeJS.ErrnoException) => {
495+
this.ending = undefined;
496+
if (!error) resolve();
497+
reject(error);
498+
};
499+
});
495500
}
496501

497-
on(event: 'notification', callback: Callback<Notification>): void;
498-
on(event: 'error', callback: Callback<DatabaseError>): void;
499-
on(event: 'notice', callback: Callback<ClientNotice>): void;
500-
on(event: string, callback: CallbackOf<Event>): void {
501-
switch (event) {
502-
case 'error': {
503-
this.events.error.on(callback as Callback<DatabaseError>);
504-
break;
505-
}
506-
case 'notice': {
507-
this.events.notice.on(callback as Callback<ClientNotice>);
508-
break;
509-
}
510-
case 'notification': {
511-
this.events.notification.on(callback as Callback<Notification>);
512-
break;
513-
}
514-
}
502+
on<K extends keyof EventMap>(event: K, listener: EventListener<K>): void {
503+
this.events.on(event, listener);
504+
}
505+
506+
off<K extends keyof EventMap>(event: K, listener: EventListener<K>): void {
507+
this.events.off(event, listener);
515508
}
516509

517510
/** Prepare a statement for later execution.
@@ -954,7 +947,7 @@ export class ClientImpl {
954947
outer: switch (code) {
955948
case 0: {
956949
nextTick(() => {
957-
this.events.connect.emit(null);
950+
this.connecting?.();
958951
});
959952
break;
960953
}
@@ -1094,10 +1087,16 @@ export class ClientImpl {
10941087

10951088
if (this.connecting) throw error;
10961089

1097-
this.events.error.emit(error);
1098-
loop: if (!this.handleError(error)) {
1090+
try {
1091+
this.events.emit('error', error);
1092+
} catch {
1093+
// If there are no subscribers for the event, an error
1094+
// is raised. We're not interesting in this behavior.
1095+
}
1096+
1097+
if (!this.handleError(error)) {
10991098
throw new Error(
1100-
'Internal error occurred while processing database error',
1099+
'An error occurred without an active query',
11011100
);
11021101
}
11031102
break;
@@ -1106,15 +1105,15 @@ export class ClientImpl {
11061105
const notice = this.parseError(
11071106
buffer.subarray(start, start + length),
11081107
);
1109-
this.events.notice.emit(notice);
1108+
this.events.emit('notice', notice);
11101109
break;
11111110
}
11121111
case Message.NotificationResponse: {
11131112
const reader = new Reader(buffer, start);
11141113
const processId = reader.readInt32BE();
11151114
const channel = reader.readCString(this.encoding);
11161115
const payload = reader.readCString(this.encoding);
1117-
this.events.notification.emit({
1116+
this.events.emit('notification', {
11181117
processId: processId,
11191118
channel: channel,
11201119
payload: payload,
@@ -1150,7 +1149,6 @@ export class ClientImpl {
11501149
this.errorHandlerQueue.shift();
11511150
this.cleanupQueue.expect(Cleanup.ErrorHandler);
11521151
} else {
1153-
this.connecting = false;
11541152
this.connected = true;
11551153
}
11561154
const status = buffer.readInt8(start);

src/index.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
import { ClientImpl, SSLMode } from './client.js';
2-
import type { Configuration, ConnectionInfo } from './client.js';
3-
export type {
4-
Callback,
5-
ClientNotice,
6-
DataTypeError,
7-
Notification,
8-
PreparedStatement,
9-
SSL,
1+
import { ClientImpl, type ConnectionInfo } from './client.js';
2+
export {
3+
SSLMode,
4+
type ClientNotice,
5+
type Configuration,
6+
type DataTypeError,
7+
type EventListener,
8+
type EventMap,
9+
type Notification,
10+
type PreparedStatement,
11+
type SSL,
1012
} from './client.js';
11-
export type { BufferEncoding, Point, ValueTypeReader } from './types.js';
13+
import type { Configuration } from './client.js';
14+
export {
15+
DataFormat,
16+
DataType,
17+
type BufferEncoding,
18+
type Point,
19+
type ValueTypeReader,
20+
} from './types.js';
1221
export type { Query, QueryOptions } from './query.js';
1322
export type {
1423
Result,
@@ -25,12 +34,6 @@ export type {
2534
TransactionStatus,
2635
} from './protocol.js';
2736

28-
export type { Configuration };
29-
30-
export { DataFormat, DataType } from './types.js';
31-
32-
export { SSLMode };
33-
3437
interface _Client extends ClientImpl {}
3538

3639
/** A database client, encapsulating a single connection to the database.

0 commit comments

Comments
 (0)