Skip to content

Commit

Permalink
Schedule node finalized cleanup on a new task
Browse files Browse the repository at this point in the history
Workaround for node-ffi-napi/weak-napi#17
Rewrite node stub to be fully compliant
  • Loading branch information
mhofman committed May 10, 2019
1 parent 0c764c5 commit 2354149
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const observedInfos = new Set<ObjectInfo>();
function finalizedCallback(this: ObjectInfo) {
if (!observedInfos.has(this)) return;
observedInfos.delete(this);
finalizationGroupJobs.setFinalized(this);
setImmediate(() => finalizationGroupJobs.setFinalized(this));
}

function getInfo(target: object) {
Expand Down
23 changes: 23 additions & 0 deletions src/node/stub.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe } from "../../tests/setup.js";
import { shouldBehaveAsFinalizationGroupAccordingToSpec } from "../tests/FinalizationGroup.shared.js";
import { shouldBehaveAsWeakRefAccordingToSpec } from "../tests/WeakRef.shared.js";
import { gc, gcAvailable } from "../../tests/collector-helper.js";
import available from "./available.js";

if (available)
describe("Weakrefs node stub", function() {
const shimDetails = import("./stub.js").then(exports => ({
gc,
...exports,
}));

shouldBehaveAsWeakRefAccordingToSpec(
shimDetails,
gcAvailable,
!available
);
shouldBehaveAsFinalizationGroupAccordingToSpec(
shimDetails,
gcAvailable
);
});
161 changes: 143 additions & 18 deletions src/node/stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,170 @@ import WeakTag from "@mhofman/weak-napi-native/weak-tag.js";
// @ts-ignore
import ObjectInfo from "@mhofman/weak-napi-native/object-info.js";

import { FinalizationGroup, WeakRef } from "../weakrefs.js";
import isObject from "../utils/lodash/isObject.js";
import { setImmediate } from "../utils/tasks/setImmediate.js";

type Cell = {
holdings: any;
unregisterToken: object | undefined;
};

const map = new WeakMap<object, Set<WeakTag>>();

import { FinalizationGroup, WeakRef } from "../weakrefs.js";
const cellsForGroup = new WeakMap<
FinalizationGroup<any>,
Map<ObjectInfo, Cell>
>();

const registrations = new WeakMap<object, Set<ObjectInfo>>();

function getCells(group: FinalizationGroup) {
const cells = cellsForGroup.get(group);
if (!cells) throw new TypeError();
return cells;
}

type Entries = Iterable<[ObjectInfo, Cell]>;

function* getEmptyCellEntries(context: {
cells: Map<ObjectInfo, Cell>;
}): Entries {
for (const [info, cell] of context.cells.entries()) {
if (!context.cells) throw new TypeError();
if (info.target) continue;
context.cells.delete(info);
yield [info, cell];
}
}

function* getCellEntry(context: {
info: ObjectInfo;
cells: Map<ObjectInfo, Cell>;
}): Entries {
const cell = context.cells.get(context.info)!;
context.cells.delete(context.info);
yield [context.info, cell];
}

const CleanupIterator: <Holdings>(
entries: Iterable<[ObjectInfo, Cell]>
) => FinalizationGroup.CleanupIterator<Holdings> = function* CleanupIterator(
entries: Iterable<[ObjectInfo, Cell]>
) {
for (const [info, cell] of entries) {
if (cell.unregisterToken)
registrations.get(cell.unregisterToken)!.delete(info);
yield cell.holdings;
}
} as <Holdings>(
entries: Iterable<[ObjectInfo, Cell]>
) => FinalizationGroup.CleanupIterator<Holdings>;

Object.defineProperty(
Object.getPrototypeOf(CleanupIterator.prototype),
Symbol.toStringTag,
{
value: "FinalizationGroup Cleanup Iterator",
configurable: true,
}
);

class FinalizationGroupNodeStub<Holdings>
implements FinalizationGroup<Holdings> {
private readonly finalizedCallback: ObjectInfo.FinalizedCallback;

// stub FinalizationGroup that calls the callback directly for each
// registered target
// No holding or unregister
class FinalizationGroupNodeStub implements FinalizationGroup<ObjectInfo> {
private finalizedCallback: ObjectInfo.FinalizedCallback;
static get [Symbol.species]() {
return FinalizationGroupNodeStub;
}
constructor(callback: FinalizationGroup.CleanupCallback<any>) {

constructor(
private readonly cleanupCallback: FinalizationGroup.CleanupCallback<
Holdings
>
) {
if (typeof cleanupCallback != "function") throw new TypeError();
const cells = new Map<ObjectInfo, Cell>();
cellsForGroup.set(this, cells);
this.finalizedCallback = function(this: ObjectInfo) {
callback(([this] as unknown) as FinalizationGroup.CleanupIterator<
ObjectInfo
>);
setImmediate(() => {
if (!cells.get(this)) return;
const context = { info: this, cells };
cleanupCallback(CleanupIterator(getCellEntry(context)));
context.info = context.cells = undefined!;
});
};
}

register(
target: object,
holdingsIgnored: any,
unregisterTokenIgnored?: any
): ObjectInfo {
holdings: Holdings,
unregisterToken?: object
): void {
const cells = getCells(this);

let tagSet = map.get(target);
if (!tagSet) {
tagSet = new Set();
map.set(target, tagSet);
}
let registrationSet = unregisterToken
? registrations.get(unregisterToken)
: undefined;
if (!registrationSet && unregisterToken !== undefined) {
if (!isObject(unregisterToken)) throw new TypeError();
registrationSet = new Set();
registrations.set(unregisterToken, registrationSet);
}
const info = new ObjectInfo(target, this.finalizedCallback);
tagSet.add(new WeakTag(info));
return info;
cells.set(info, {
holdings,
unregisterToken,
});
if (registrationSet) registrationSet.add(info);
}

unregister(unregisterToken?: any): boolean {
return false;
unregister(unregisterToken: object): boolean {
const cells = getCells(this);

const registrationSet = registrations.get(unregisterToken);
if (!registrationSet) {
if (!isObject(unregisterToken)) throw new TypeError();
return false;
}

let removed = false;
for (const info of registrationSet) {
const cell = cells.get(info)!;

if (!cell) continue;

cells.delete(info);
registrationSet.delete(info);
removed = true;
}

return removed;
}

cleanupSome(
cleanupCallback?: FinalizationGroup.CleanupCallback<any> | undefined
): void {}
cleanupCallback?:
| FinalizationGroup.CleanupCallback<Holdings>
| undefined
): void {
const context = { cells: getCells(this) };
const emptyObjectInfos = getEmptyCellEntries(context);

if (cleanupCallback === undefined) {
cleanupCallback = this
.cleanupCallback as FinalizationGroup.CleanupCallback<Holdings>;
}

cleanupCallback(CleanupIterator(emptyObjectInfos));

context.cells = undefined!;
}
}

class WeakRefNodeStub<T extends object = object> implements WeakRef<T> {
Expand All @@ -52,6 +176,7 @@ class WeakRefNodeStub<T extends object = object> implements WeakRef<T> {
return WeakRefNodeStub;
}
constructor(target: T) {
if (!isObject(target)) throw new TypeError();
this.info = new ObjectInfo(target, () => {});
}

Expand Down
6 changes: 5 additions & 1 deletion src/tests/FinalizationGroup.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,8 +522,10 @@ export function shouldBehaveAsFinalizationGroupAccordingToSpec(
it("doesn't remove cell if iterator is closed before", async function() {
const holdings = [{}, {}];
let notIterated: object | undefined;
let invocations = 0;
const finalizationGroup = new FinalizationGroup<object>(
items => {
invocations++;
for (const item of items) {
notIterated = item;
expect(holdings).to.contain(notIterated);
Expand All @@ -546,7 +548,9 @@ export function shouldBehaveAsFinalizationGroupAccordingToSpec(
object = undefined!;
await collected;
expect(notIterated).to.be.ok;
if (unregisterReturnsBool) {
if (invocations > 1) {
this.skip();
} else if (unregisterReturnsBool) {
expect(finalizationGroup.unregister(notIterated!))
.to.be.true;
} else if (workingCleanupSome) {
Expand Down

0 comments on commit 2354149

Please sign in to comment.