Skip to content

Commit

Permalink
Improve Node References and JSDoc typings (#57)
Browse files Browse the repository at this point in the history
add jsdoc typings

+

fix linting

+

+

+

+
  • Loading branch information
lifeart committed Oct 11, 2023
1 parent 1492778 commit dba199b
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 6 deletions.
4 changes: 4 additions & 0 deletions addon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function ref(name, fn) {
const value = bucketFor(this).get(name);
return maybeReturnCreated(value, createdValues, fn, this);
},
configurable: true,
};
};
}
Expand All @@ -49,6 +50,7 @@ export function globalRef(name, fn) {
const value = bucketFor(getOwner(this) || resolveGlobalRef()).get(name);
return maybeReturnCreated(value, createdValues, fn, this);
},
configurable: true,
};
};
}
Expand All @@ -64,6 +66,7 @@ export function trackedRef(name, fn) {
const value = bucketFor(this).getTracked(name);
return maybeReturnCreated(value, createdValues, fn, this);
},
configurable: true,
};
};
}
Expand All @@ -81,6 +84,7 @@ export function trackedGlobalRef(name, fn) {
).getTracked(name);
return maybeReturnCreated(value, createdValues, fn, this);
},
configurable: true,
};
};
}
7 changes: 6 additions & 1 deletion addon/modifiers/create-ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ export default class RefModifier extends Modifier {
setGlobalRef(getOwner(this));

registerDestructor(this, () => {
const element = this._element;
this.cleanMutationObservers();
this.cleanResizeObservers();
getNodeDestructors(this._element).forEach((cb) => cb());
getNodeDestructors(element).forEach((cb) => cb());
if (element === bucketFor(this._ctx).get(this._key)) {
bucketFor(this._ctx).add(this._key, null);
}
delete this._element;
});
}
// to minimise overhead, user should be specific about
Expand Down
125 changes: 120 additions & 5 deletions addon/utils/ref.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,74 @@
// @ts-check
/*eslint no-undef: "warn"*/

import {
registerDestructor,
isDestroying,
isDestroyed,
} from '@ember/destroyable';
import { tracked } from '@glimmer/tracking';

/**
* @type {object | null}
*/
let lastGlobalRef = null;
/**
* @type {WeakMap<object, ReturnType<typeof createBucket>>}
*/
const buckets = new WeakMap();
/**
* @type {WeakMap<HTMLElement, Array<() => void>>}
*/
const nodeDestructors = new WeakMap();

const hasWeakRef = typeof WeakRef !== 'undefined';

function fromWeakRefIfSupported(node) {
if (hasWeakRef && node instanceof WeakRef) {

Check warning on line 27 in addon/utils/ref.js

View workflow job for this annotation

GitHub Actions / Lint

'WeakRef' is not defined
return node.deref() ?? null;
}
return node;
}

/**
*
* @param {null | undefined | WeeakRef | HTMLElement } node
* @returns
*/
function toWeakRefIfSupported(node) {
if (node === null || node === undefined) {
return null;
}
if (node instanceof WeakRef) {

Check warning on line 42 in addon/utils/ref.js

View workflow job for this annotation

GitHub Actions / Lint

'WeakRef' is not defined
return node;
}
if (hasWeakRef) {
return new WeakRef(node);

Check warning on line 46 in addon/utils/ref.js

View workflow job for this annotation

GitHub Actions / Lint

'WeakRef' is not defined
}
return node;
}

class FieldCell {
@tracked value = null;
/**
/**
* @type {null | (WeakRef<HTMLElement> | HTMLElement)}
*/
@tracked
_element = null;
get value() {
if (this._element) {
return fromWeakRefIfSupported(this._element);
} else {
return null;
}
}
set value(element) {
if (element) {
this._element = toWeakRefIfSupported(element);
} else {
this._element = null;
}
}
}

