Package: @accelint/map-toolkit@4.0.0 (shared/create-map-store)
Symptom
Cursor display sticks on --, --. Viewport sticks on -- x -- NM. No console errors. No thrown exceptions. MapEvents.hover events fire correctly on the bus singleton — verified by attaching a separate Broadcast.getInstance().on(MapEvents.hover, …) listener that logs every event. The toolkit's own useCursorCoordinates / useMapViewport hooks just don't see the updates.
Same hazard applies in principle to viewportStore, cameraStore, mapModeStore, and any other createMapStore-built singleton.
Empirical proof of dual-instance
import { cursorCoordinateStore } from '@accelint/map-toolkit/cursor-coordinates';
cursorCoordinateStore.set(BASE_MAP_ID, { coordinate: [-98.17, 26.30] });
// cursorCoordinateStore.get(BASE_MAP_ID) returns the new value ✓
// useCursorCoordinates(BASE_MAP_ID) consumer's formattedCoord stays "--, --" ✗
The package-boundary cursorCoordinateStore handle and the internal-relative one consumed by useCursorCoordinates are two different in-memory objects sharing one exported symbol. Their internal instances Maps are not the same Map.
@accelint/bus is unaffected — Broadcast.getInstance() is a true singleton (single class definition in the bundle) and de-dupes correctly. We have pnpm.overrides forcing one bus version.
Root cause
@accelint/map-toolkit/dist/shared/create-map-store.js:
function createMapStore(config) {
const { defaultState, actions: createActions, bus, onCleanup } = config;
const instances = new Map(); // module-private closure
const pendingInitialState = new Map(); // module-private closure
const subscriptionCache = new Map(); // module-private closure
const snapshotCache = new Map(); // module-private closure
// ...
return { use, useSelector, get, set, subscribe, snapshot, ... };
}
The four Maps are created fresh every time createMapStore runs. The factory runs at module-init time of each store file (cursor-coordinates/store.js, etc.).
Under pnpm hoisted layouts, peer-dep nested node_modules/@accelint/... symlinks resolve to a different specifier path than the top-level node_modules/@accelint/... symlink, even though both target the same on-disk package. Turbopack treats those as distinct modules and re-runs initialization. Two parallel createMapStore invocations → two parallel sets of Maps → two parallel state containers, all returned under the same exported store symbol.
Suggested fix
Anchor the per-store Maps on globalThis keyed by an interned symbol so multiple evaluations converge on the same in-memory state container:
function createMapStore(config) {
const STORE_KEY = Symbol.for(`@accelint/map-toolkit/${config.id}`);
const globalState = globalThis[STORE_KEY] ??= {
instances: new Map(),
pendingInitialState: new Map(),
subscriptionCache: new Map(),
snapshotCache: new Map(),
};
const { instances, pendingInitialState, subscriptionCache, snapshotCache } = globalState;
// ... rest of the factory unchanged, just close over the shared Maps
}
This converges all evaluations onto the same state container while preserving the public API. Symbol.for is intentional — interned symbols are equal across realms, which the bus singleton already relies on.
Happy to PR this if useful.
Consumer workaround
Subscribe directly to Broadcast.getInstance().on(MapEvents.hover, …) / MapEvents.viewport and feed the toolkit's pure formatters (formatDecimalDegrees, getViewportSize). Bypasses useCursorCoordinates / <ViewportSize> entirely. This works because the bus is a true singleton, but it diverges from the documented hook pattern.
Versions
@accelint/map-toolkit@4.0.0
@accelint/bus@4.0.0 (forced via pnpm.overrides to dedupe)
- Next.js 16.1.6 (App Router)
- React 19.1.0 with
reactStrictMode: true
- Turbopack dev mode
- TypeScript 5.8
- pnpm 10.9 hoisted node_modules
Severity
Medium — silent UI regression with no console errors. Took meaningful investigation to localize because the hook surface looks like it's working (no exceptions, types match, hover events demonstrably firing on the bus). Affects three downstream consumer apps; we shipped consumer-side workarounds to unblock demos.
Package:
@accelint/map-toolkit@4.0.0(shared/create-map-store)Symptom
Cursor display sticks on
--, --. Viewport sticks on-- x -- NM. No console errors. No thrown exceptions.MapEvents.hoverevents fire correctly on the bus singleton — verified by attaching a separateBroadcast.getInstance().on(MapEvents.hover, …)listener that logs every event. The toolkit's ownuseCursorCoordinates/useMapViewporthooks just don't see the updates.Same hazard applies in principle to
viewportStore,cameraStore,mapModeStore, and any othercreateMapStore-built singleton.Empirical proof of dual-instance
The package-boundary
cursorCoordinateStorehandle and the internal-relative one consumed byuseCursorCoordinatesare two different in-memory objects sharing one exported symbol. Their internalinstancesMaps are not the same Map.@accelint/busis unaffected —Broadcast.getInstance()is a true singleton (single class definition in the bundle) and de-dupes correctly. We havepnpm.overridesforcing one bus version.Root cause
@accelint/map-toolkit/dist/shared/create-map-store.js:The four
Maps are created fresh every timecreateMapStoreruns. The factory runs at module-init time of each store file (cursor-coordinates/store.js, etc.).Under pnpm hoisted layouts, peer-dep nested
node_modules/@accelint/...symlinks resolve to a different specifier path than the top-levelnode_modules/@accelint/...symlink, even though both target the same on-disk package. Turbopack treats those as distinct modules and re-runs initialization. Two parallelcreateMapStoreinvocations → two parallel sets ofMaps → two parallel state containers, all returned under the same exported store symbol.Suggested fix
Anchor the per-store
Maps onglobalThiskeyed by an interned symbol so multiple evaluations converge on the same in-memory state container:This converges all evaluations onto the same state container while preserving the public API.
Symbol.foris intentional — interned symbols are equal across realms, which the bus singleton already relies on.Happy to PR this if useful.
Consumer workaround
Subscribe directly to
Broadcast.getInstance().on(MapEvents.hover, …)/MapEvents.viewportand feed the toolkit's pure formatters (formatDecimalDegrees,getViewportSize). BypassesuseCursorCoordinates/<ViewportSize>entirely. This works because the bus is a true singleton, but it diverges from the documented hook pattern.Versions
@accelint/map-toolkit@4.0.0@accelint/bus@4.0.0(forced viapnpm.overridesto dedupe)reactStrictMode: trueSeverity
Medium — silent UI regression with no console errors. Took meaningful investigation to localize because the hook surface looks like it's working (no exceptions, types match, hover events demonstrably firing on the bus). Affects three downstream consumer apps; we shipped consumer-side workarounds to unblock demos.