refactor: hoist Database to module scope so declarations can be generated - #1513
Open
StoneCypher wants to merge 1 commit into
Open
refactor: hoist Database to module scope so declarations can be generated#1513StoneCypher wants to merge 1 commit into
StoneCypher wants to merge 1 commit into
Conversation
…ated
lib/database.js declared Database inside the createDatabase factory and returned
it. TypeScript cannot emit a declaration for a class it cannot name, so a
factory-local constructor blocks generating .d.ts files from JSDoc, failing with
TS9005 "Declaration emit for this file requires using private name 'Database'".
Database now sits at module scope. Each entrypoint's constructor comes from the
new lib/with-binding.js, which supplies that entrypoint's addon loader. The
constructor reads that loader from new.target rather than from a closure, which
is what permits the module-scoped declaration, and which resolves through the
static prototype chain so subclasses of an entrypoint constructor keep working.
No caller-visible behavior changes.
Design notes:
- Bound prototypes copy Database.prototype's descriptors rather than chaining to
it, so instances stay exactly one link from Object.prototype. Chaining added a
prototype hop to every method lookup and made `constructor` enumerable in
for..in over an instance.
- new.target is forwarded in BoundDatabase. Hardcoding its own identity there
silently breaks `class Sub extends Database {}`: subclass instances come back
as base instances, not instanceof Sub, unable to see their own methods, with
no error raised.
- The five dynamic getters installed via Object.defineProperties are untouched.
They remain enumerable own accessors on the instance, so Object.keys(), spread,
and JSON.stringify() behave exactly as before.
This branch contains no test changes, so that the diff is limited to the change
under review. The lib/ tree here is byte-identical to the branch on which the
caller-pattern suite was run.
Contributor
Author
Contributor
|
We'll wait for @JoshuaWise's input on this since a refactor is involved, even though it isn't major. |
|
The doc comment for |
Member
|
I'm out of town until next week, so but will review this as soon as I get back. I do think it's time we have TypeScript in |
Contributor
Author
|
@JoshuaWise - okay, if that's the case then this pr is largely unnecessary would you like me to knock together a typescript setup for you real quick to look at when you get back? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tl;dr: hoist
Databaseso that tsc will generate its types correctly, and re-bind the connectors so that the outside can't tell anything changed.it would be smart to merge the tests in #1512 so that this is known to be safe first
What
Moves
Databasefrom inside thecreateDatabasefactory to module scope inlib/database.js, and introduceslib/with-binding.jsto produce each entrypoint's bound constructor. Nine entrypoint files change by one token each.No caller-visible behavior changes.
Why
TypeScript cannot emit a declaration for a class it cannot name. A factory-local constructor therefore blocks generating
.d.tsfiles from JSDoc:That error is informative rather than fatal — it means TS did understand
Databaseas a class, including all fifteenDatabase.prototype.x = require(...)assignments. The prototype-assignment style is fine. Only the scope was blocking.With
Databaseat module scope, the same input emits what this package needs: a mergedfunction+classdeclaration, which is the only way to model a constructor that also supports being called withoutnew.How the binding is threaded
Each entrypoint still needs its own addon loader, and each still gets its own constructor and prototype.
lib/with-binding.jsbuilds a bound constructor per entrypoint and stores the loader on it under a symbol.Databasereads it fromnew.target, which resolves through the static prototype chain so subclasses of an entrypoint constructor keep working.Verification
The caller-pattern suite passes with all 51 existing assertions unmodified — none edited, weakened, or removed. This PR appends 2 new ones:
Database(visible in stack traces,util.inspectoutput, anddb.constructor.name)Both are caller-visible properties the suite didn't cover, and both would have broken under a naive implementation of this change. Worth checking the diff on
test/03.caller-patterns.jsto confirm it is purely additive — a refactor that edits its own safety net deserves suspicion.Two design decisions worth reviewing
Prototype descriptors are copied, not chained.
Object.create(Database.prototype)would have been the obvious construction, and it was my first draft. The existing assertion that instances sit exactly one link fromObject.prototyperuled it out: chaining adds a prototype hop to every method lookup, and it also makesconstructoran own enumerable property, so it starts appearing infor..inover an instance. Copying descriptors preserves both properties.The tradeoff is that the copy is a one-time snapshot, so a prototype member assigned after
withBinding()runs would be invisible to bound constructors. That can't happen today —lib/database.jspopulates the prototype at module load, before any entrypoint can callwithBinding— and the existing "exposes the same prototype surface from every entrypoint" assertion would catch it if someone later introduced a deferred assignment.new.targetis forwarded. Hardcoding the bound constructor's own identity inReflect.constructpasses almost every check but silently breaks subclassing:class Sub extends Database {}yields instances that aren'tinstanceof Suband can't see their own methods, with no error raised. I shipped exactly that bug in an earlier draft; it's now covered by test.Not in this PR
The JSDoc annotations, the generated
.d.tsfiles,attw, and the npm scripts. Those want separate review, on top of this.Untouched
The five dynamic getters installed via
Object.definePropertiesinlib/methods/wrappers.jsare unchanged. They remain enumerable own accessors on the instance, soObject.keys(), spread, andJSON.stringify()behave exactly as before. They will need JSDoc declarations for the generation work, since descriptor-installed properties don't appear in emitted declarations — but that's a later concern and needs no runtime change.