export function setGlobalRef(value) {
Expand All @@ -27,35 +85,62 @@ export function resolveGlobalRef() {

function createBucket() {
return {
/**
* @type { Record<string, HTMLElement> }
*/
bucket: {},
/**
* @type { Record<string, FieldCell> }
*/
keys: {},
/**
* @param {string} key
*/
createTrackedCell(key) {
if (!(key in this.keys)) {
this.keys[key] = new FieldCell();
}
},
/**
* @param {string} name
* @returns { HTMLElement | null }
*/
get(name) {
this.createTrackedCell(name);
return this.bucket[name] || null;
return fromWeakRefIfSupported(this.bucket[name]) || null;
},
/**
* @param {string} name
*/
dirtyTrackedCell(name) {
this.createTrackedCell(name);
const val = this.keys[name].value;
this.keys[name].value = val;
},
/**
* @param {string} name
*/
getTracked(name) {
this.createTrackedCell(name);
return this.keys[name].value;
},
/**
* @param {string} name
* @param {HTMLElement} value
*/
add(name, value) {
this.createTrackedCell(name);
this.keys[name].value = value;
this.bucket[name] = value;
this.bucket[name] = toWeakRefIfSupported(value);
if (!(name in this.notificationsFor)) {
this.notificationsFor[name] = [];
}
this.notificationsFor[name].forEach((fn) => fn());
},
/**
* @param {string} name
* @param {() => void} fn
*/
addNotificationFor(name, fn) {
if (!(name in this.notificationsFor)) {
this.notificationsFor[name] = [];
Expand All @@ -67,26 +152,49 @@ function createBucket() {
);
};
},
/**
* @type { Record<string, Array<() => void>> }
*/
notificationsFor: {},
};
}

/**
*
* @param {HTMLElement} node
* @returns {Array<() => void>}
*/
export function getNodeDestructors(node) {
return nodeDestructors.get(node) || [];
}

/**
* @param {HTMLElement} node
* @param {() => void} cb
*/
export function registerNodeDestructor(node, cb) {
if (!nodeDestructors.has(node)) {
nodeDestructors.set(node, []);
}
nodeDestructors.get(node).push(cb);
nodeDestructors.get(node)?.push(cb);
}
/**
*
* @param {HTMLElement} node
* @param {()=> void} cb
*/
export function unregisterNodeDestructor(node, cb) {
const destructors = nodeDestructors.get(node) || [];
nodeDestructors.set(
node,
destructors.filter((el) => el !== cb)
);
}
/**
*
* @param {object} rawCtx
* @returns {ReturnType<typeof createBucket> | undefined}
*/
export function bucketFor(rawCtx) {
const ctx = rawCtx;
if (!buckets.has(ctx)) {
Expand All @@ -104,7 +212,14 @@ export function bucketFor(rawCtx) {
}
return buckets.get(ctx);
}
/**
*
* @param {string} name
* @param {object} bucketRef
* @param {()=>void} cb
* @returns
*/
export function watchFor(name, bucketRef, cb) {
const bucket = bucketFor(bucketRef);
return bucket.addNotificationFor(name, cb);
return bucket?.addNotificationFor(name, cb);
}
9 changes: 9 additions & 0 deletions config/ember-try.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ module.exports = async function () {
'ember-auto-import': '~2.4.0',
webpack: '~5.67.0',
},
dependencies: {
'@ember/string': '3.1.1',
},
},
},
{
Expand All @@ -33,6 +36,9 @@ module.exports = async function () {
'ember-auto-import': '~2.4.0',
webpack: '~5.67.0',
},
dependencies: {
'@ember/string': '3.1.1',
},
},
},
{
Expand All @@ -43,6 +49,9 @@ module.exports = async function () {
'ember-auto-import': '~2.4.0',
webpack: '~5.67.0',
},
dependencies: {
'@ember/string': '3.1.1',
},
},
},
{
Expand Down
10 changes: 10 additions & 0 deletions tests/dummy/app/components/ref-gc.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div>
<button type="button" class="add-items p-2 ml-2" {{on "click" this.addItems}}>Add 100 items</button>
<button type="button" class="clear p-2 ml-2" {{on "click" this.clear}}>Clear</button>
</div>

{{#each this.items as |item|}}
<div {{create-ref (concat "item-" item) bucket=this}}>
{{item}}
</div>
{{/each}}
34 changes: 34 additions & 0 deletions tests/dummy/app/components/ref-gc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { ref } from 'ember-ref-bucket';

export default class Test extends Component {
constructor() {
super(...arguments);
this.args.sendContext(this);
}
/**
* @type {number[]}
*/
@tracked items = [];

@action addItems() {
this.items = [
...this.items,
...Array(100)
.fill(0)
.map((_, i) => i),
];
this.items.forEach((item) => {
Object.defineProperty(this, `item-${item}`, ref(`item-${item}`)(this));
});
}

@action clear() {
// this.items.forEach((item) => {
// delete this[`item-${item}`];
// });
this.items = [];
}
}
33 changes: 33 additions & 0 deletions tests/integration/components/ref-gc-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | ref-gc', function (hooks) {
setupRenderingTest(hooks);

// TODO: Replace this with your real tests.
test('it renders', async function (assert) {
let context = null;
this.sendContext = (ctx) => {
context = ctx;
};

await render(hbs`<RefGc @sendContext={{this.sendContext}} />`);

await click('.add-items');

assert.strictEqual(typeof context[`item-${0}`], 'object');
assert.strictEqual(context[`item-${0}`].tagName, 'DIV');

await click('.clear');

assert.strictEqual(typeof context[`item-${0}`], 'object');
assert.strictEqual(String(context[`item-${0}`]), 'null');

await click('.add-items');
assert.strictEqual(context[`item-${0}`].tagName, 'DIV');
await click('.clear');
assert.strictEqual(String(context[`item-${0}`]), 'null');
});
});

0 comments on commit dba199b

Please sign in to comment.