Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
towerofnix committed Jun 7, 2024
1 parent 5c2b79f commit 0e598ed
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 89 deletions.
61 changes: 48 additions & 13 deletions src/data/cacheable-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ function inspect(value) {
export default class CacheableObject {
static propertyDescriptors = Symbol.for('CacheableObject.propertyDescriptors');

#propertyDescriptors = null;

#propertyUpdateValues = Object.create(null);
#propertyUpdateCacheInvalidators = Object.create(null);

Expand All @@ -97,7 +99,21 @@ export default class CacheableObject {
// That means initial data must be provided by following up with update()
// after constructing the new instance of the Thing (sub)class.

constructor() {
// It's possible to provide a different set of property descriptors than the
// ones that are just present on the constructor.
constructor(standinPropertyDescriptors = null) {
this.#propertyDescriptors =
(standinPropertyDescriptors
? standinPropertyDescriptors
: this.constructor[CacheableObject.propertyDescriptors]);

if (!this.#propertyDescriptors) {
throw new Error(
`Expected CacheableObject.propertyDescriptors` +
` on constructor ${this.constructor.name},` +
` or provided directly to CacheableObject`);
}

this.#defineProperties();
this.#initializeUpdatingPropertyValues();

Expand All @@ -116,11 +132,30 @@ export default class CacheableObject {
}

#withEachPropertyDescriptor(callback) {
const {[CacheableObject.propertyDescriptors]: propertyDescriptors} =
this.constructor;
const descriptorKeys =
this.#keysTilPrototype(this.#propertyDescriptors, Object.prototype);

for (const property of Reflect.ownKeys(propertyDescriptors)) {
callback(property, propertyDescriptors[property]);
for (const property of descriptorKeys) {
callback(property, this.#propertyDescriptors[property]);
}
}

#keysTilPrototype(object, end) {
if (object === null) {
return new Set();
}

const prototypeKeys =
(Object.getPrototypeOf(object) === end
? null
: this.#keysTilPrototype(Object.getPrototypeOf(object), end));

const ownKeys = Reflect.ownKeys(object);

if (prototypeKeys) {
return new Set([...prototypeKeys, ...ownKeys]);
} else {
return new Set(ownKeys);
}
}

Expand All @@ -145,10 +180,6 @@ export default class CacheableObject {
}

#defineProperties() {
if (!this.constructor[CacheableObject.propertyDescriptors]) {
throw new Error(`Expected constructor ${this.constructor.name} to provide CacheableObject.propertyDescriptors`);
}

this.#withEachPropertyDescriptor((property, descriptor) => {
const {flags} = descriptor;

Expand All @@ -165,7 +196,12 @@ export default class CacheableObject {
definition.get = this.#getExposeObjectDefinitionGetterFunction(property);
}

Object.defineProperty(this, property, definition);
try {
Object.defineProperty(this, property, definition);
} catch (error) {
console.log('I am:', this);
throw error;
}
});

Object.seal(this);
Expand Down Expand Up @@ -206,7 +242,7 @@ export default class CacheableObject {
}

#getPropertyDescriptor(property) {
return this.constructor[CacheableObject.propertyDescriptors][property];
return this.#propertyDescriptors[property];
}

#invalidateCachesDependentUpon(property) {
Expand Down Expand Up @@ -331,8 +367,7 @@ export default class CacheableObject {
return;
}

const {[CacheableObject.propertyDescriptors]: propertyDescriptors} =
obj.constructor;
const propertyDescriptors = obj.#propertyDescriptors;

if (!propertyDescriptors) {
console.warn('Missing property descriptors:', obj);
Expand Down
151 changes: 130 additions & 21 deletions src/data/thing.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export default class Thing extends CacheableObject {

static [Symbol.for('Thing.selectAll')] = _wikiData => [];

// Magical constructor function that is the real entry point for, well,
// constructing any Thing subclass. Refer to the section about property
// descriptors later in this class for the high-level overview!
constructor() {
super(Thing.acquirePropertyDescriptors(new.target));
}

// Default custom inspect function, which may be overridden by Thing
// subclasses. This will be used when displaying aggregate errors and other
// command-line logging - it's the place to provide information useful in
Expand Down Expand Up @@ -78,32 +85,134 @@ export default class Thing extends CacheableObject {
return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
}

static computePropertyDescriptors(constructor, {
thingConstructors,
}) {
if (!constructor[Thing.getPropertyDescriptors]) {
throw new Error(`Missing [Thing.getPropertyDescriptors] function`);
// The terminology around property descriptors is kind of pathetic, because
// there just aren't enough verbs! Here's the rundown:
//
// static Thing.getPropertyDescriptors:
// This is a *well-known symbol*. Subclasses use it to declare their
// property descriptors.
//
// static [Thing.getPropertyDescriptors](thingConstructors):
// This is a *static method* that subclasses of Thing define. It returns
// the property descriptors which are meaningful on that class, as well as
// its own subclasses (unless overridden). It takes thingConstructors like
// other utility functions - these are the identities of the constructors
// which its own property descriptors may access.
//
// static Thing.preparePropertyDescriptors(thingConstructors):
// This is a *static method* that Thing itself defines. It is a utility
// function which calls Thing.decidePropertyDescriptors on each of the
// provided constructors.
//
// static Thing.decidePropertyDescriptors(constructor, thingConstructors):
// This is a *static method* that Thing itself defines. It is a primitive
// function which calls Thing.computePropertyDescriptors and declares its
// result as the property descriptors which all instances of the provided
// Thing subclass will use. Before it is called, it's impossible to
// construct that particular subclass. Likewise, you can't ever call it
// again for the same constructor.
//
// static Thing.computePropertyDescriptors(constructor, thingConstructors):
// This is a *static method* that Thing itself defines. It is a primitive
// function that does some inheritence shenanigans to combine property
// descriptors statically defined on the provided Thing subclass as well
// as its superclasses, on both [CacheableObject.propertyDescriptors] and
// [Thing.getPropertyDescriptors], the latter of which it's responsible
// for calling. Unlike Thing.decidePropertyDescriptors, this function can
// be called any number of times for the same constructor - but it never
// actually stores anything to do with the constructor, so on its own it
// can only be used for introspection or "imagining" what a class would
// look like contextualized with different thingConstructors.
//
// static Thing.acquirePropertyDescriptors(constructor):
// This is a *static method* that Thing itself defines. It is a primitive
// function which gets the previously decided property descriptors to use
// for the provided Thing subclass. If it hasn't yet been decided, this
// throws an error. This is used when constructing instances of Thing
// subclasses, to ~get~ ~decide~ *acquire* the property descriptors which
// are provided to the CacheableObject super() constructing call.
//
// Kapiche? Nice!

static #propertyDescriptorCache = new WeakMap();

static preparePropertyDescriptors(thingConstructors) {
for (const constructor of Object.values(thingConstructors)) {
Thing.decidePropertyDescriptors(constructor, thingConstructors);
}
}

static decidePropertyDescriptors(constructor, thingConstructors) {
if (this.#propertyDescriptorCache.has(constructor)) {
throw new Error(
`Constructor ${constructor.name} has already had its property descriptors decided`);
} else {
this.#propertyDescriptorCache.set(
constructor,
this.computePropertyDescriptors(constructor, thingConstructors));
}
}

static computePropertyDescriptors(constructor, thingConstructors) {
let topOfChain = null;

const superclass =
Object.getPrototypeOf(constructor) ?? null;

if (superclass) {
const superDescriptors =
(superclass
? Thing.computePropertyDescriptors(superclass, thingConstructors)
: null);

topOfChain = superDescriptors;
}

const results =
constructor[Thing.getPropertyDescriptors](thingConstructors);

for (const [key, value] of Object.entries(results)) {
if (Array.isArray(value)) {
results[key] = compositeFrom({
annotation: `${constructor.name}.${key}`,
compose: false,
steps: value,
});
} else if (value.toResolvedComposition) {
results[key] = compositeFrom(value.toResolvedComposition());
if (Object.hasOwn(constructor, CacheableObject.propertyDescriptors)) {
const classDescriptors = Object.create(topOfChain);

Object.assign(classDescriptors, constructor[CacheableObject.propertyDescriptors]);
Object.seal(classDescriptors);

topOfChain = classDescriptors;
}

if (Object.hasOwn(constructor, Thing.getPropertyDescriptors)) {
const thingDescriptors = Object.create(topOfChain);

const results =
constructor[Thing.getPropertyDescriptors](thingConstructors);

for (const [key, value] of Object.entries(results)) {
if (Array.isArray(value)) {
results[key] =
compositeFrom({
annotation: `${constructor.name}.${key}`,
compose: false,
steps: value,
});
} else if (value.toResolvedComposition) {
results[key] =
compositeFrom(value.toResolvedComposition());
}
}

Object.assign(thingDescriptors, results);
Object.seal(thingDescriptors);

topOfChain = thingDescriptors;
}

return {
...constructor[CacheableObject.propertyDescriptors] ?? {},
...results,
};
return topOfChain;
}

static acquirePropertyDescriptors(constructor) {
if (this.#propertyDescriptorCache.has(constructor)) {
return this.#propertyDescriptorCache.get(constructor);
} else {
throw new Error(
`Constructor ${constructor.name} never had its property descriptors decided`);
}
}

static extendDocumentSpec(thingClass, subspec) {
Expand Down
4 changes: 1 addition & 3 deletions src/data/things/homepage-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,7 @@ export class HomepageLayoutRow extends Thing {
export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
static [Thing.friendlyName] = `Homepage Albums Row`;

static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({
...HomepageLayoutRow[Thing.getPropertyDescriptors](opts),

static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({
// Update & expose

type: {
Expand Down
5 changes: 1 addition & 4 deletions src/data/things/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,7 @@ function evaluatePropertyDescriptors() {
message: `Errors evaluating Thing class property descriptors`,

op(constructor) {
constructor[CacheableObject.propertyDescriptors] =
Thing.computePropertyDescriptors(constructor, {
thingConstructors: allClasses,
});
Thing.decidePropertyDescriptors(constructor, allClasses);
},

showFailedClasses(failedClasses) {
Expand Down
13 changes: 13 additions & 0 deletions src/data/things/listing.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {inspect} from 'node:util';

import {input} from '#composite';
import Thing from '#thing';
import {isStringNonEmpty} from '#validators';
Expand Down Expand Up @@ -70,6 +72,17 @@ export class Listing extends Thing {

// Expose only

data: {
flags: {expose: true},
expose: {
dependencies: ['this'],
compute: ({this: myself}) => {
console.warn(`${inspect(myself)} - "data" not implemented yet`);
return [];
},
},
},

indexListing: [
{
dependencies: ['directory'],
Expand Down
Loading

0 comments on commit 0e598ed

Please sign in to comment.