Skip to content

feat(api)!: singleton modules, shared cache, direct namespace export (0.9.0, closes #41)#82

Merged
kalwalt merged 1 commit into
devfrom
feat/41-api-parity
Jul 11, 2026
Merged

feat(api)!: singleton modules, shared cache, direct namespace export (0.9.0, closes #41)#82
kalwalt merged 1 commit into
devfrom
feat/41-api-parity

Conversation

@kalwalt

@kalwalt kalwalt commented Jul 11, 2026

Copy link
Copy Markdown
Member

Closes #41. Breaking change — ships as 0.9.0. Restores original jsfeat's calling convention. Designed via /brainstorming; full migration guide added at docs/migration-0.9.md.

What changes for consumers

// BEFORE (0.8.x)
import pkg from "@webarkit/jsfeat-next";
const jsfeatNext = pkg.jsfeatNext;          // double-namespace unwrap
const imgproc = new jsfeatNext.imgproc();   // instantiate every module
imgproc.grayscale(rgba, w, h, gray);

// AFTER (0.9.0)
import jsfeatNext from "@webarkit/jsfeat-next";   // default export IS the namespace
jsfeatNext.imgproc.grayscale(rgba, w, h, gray);   // singleton — no `new`

Three things resolved (all three were rows 1, 4, 5 of the parity audit's Axis 2):

  1. Double namespace gonesrc/index.ts default-exports the namespace directly, so the UMD global and ESM default both ARE the namespace.
  2. Algorithm modules are singletons — the 14 algorithm modules (imgproc, math, matmath, linalg, transform, fast_corners, yape, yape06, orb, optical_flow_lk, motion_estimator, affine2d, homography2d, cache) are pre-constructed instances on the namespace; no new.
  3. One shared buffer poolsrc/core/core.ts creates a module-level shared_cache every module binds to, replacing per-instance 30-buffer/76.8 KB pools. Falls out naturally from singletons.

Unchanged: the data-structure constructors (matrix_t, keypoint_t, pyramid_t, ransac_params_t) are still newed — they're genuine constructors in jsfeat too. Constants stay on the namespace.

Why singletons, not a statics-rewrite (the ground truth)

This isn't "match jsfeat for its own sake" — verified against the actual inspirit/jsfeat source:

  • jsfeat's modules are singletons by design, even stateful ones. jsfeat_fast_corners.js is an IIFE holding closure-level _threshold, exposed as one object, and configured once at load via fast_corners.set_threshold(20). There was never a notion of multiple independently-configured detectors.
  • The cache's whole purpose requires sharing. jsfeat's imgproc borrows from the one global jsfeat.cache so buffers freed by one stage feed the next. Per-instance pools defeated that.
  • Removes a footgun. With classes, following jsfeat-style examples you could new a module inside a per-frame loop and silently reallocate pools every frame. With singletons that's impossible by construction.
  • The classes are kept verbatim (only how they're attached changed), so the 57 parity tests keep guarding unmodified algorithm code — a statics-rewrite would have churned every protected line for zero benefit.

Versioning

Ships as 0.9.0 (pre-1.0 semver allows breaking minors). 1.0.0-alpha was considered and rejected — the release pipeline can't handle prerelease tags yet (filed as #81). Hard cut, no compat shim.

Tests & verification

  • Parity suite converted to the new convention; new tests/api-shape.test.ts pins the namespace shape, singleton identity, shared-cache identity, and constructor surface. 63/63 pass.
  • All 22 examples/ converted; the 5 non-camera ones executed in Node against the new bundle, all 22 static-scanned, and browser-tested by @kalwalt. browser.html's new jsfeat.cache() usage rewritten (jsfeat never supported that either).
  • tsc --noEmit clean · prettier --check clean · UMD+ESM bundle shapes verified (no double namespace, singletons callable, shared pool public).
  • dist/ + types/ rebuilt and committed in this PR — a deliberate deviation from the source-only convention, at @kalwalt's request: for an API-breaking change, a stale committed bundle made local browser testing confusing.

Docs

docs/migration-0.9.md (full old→new table + motivation), README quick start, AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, and the audit doc (Axis 2 marked resolved with a new status column).

Decision log

Decision Alternative Why
Singletons via new X() in aggregator Rewrite classes into static/object-literal namespaces Keeps classes verbatim → parity tests still guard them
Shared module-level shared_cache Keep per-instance pools Matches jsfeat; removes per-frame realloc footgun
0.9.0, hard cut 1.0.0-alpha / compat shim Pipeline can't do prerelease tags (#81); few consumers; shim adds surface
Single PR Module-per-PR (like #47) One coherent, interdependent change
Commit dist/types here Source-only (rebuild at release) User request; avoids stale-bundle test confusion on a breaking change

🤖 Generated with Claude Code

…41)

BREAKING CHANGE: restores original jsfeat's calling convention (ships as
0.9.0). Designed via /brainstorming with every decision user-confirmed;
full migration guide in docs/migration-0.9.md.

What changed:
- The 14 algorithm modules (imgproc, math, matmath, linalg, transform,
  fast_corners, yape, yape06, orb, optical_flow_lk, motion_estimator,
  affine2d, homography2d, cache) are now SINGLETON INSTANCES on the
  namespace: jsfeatNext.imgproc.grayscale(...) -- no `new`. The classes
  themselves are untouched (kept verbatim so the parity suite still
  guards unmodified algorithm code); the aggregator instantiates them
  once at load.
- ONE shared buffer pool: src/core/core.ts now creates a module-level
  shared_cache that every module binds to, replacing the per-instance
  30-buffer/76.8KB pools. jsfeatNext.cache is that pool instance
  (get_buffer/put_buffer), matching original jsfeat's global
  jsfeat.cache -- which was never a constructor.
- The jsfeatNext.jsfeatNext double namespace is gone: src/index.ts
  default-exports the namespace directly (UMD global and ESM default
  are now the namespace itself).
- Data-structure constructors unchanged: matrix_t, keypoint_t,
  pyramid_t, ransac_params_t are still `new`ed, as in original jsfeat.

Ground truth for the design (verified against inspirit/jsfeat source,
not assumed): jsfeat's own modules are singletons even when stateful
(fast_corners keeps closure-level _threshold and is configured once via
set_threshold(20) at load); imgproc borrows from the single global
jsfeat.cache -- per-instance pools defeated the cache's purpose and
made new-in-a-frame-loop silently reallocate pools every frame.

Also in this change:
- tests: parity suite converted to the new convention (63 tests total,
  including a new tests/api-shape.test.ts pinning the namespace shape,
  singleton identity, shared-cache identity and constructor surface)
- examples: all 22 converted and validated against the new bundle (the
  5 non-camera ones executed in Node, all 22 static-scanned, and
  browser-tested by the user); browser.html's cache usage rewritten (it
  used new jsfeat.cache()+allocate(), which original jsfeat never
  supported either); grayscale example simplified to direct
  jsfeatNext.imgproc.grayscale() calls
- docs: docs/migration-0.9.md (full old->new mapping + motivation),
  README quick start, AGENTS.md architecture/gotchas (also refreshed
  for the post-#47 layout), CLAUDE.md notes (were stale re: monolith
  and 'no tests'), copilot-instructions, audit doc Axis 2 marked
  resolved with a status column on the severity table
- dist/ + types/ rebuilt and committed IN THIS PR (deviation from the
  source-only convention, at user request: an API-breaking change with
  a stale committed bundle made local example testing confusing)

Verified: tsc --noEmit clean; npm test 63/63; prettier clean; UMD and
ESM bundle shapes checked (no double namespace, singletons callable,
shared pool public); examples validated against the new bundle and
manually tested in a browser by the user.

Closes #41.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kalwalt kalwalt self-assigned this Jul 11, 2026
@kalwalt kalwalt added documentation Improvements or additions to documentation enhancement New feature or request Typescript all about Typescript code design labels Jul 11, 2026
@kalwalt kalwalt added this to the Parity & Modernization milestone Jul 11, 2026
@kalwalt kalwalt merged commit 3b9eb95 into dev Jul 11, 2026
4 checks passed
@kalwalt kalwalt deleted the feat/41-api-parity branch July 11, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code design documentation Improvements or additions to documentation enhancement New feature or request Typescript all about Typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant