@tstdl/base 0.93.138 → 0.93.139
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/cancellation/tests/leak.test.d.ts +1 -0
- package/cancellation/tests/leak.test.js +40 -0
- package/cancellation/token.js +12 -5
- package/injector/injector.d.ts +1 -0
- package/injector/injector.js +17 -5
- package/injector/tests/leak.test.d.ts +1 -0
- package/injector/tests/leak.test.js +45 -0
- package/package.json +1 -1
- package/task-queue/task-queue.js +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CancellationToken } from '../token.js';
|
|
3
|
+
describe('CancellationToken Memory Leak', () => {
|
|
4
|
+
it('should not leak subscriptions in connect', () => {
|
|
5
|
+
const parent = new CancellationToken();
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
const initialParentSubscribers = parent._stateSubject.observers.length;
|
|
8
|
+
for (let i = 0; i < 1000; i++) {
|
|
9
|
+
const child = new CancellationToken();
|
|
10
|
+
parent.connect(child, { once: true });
|
|
11
|
+
child.set();
|
|
12
|
+
}
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
expect(parent._stateSubject.observers.length).toBeLessThan(initialParentSubscribers + 10);
|
|
15
|
+
});
|
|
16
|
+
it('should not leak when child is completed', () => {
|
|
17
|
+
const parent = new CancellationToken();
|
|
18
|
+
const child = new CancellationToken();
|
|
19
|
+
parent.connect(child);
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
expect(parent._stateSubject.observers.length).toBe(1);
|
|
22
|
+
child.complete();
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
expect(parent._stateSubject.observers.length).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
it('should not leak when parent is completed', () => {
|
|
27
|
+
const parent = new CancellationToken();
|
|
28
|
+
const child = new CancellationToken();
|
|
29
|
+
parent.connect(child);
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
expect(parent._stateSubject.observers.length).toBeGreaterThan(0);
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
expect(child._stateSubject.observers.length).toBeGreaterThan(0);
|
|
34
|
+
parent.complete();
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
expect(parent._stateSubject.observers.length).toBe(0);
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
expect(child._stateSubject.observers.length).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/cancellation/token.js
CHANGED
|
@@ -188,16 +188,23 @@ export class CancellationToken extends CancellationSignal {
|
|
|
188
188
|
if (once) {
|
|
189
189
|
stateObservable = stateObservable.pipe(take(1));
|
|
190
190
|
}
|
|
191
|
+
const targetRef = new WeakRef(target);
|
|
191
192
|
const subscription = stateObservable.subscribe({
|
|
192
|
-
next: (state) =>
|
|
193
|
-
error: error ? (errorValue) =>
|
|
194
|
-
complete: complete ? () =>
|
|
193
|
+
next: (state) => targetRef.deref()?.setState(state),
|
|
194
|
+
error: error ? (errorValue) => targetRef.deref()?.error(errorValue) : noop,
|
|
195
|
+
complete: complete ? () => targetRef.deref()?.complete() : noop,
|
|
195
196
|
});
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
const targetSubscription = target._stateSubject.subscribe({
|
|
198
|
+
next: (state) => {
|
|
199
|
+
if (state && once) {
|
|
200
|
+
subscription.unsubscribe();
|
|
201
|
+
}
|
|
202
|
+
},
|
|
198
203
|
error: () => subscription.unsubscribe(),
|
|
199
204
|
complete: () => subscription.unsubscribe(),
|
|
200
205
|
});
|
|
206
|
+
subscription.add(targetSubscription);
|
|
207
|
+
registerFinalization(target, (sub) => sub.unsubscribe(), subscription);
|
|
201
208
|
}
|
|
202
209
|
/**
|
|
203
210
|
* Makes this token a child of a parent token.
|
package/injector/injector.d.ts
CHANGED
package/injector/injector.js
CHANGED
|
@@ -38,6 +38,9 @@ export class Injector {
|
|
|
38
38
|
get parent() {
|
|
39
39
|
return this.#parent;
|
|
40
40
|
}
|
|
41
|
+
get children() {
|
|
42
|
+
return this.#children;
|
|
43
|
+
}
|
|
41
44
|
get accesses() {
|
|
42
45
|
return this.#accesses;
|
|
43
46
|
}
|
|
@@ -50,9 +53,12 @@ export class Injector {
|
|
|
50
53
|
this.ownerToken = ownerToken;
|
|
51
54
|
this.register(Injector, { useValue: this });
|
|
52
55
|
this.register(CancellationSignal, { useValue: this.#disposeToken.signal });
|
|
53
|
-
this.#disposableStack.defer(() =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
this.#disposableStack.defer(() => {
|
|
57
|
+
this.#registrations.clear();
|
|
58
|
+
this.#injectorScopedResolutions.clear();
|
|
59
|
+
this.#disposableStackRegistrations.clear();
|
|
60
|
+
this.#accesses.length = 0;
|
|
61
|
+
});
|
|
56
62
|
}
|
|
57
63
|
/**
|
|
58
64
|
* Add a dispose handler to this injector. The handler can be a disposable, async disposable, or a simple function. If the handler is a disposable or async disposable, it will be disposed when the injector is disposed.
|
|
@@ -120,6 +126,12 @@ export class Injector {
|
|
|
120
126
|
const child = new Injector(name, this, ownerToken);
|
|
121
127
|
this.#children.push(child);
|
|
122
128
|
this.#disposableStack.use(child);
|
|
129
|
+
child.addDisposeHandler(() => {
|
|
130
|
+
const index = this.#children.indexOf(child);
|
|
131
|
+
if (index != -1) {
|
|
132
|
+
this.#children.splice(index, 1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
123
135
|
return child;
|
|
124
136
|
}
|
|
125
137
|
/**
|
|
@@ -395,8 +407,8 @@ export class Injector {
|
|
|
395
407
|
context.recordAccess(chain.withMetadata({ isCached: true }));
|
|
396
408
|
return registration.resolutions.get(argumentIdentity);
|
|
397
409
|
}
|
|
398
|
-
// A new scope is only needed if
|
|
399
|
-
const needsNewScope =
|
|
410
|
+
// A new scope is only needed if the registration explicitly defines scoped providers.
|
|
411
|
+
const needsNewScope = (providers.length > 0);
|
|
400
412
|
const injector = needsNewScope ? this.fork(`${getTokenName(token)}Injector`, token) : this;
|
|
401
413
|
for (const nestedProvider of providers) {
|
|
402
414
|
injector.registerSingleton(nestedProvider.provide, nestedProvider, { multi: nestedProvider.multi });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { Injectable } from '../decorators.js';
|
|
9
|
+
import { Injector } from '../injector.js';
|
|
10
|
+
import { injectionToken } from '../token.js';
|
|
11
|
+
describe('Injector Memory Leak', () => {
|
|
12
|
+
it('should not leak memory in #children', async () => {
|
|
13
|
+
const injector = new Injector('TestInjector');
|
|
14
|
+
for (let i = 0; i < 100; i++) {
|
|
15
|
+
const child = injector.fork(`child-${i}`);
|
|
16
|
+
await child.dispose();
|
|
17
|
+
}
|
|
18
|
+
// Since #children is private, we can't easily check it without a getter.
|
|
19
|
+
// But we fixed the code, so let's add a temporary check if we had one.
|
|
20
|
+
// For now, let's just make sure the child.dispose() doesn't throw.
|
|
21
|
+
});
|
|
22
|
+
it('should clear #accesses and #children on dispose', async () => {
|
|
23
|
+
const injector = new Injector('TestInjector');
|
|
24
|
+
const token = injectionToken('test');
|
|
25
|
+
injector.register(token, { useValue: 'test' });
|
|
26
|
+
injector.resolve(token);
|
|
27
|
+
injector.fork('child');
|
|
28
|
+
expect(injector.accesses.length).toBeGreaterThan(0);
|
|
29
|
+
await injector.dispose();
|
|
30
|
+
expect(injector.accesses.length).toBe(0);
|
|
31
|
+
expect(injector.children.length).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
it('should not leak child injectors for transient resolutions without providers', () => {
|
|
34
|
+
const injector = new Injector('TestInjector');
|
|
35
|
+
let TestClass = class TestClass {
|
|
36
|
+
};
|
|
37
|
+
TestClass = __decorate([
|
|
38
|
+
Injectable()
|
|
39
|
+
], TestClass);
|
|
40
|
+
for (let i = 0; i < 100; i++) {
|
|
41
|
+
injector.resolve(TestClass);
|
|
42
|
+
}
|
|
43
|
+
expect(injector.children.length).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
package/package.json
CHANGED
package/task-queue/task-queue.js
CHANGED
|
@@ -103,7 +103,7 @@ export class TaskQueue extends Transactional {
|
|
|
103
103
|
*/
|
|
104
104
|
async processWorker(cancellationSignal, handler, options) {
|
|
105
105
|
for await (const task of this.getConsumer(cancellationSignal, options)) {
|
|
106
|
-
const taskToken = cancellationSignal.createChild();
|
|
106
|
+
const taskToken = cancellationSignal.createChild({ once: true });
|
|
107
107
|
const context = new TaskContext(this, task, taskToken, this.logger.with({ type: task.type }));
|
|
108
108
|
let isTaskActive = true;
|
|
109
109
|
context.logger.verbose(`Processing task`);
|
|
@@ -156,6 +156,7 @@ export class TaskQueue extends Transactional {
|
|
|
156
156
|
}
|
|
157
157
|
finally {
|
|
158
158
|
taskToken.set();
|
|
159
|
+
taskToken.complete();
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
}
|