links-notation 0.26.0 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/README.ru.md +7 -0
- package/dist/index.js +98 -2
- package/index.d.ts +19 -1
- package/package.json +3 -3
- package/src/Emitter.js +134 -0
- package/src/StreamParser.js +2 -2
- package/tests/BrowserBundle.test.js +187 -0
package/README.md
CHANGED
|
@@ -27,6 +27,14 @@ bun add links-notation
|
|
|
27
27
|
import { Parser, Link } from 'npm:links-notation@^0.6.0';
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
#### In browsers
|
|
31
|
+
|
|
32
|
+
The package has no runtime dependencies and imports no Node.js built-in
|
|
33
|
+
modules, so bundlers need no polyfills. For example, esbuild bundles it with
|
|
34
|
+
`esbuild entry.js --bundle --platform=browser`. Before 0.27.1, `StreamParser`
|
|
35
|
+
imported Node's `events` module, and browser bundles needed the `events`
|
|
36
|
+
package installed as a polyfill.
|
|
37
|
+
|
|
30
38
|
### Local Development Setup
|
|
31
39
|
|
|
32
40
|
For contributors working on the source code:
|
|
@@ -443,7 +451,11 @@ Container for grouping related links.
|
|
|
443
451
|
|
|
444
452
|
#### `StreamParser`
|
|
445
453
|
|
|
446
|
-
|
|
454
|
+
Event-emitting streaming parser for incremental parsing. Its built-in emitter
|
|
455
|
+
has the Node.js `EventEmitter` listener methods (`on`, `once`, `off`,
|
|
456
|
+
`addListener`, `removeListener`, `prependListener`, `prependOnceListener`,
|
|
457
|
+
`removeAllListeners`, `emit`, `listeners`, `listenerCount`, `eventNames`) and
|
|
458
|
+
works in browsers without a polyfill. It is not an `instanceof EventEmitter`.
|
|
447
459
|
|
|
448
460
|
- `constructor(options)` - Create a new streaming parser
|
|
449
461
|
- `options.maxInputSize` - Maximum input size in bytes (default: 10MB)
|
package/README.ru.md
CHANGED
|
@@ -174,6 +174,13 @@ stream.end('следующая связь');
|
|
|
174
174
|
`drain()`, `reset()` и ограничение размера незавершённой записи. См.
|
|
175
175
|
[исполняемый пример](../examples/js_streaming_parser.js).
|
|
176
176
|
|
|
177
|
+
Пакет не импортирует встроенные модули Node.js, поэтому сборщикам для браузера
|
|
178
|
+
не нужны полифилы: например, `esbuild entry.js --bundle --platform=browser`.
|
|
179
|
+
Встроенный в `StreamParser` эмиттер поддерживает методы подписки
|
|
180
|
+
`EventEmitter` из Node.js (`on`, `once`, `off`, `removeListener`, `emit`,
|
|
181
|
+
`listenerCount` и другие). До версии 0.27.1 `StreamParser` импортировал модуль
|
|
182
|
+
`events`, и для браузерной сборки требовался пакет-полифил `events`.
|
|
183
|
+
|
|
177
184
|
## Примеры синтаксиса
|
|
178
185
|
|
|
179
186
|
### Дуплеты (2-кортежи)
|
package/dist/index.js
CHANGED
|
@@ -3072,8 +3072,104 @@ function formatIndentedDocument(entries) {
|
|
|
3072
3072
|
`) + `
|
|
3073
3073
|
` : "";
|
|
3074
3074
|
}
|
|
3075
|
+
// src/Emitter.js
|
|
3076
|
+
class Emitter {
|
|
3077
|
+
#listeners = new Map;
|
|
3078
|
+
on(event, listener) {
|
|
3079
|
+
return this.#add(event, listener, false, false);
|
|
3080
|
+
}
|
|
3081
|
+
addListener(event, listener) {
|
|
3082
|
+
return this.on(event, listener);
|
|
3083
|
+
}
|
|
3084
|
+
prependListener(event, listener) {
|
|
3085
|
+
return this.#add(event, listener, false, true);
|
|
3086
|
+
}
|
|
3087
|
+
once(event, listener) {
|
|
3088
|
+
return this.#add(event, listener, true, false);
|
|
3089
|
+
}
|
|
3090
|
+
prependOnceListener(event, listener) {
|
|
3091
|
+
return this.#add(event, listener, true, true);
|
|
3092
|
+
}
|
|
3093
|
+
off(event, listener) {
|
|
3094
|
+
checkListener(listener);
|
|
3095
|
+
const entries = this.#listeners.get(event);
|
|
3096
|
+
if (!entries)
|
|
3097
|
+
return this;
|
|
3098
|
+
const entry = entries.findLast((entry) => entry.listener === listener);
|
|
3099
|
+
if (entry)
|
|
3100
|
+
this.#remove(event, entry);
|
|
3101
|
+
return this;
|
|
3102
|
+
}
|
|
3103
|
+
removeListener(event, listener) {
|
|
3104
|
+
return this.off(event, listener);
|
|
3105
|
+
}
|
|
3106
|
+
removeAllListeners(event) {
|
|
3107
|
+
if (event === undefined) {
|
|
3108
|
+
this.#listeners.clear();
|
|
3109
|
+
} else {
|
|
3110
|
+
this.#listeners.delete(event);
|
|
3111
|
+
}
|
|
3112
|
+
return this;
|
|
3113
|
+
}
|
|
3114
|
+
emit(event, ...arguments_) {
|
|
3115
|
+
const entries = this.#listeners.get(event);
|
|
3116
|
+
if (!entries || entries.length === 0) {
|
|
3117
|
+
if (event === "error") {
|
|
3118
|
+
const error = arguments_[0];
|
|
3119
|
+
if (error instanceof Error)
|
|
3120
|
+
throw error;
|
|
3121
|
+
throw new Error(`Unhandled error. (${String(error)})`);
|
|
3122
|
+
}
|
|
3123
|
+
return false;
|
|
3124
|
+
}
|
|
3125
|
+
for (const entry of [...entries]) {
|
|
3126
|
+
if (entry.once)
|
|
3127
|
+
this.#remove(event, entry);
|
|
3128
|
+
entry.listener.apply(this, arguments_);
|
|
3129
|
+
}
|
|
3130
|
+
return true;
|
|
3131
|
+
}
|
|
3132
|
+
listeners(event) {
|
|
3133
|
+
return (this.#listeners.get(event) ?? []).map((entry) => entry.listener);
|
|
3134
|
+
}
|
|
3135
|
+
listenerCount(event) {
|
|
3136
|
+
return this.#listeners.get(event)?.length ?? 0;
|
|
3137
|
+
}
|
|
3138
|
+
eventNames() {
|
|
3139
|
+
return [...this.#listeners.keys()];
|
|
3140
|
+
}
|
|
3141
|
+
#remove(event, entry) {
|
|
3142
|
+
const entries = this.#listeners.get(event);
|
|
3143
|
+
const index = entries?.indexOf(entry) ?? -1;
|
|
3144
|
+
if (index === -1)
|
|
3145
|
+
return;
|
|
3146
|
+
entries.splice(index, 1);
|
|
3147
|
+
if (entries.length === 0)
|
|
3148
|
+
this.#listeners.delete(event);
|
|
3149
|
+
}
|
|
3150
|
+
#add(event, listener, once, prepend) {
|
|
3151
|
+
checkListener(listener);
|
|
3152
|
+
let entries = this.#listeners.get(event);
|
|
3153
|
+
if (!entries) {
|
|
3154
|
+
entries = [];
|
|
3155
|
+
this.#listeners.set(event, entries);
|
|
3156
|
+
}
|
|
3157
|
+
const entry = { listener, once };
|
|
3158
|
+
if (prepend) {
|
|
3159
|
+
entries.unshift(entry);
|
|
3160
|
+
} else {
|
|
3161
|
+
entries.push(entry);
|
|
3162
|
+
}
|
|
3163
|
+
return this;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
function checkListener(listener) {
|
|
3167
|
+
if (typeof listener !== "function") {
|
|
3168
|
+
throw new TypeError(`The "listener" argument must be a function. Received ${typeof listener}`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3075
3172
|
// src/StreamParser.js
|
|
3076
|
-
import { EventEmitter } from "events";
|
|
3077
3173
|
var DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
3078
3174
|
|
|
3079
3175
|
class StreamParseError extends Error {
|
|
@@ -3094,7 +3190,7 @@ class StreamParseError extends Error {
|
|
|
3094
3190
|
}
|
|
3095
3191
|
}
|
|
3096
3192
|
|
|
3097
|
-
class StreamParser extends
|
|
3193
|
+
class StreamParser extends Emitter {
|
|
3098
3194
|
constructor(options = {}) {
|
|
3099
3195
|
super();
|
|
3100
3196
|
this.parser = options.parser ?? new Parser(options);
|
package/index.d.ts
CHANGED
|
@@ -251,7 +251,10 @@ export class StreamParseError extends Error {
|
|
|
251
251
|
snippet: string;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* Incrementally emits complete top-level Links Notation records. Its listener
|
|
256
|
+
* methods mirror Node.js EventEmitter without depending on Node built-ins.
|
|
257
|
+
*/
|
|
255
258
|
export class StreamParser {
|
|
256
259
|
parser: Parser;
|
|
257
260
|
comments: boolean;
|
|
@@ -282,6 +285,21 @@ export class StreamParser {
|
|
|
282
285
|
once(event: 'end', listener: (links: Link[]) => void): this;
|
|
283
286
|
once(event: 'error', listener: (error: Error) => void): this;
|
|
284
287
|
off(event: string, listener: (...arguments_: any[]) => void): this;
|
|
288
|
+
addListener(event: string, listener: (...arguments_: any[]) => void): this;
|
|
289
|
+
removeListener(event: string, listener: (...arguments_: any[]) => void): this;
|
|
290
|
+
prependListener(
|
|
291
|
+
event: string,
|
|
292
|
+
listener: (...arguments_: any[]) => void
|
|
293
|
+
): this;
|
|
294
|
+
prependOnceListener(
|
|
295
|
+
event: string,
|
|
296
|
+
listener: (...arguments_: any[]) => void
|
|
297
|
+
): this;
|
|
298
|
+
removeAllListeners(event?: string): this;
|
|
299
|
+
emit(event: string, ...arguments_: any[]): boolean;
|
|
300
|
+
listeners(event: string): Array<(...arguments_: any[]) => void>;
|
|
301
|
+
listenerCount(event: string): number;
|
|
302
|
+
eventNames(): string[];
|
|
285
303
|
|
|
286
304
|
/** Lazily parse a synchronous iterable of chunks. */
|
|
287
305
|
static parse(
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "links-notation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Links Notation parser for JavaScript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "bun run build:grammar && bun build src/index.js --outdir=dist --target=
|
|
9
|
+
"build": "bun run build:grammar && bun build src/index.js --outdir=dist --target=browser",
|
|
10
10
|
"build:grammar": "peggy --format es -o src/parser-generated.js src/grammar.pegjs",
|
|
11
11
|
"test": "bun test",
|
|
12
12
|
"test:watch": "bun test --watch",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"homepage": "https://github.com/link-foundation/links-notation#readme",
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"bun-types": "^1.4.3",
|
|
36
|
-
"eslint": "^10.
|
|
36
|
+
"eslint": "^10.13.0",
|
|
37
37
|
"peggy": "^5.1.0",
|
|
38
38
|
"prettier": "^3.9.10",
|
|
39
39
|
"typescript": "^7.0.2"
|
package/src/Emitter.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dependency-free event emitter with the Node.js `EventEmitter` methods that
|
|
3
|
+
* StreamParser documents.
|
|
4
|
+
*
|
|
5
|
+
* Importing Node's built-in `events` module made every bundle of the package
|
|
6
|
+
* entry point, including parser-only browser bundles, fail to resolve `events`
|
|
7
|
+
* unless the consumer installed a polyfill (issue #351). This class runs
|
|
8
|
+
* unchanged in browsers, Node.js, Bun, and Deno. It keeps Node's semantics:
|
|
9
|
+
* listeners run synchronously in registration order, and emitting `error`
|
|
10
|
+
* without a listener throws the error. It also works with Node's
|
|
11
|
+
* `events.once()` and `events.on()` helpers.
|
|
12
|
+
*/
|
|
13
|
+
export class Emitter {
|
|
14
|
+
#listeners = new Map();
|
|
15
|
+
|
|
16
|
+
/** Add a listener for an event. */
|
|
17
|
+
on(event, listener) {
|
|
18
|
+
return this.#add(event, listener, false, false);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Alias of on(), as in Node.js. */
|
|
22
|
+
addListener(event, listener) {
|
|
23
|
+
return this.on(event, listener);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Add a listener that runs before the existing ones. */
|
|
27
|
+
prependListener(event, listener) {
|
|
28
|
+
return this.#add(event, listener, false, true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Add a listener that is removed after its first call. */
|
|
32
|
+
once(event, listener) {
|
|
33
|
+
return this.#add(event, listener, true, false);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Add a one-time listener that runs before the existing ones. */
|
|
37
|
+
prependOnceListener(event, listener) {
|
|
38
|
+
return this.#add(event, listener, true, true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Remove the most recently added registration of a listener. */
|
|
42
|
+
off(event, listener) {
|
|
43
|
+
checkListener(listener);
|
|
44
|
+
const entries = this.#listeners.get(event);
|
|
45
|
+
if (!entries) return this;
|
|
46
|
+
const entry = entries.findLast((entry) => entry.listener === listener);
|
|
47
|
+
if (entry) this.#remove(event, entry);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Alias of off(), as in Node.js. */
|
|
52
|
+
removeListener(event, listener) {
|
|
53
|
+
return this.off(event, listener);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Remove the listeners of one event, or of every event. */
|
|
57
|
+
removeAllListeners(event) {
|
|
58
|
+
if (event === undefined) {
|
|
59
|
+
this.#listeners.clear();
|
|
60
|
+
} else {
|
|
61
|
+
this.#listeners.delete(event);
|
|
62
|
+
}
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Call the listeners of an event with the given arguments and return whether
|
|
68
|
+
* there were any. An `error` event without listeners throws its argument.
|
|
69
|
+
*/
|
|
70
|
+
emit(event, ...arguments_) {
|
|
71
|
+
const entries = this.#listeners.get(event);
|
|
72
|
+
if (!entries || entries.length === 0) {
|
|
73
|
+
if (event === 'error') {
|
|
74
|
+
const error = arguments_[0];
|
|
75
|
+
if (error instanceof Error) throw error;
|
|
76
|
+
throw new Error(`Unhandled error. (${String(error)})`);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
// Listeners added or removed while emitting take effect on the next emit.
|
|
81
|
+
for (const entry of [...entries]) {
|
|
82
|
+
if (entry.once) this.#remove(event, entry);
|
|
83
|
+
entry.listener.apply(this, arguments_);
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Return a copy of the listeners of an event. */
|
|
89
|
+
listeners(event) {
|
|
90
|
+
return (this.#listeners.get(event) ?? []).map((entry) => entry.listener);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Return the number of listeners of an event. */
|
|
94
|
+
listenerCount(event) {
|
|
95
|
+
return this.#listeners.get(event)?.length ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Return the names of events that have listeners. */
|
|
99
|
+
eventNames() {
|
|
100
|
+
return [...this.#listeners.keys()];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#remove(event, entry) {
|
|
104
|
+
const entries = this.#listeners.get(event);
|
|
105
|
+
const index = entries?.indexOf(entry) ?? -1;
|
|
106
|
+
if (index === -1) return;
|
|
107
|
+
entries.splice(index, 1);
|
|
108
|
+
if (entries.length === 0) this.#listeners.delete(event);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#add(event, listener, once, prepend) {
|
|
112
|
+
checkListener(listener);
|
|
113
|
+
let entries = this.#listeners.get(event);
|
|
114
|
+
if (!entries) {
|
|
115
|
+
entries = [];
|
|
116
|
+
this.#listeners.set(event, entries);
|
|
117
|
+
}
|
|
118
|
+
const entry = { listener, once };
|
|
119
|
+
if (prepend) {
|
|
120
|
+
entries.unshift(entry);
|
|
121
|
+
} else {
|
|
122
|
+
entries.push(entry);
|
|
123
|
+
}
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function checkListener(listener) {
|
|
129
|
+
if (typeof listener !== 'function') {
|
|
130
|
+
throw new TypeError(
|
|
131
|
+
`The "listener" argument must be a function. Received ${typeof listener}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/StreamParser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Emitter } from './Emitter.js';
|
|
2
2
|
import { Parser } from './Parser.js';
|
|
3
3
|
import { DelimitedReferences } from './quotes.js';
|
|
4
4
|
|
|
@@ -39,7 +39,7 @@ export class StreamParseError extends Error {
|
|
|
39
39
|
* uses the regular Parser as the source of truth, which keeps quoted strings,
|
|
40
40
|
* nested contexts, comments, and future grammar changes in one place.
|
|
41
41
|
*/
|
|
42
|
-
export class StreamParser extends
|
|
42
|
+
export class StreamParser extends Emitter {
|
|
43
43
|
constructor(options = {}) {
|
|
44
44
|
super();
|
|
45
45
|
this.parser = options.parser ?? new Parser(options);
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { on, once } from 'node:events';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
5
|
+
import { Emitter } from '../src/Emitter.js';
|
|
6
|
+
import { Parser, StreamParser } from '../src/index.js';
|
|
7
|
+
|
|
8
|
+
const source = fileURLToPath(new URL('../src/index.js', import.meta.url));
|
|
9
|
+
const published = fileURLToPath(new URL('../dist/index.js', import.meta.url));
|
|
10
|
+
|
|
11
|
+
// Bundlers such as esbuild (`--platform=browser`) cannot resolve Node built-ins
|
|
12
|
+
// like `events`, so any bare import in the package graph breaks browser builds
|
|
13
|
+
// unless consumers add a polyfill (issue #351). Bun's own browser target would
|
|
14
|
+
// silently polyfill them, so this plugin fails on every bare import instead.
|
|
15
|
+
const rejectBareImports = (imports) => ({
|
|
16
|
+
name: 'reject-bare-imports',
|
|
17
|
+
setup(build) {
|
|
18
|
+
build.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
19
|
+
imports.push(`${args.path} (from ${args.importer})`);
|
|
20
|
+
return { path: args.path, external: true };
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function bundleForBrowser(entrypoint) {
|
|
26
|
+
const imports = [];
|
|
27
|
+
const result = await globalThis.Bun.build({
|
|
28
|
+
entrypoints: [entrypoint],
|
|
29
|
+
target: 'browser',
|
|
30
|
+
plugins: [rejectBareImports(imports)],
|
|
31
|
+
});
|
|
32
|
+
return { result, imports };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('browser bundling (issue #351)', () => {
|
|
36
|
+
test('the source entry point imports no Node built-ins or packages', async () => {
|
|
37
|
+
const { result, imports } = await bundleForBrowser(source);
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
expect(imports).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('the published dist entry point imports no Node built-ins or packages', async () => {
|
|
43
|
+
const { result, imports } = await bundleForBrowser(published);
|
|
44
|
+
expect(result.success).toBe(true);
|
|
45
|
+
expect(imports).toEqual([]);
|
|
46
|
+
|
|
47
|
+
const text = readFileSync(published, 'utf8');
|
|
48
|
+
expect(text).not.toMatch(/^\s*import\s[^;]*from\s*["'][^./]/m);
|
|
49
|
+
expect(text).not.toMatch(/\brequire\(\s*["']/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('the dist bundle parses and streams like the source', async () => {
|
|
53
|
+
const dist = await import(published);
|
|
54
|
+
const input = '(law: text)\nnext value';
|
|
55
|
+
expect(dist.formatLinks(new dist.Parser().parse(input))).toBe(
|
|
56
|
+
dist.formatLinks(new Parser().parse(input))
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const stream = new dist.StreamParser();
|
|
60
|
+
const seen = [];
|
|
61
|
+
stream.on('link', (link) => seen.push(link.toString()));
|
|
62
|
+
stream.end(input);
|
|
63
|
+
expect(seen).toEqual(['(law: text)', '(next value)']);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Emitter', () => {
|
|
68
|
+
test('calls listeners in order with the emitter as this', () => {
|
|
69
|
+
const emitter = new Emitter();
|
|
70
|
+
const calls = [];
|
|
71
|
+
emitter.on('event', function (value) {
|
|
72
|
+
calls.push(['first', value, this === emitter]);
|
|
73
|
+
});
|
|
74
|
+
emitter.addListener('event', (value) => calls.push(['second', value]));
|
|
75
|
+
emitter.prependListener('event', (value) => calls.push(['zeroth', value]));
|
|
76
|
+
|
|
77
|
+
expect(emitter.emit('event', 1)).toBe(true);
|
|
78
|
+
expect(calls).toEqual([
|
|
79
|
+
['zeroth', 1],
|
|
80
|
+
['first', 1, true],
|
|
81
|
+
['second', 1],
|
|
82
|
+
]);
|
|
83
|
+
expect(emitter.emit('missing')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('once listeners run a single time', () => {
|
|
87
|
+
const emitter = new Emitter();
|
|
88
|
+
const calls = [];
|
|
89
|
+
emitter.once('event', (value) => calls.push(`once ${value}`));
|
|
90
|
+
emitter.prependOnceListener('event', (value) => calls.push(`pre ${value}`));
|
|
91
|
+
emitter.emit('event', 1);
|
|
92
|
+
emitter.emit('event', 2);
|
|
93
|
+
expect(calls).toEqual(['pre 1', 'once 1']);
|
|
94
|
+
expect(emitter.listenerCount('event')).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('off removes the latest registration, including once listeners', () => {
|
|
98
|
+
const emitter = new Emitter();
|
|
99
|
+
const calls = [];
|
|
100
|
+
const listener = (value) => calls.push(value);
|
|
101
|
+
emitter.on('event', listener);
|
|
102
|
+
emitter.on('event', listener);
|
|
103
|
+
emitter.off('event', listener);
|
|
104
|
+
emitter.emit('event', 'a');
|
|
105
|
+
emitter.removeListener('event', listener);
|
|
106
|
+
emitter.emit('event', 'b');
|
|
107
|
+
emitter.once('event', listener);
|
|
108
|
+
emitter.off('event', listener);
|
|
109
|
+
emitter.emit('event', 'c');
|
|
110
|
+
expect(calls).toEqual(['a']);
|
|
111
|
+
expect(emitter.eventNames()).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('listener changes during emit take effect on the next emit', () => {
|
|
115
|
+
const emitter = new Emitter();
|
|
116
|
+
const calls = [];
|
|
117
|
+
const second = () => calls.push('second');
|
|
118
|
+
emitter.on('event', () => {
|
|
119
|
+
calls.push('first');
|
|
120
|
+
emitter.off('event', second);
|
|
121
|
+
emitter.on('event', () => calls.push('added'));
|
|
122
|
+
});
|
|
123
|
+
emitter.on('event', second);
|
|
124
|
+
emitter.emit('event');
|
|
125
|
+
expect(calls).toEqual(['first', 'second']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('inspects and removes listeners', () => {
|
|
129
|
+
const emitter = new Emitter();
|
|
130
|
+
const listener = () => {};
|
|
131
|
+
emitter.on('a', listener).on('b', listener).on('b', listener);
|
|
132
|
+
expect(emitter.listeners('b')).toEqual([listener, listener]);
|
|
133
|
+
expect(emitter.listenerCount('b')).toBe(2);
|
|
134
|
+
expect(emitter.eventNames()).toEqual(['a', 'b']);
|
|
135
|
+
emitter.removeAllListeners('b');
|
|
136
|
+
expect(emitter.eventNames()).toEqual(['a']);
|
|
137
|
+
emitter.removeAllListeners();
|
|
138
|
+
expect(emitter.listenerCount('a')).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('an unhandled error event throws like Node.js', () => {
|
|
142
|
+
const emitter = new Emitter();
|
|
143
|
+
const error = new Error('boom');
|
|
144
|
+
expect(() => emitter.emit('error', error)).toThrow(error);
|
|
145
|
+
expect(() => emitter.emit('error', 'text')).toThrow(
|
|
146
|
+
'Unhandled error. (text)'
|
|
147
|
+
);
|
|
148
|
+
emitter.on('error', () => {});
|
|
149
|
+
expect(emitter.emit('error', error)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('rejects listeners that are not functions', () => {
|
|
153
|
+
expect(() => new Emitter().on('event', 'nope')).toThrow(TypeError);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('works with the Node.js events.once helper', async () => {
|
|
157
|
+
const stream = new StreamParser();
|
|
158
|
+
const ended = once(stream, 'end');
|
|
159
|
+
stream.end('a b');
|
|
160
|
+
const [links] = await ended;
|
|
161
|
+
expect(links.map(String)).toEqual(['(a b)']);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('works with the Node.js events.on async iterator', async () => {
|
|
165
|
+
const stream = new StreamParser();
|
|
166
|
+
const controller = new globalThis.AbortController();
|
|
167
|
+
const links = on(stream, 'link', { signal: controller.signal });
|
|
168
|
+
globalThis.setTimeout(() => stream.end('a b\nc d'), 0);
|
|
169
|
+
const seen = [];
|
|
170
|
+
try {
|
|
171
|
+
for await (const [link] of links) {
|
|
172
|
+
seen.push(String(link));
|
|
173
|
+
if (seen.length === 2) controller.abort();
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error.name !== 'AbortError') throw error;
|
|
177
|
+
}
|
|
178
|
+
expect(seen).toEqual(['(a b)', '(c d)']);
|
|
179
|
+
expect(stream.listenerCount('link')).toBe(0);
|
|
180
|
+
expect(stream.listenerCount('error')).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('StreamParser still throws the stream error without a listener', () => {
|
|
184
|
+
const stream = new StreamParser();
|
|
185
|
+
expect(() => stream.write(1)).toThrow(TypeError);
|
|
186
|
+
});
|
|
187
|
+
});
|