@accelint/map-toolkit 0.6.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/catalog-info.yaml +7 -6
- package/dist/camera/events.js.map +1 -1
- package/dist/camera/index.d.ts +2 -2
- package/dist/camera/index.js +2 -2
- package/dist/camera/store.d.ts +120 -0
- package/dist/camera/store.js +279 -0
- package/dist/camera/store.js.map +1 -0
- package/dist/cursor-coordinates/index.d.ts +4 -2
- package/dist/cursor-coordinates/index.js +3 -2
- package/dist/cursor-coordinates/store.d.ts +48 -0
- package/dist/cursor-coordinates/store.js +92 -0
- package/dist/cursor-coordinates/store.js.map +1 -0
- package/dist/cursor-coordinates/types.d.ts +87 -0
- package/dist/cursor-coordinates/types.js +12 -0
- package/dist/cursor-coordinates/use-cursor-coordinates.d.ts +41 -37
- package/dist/cursor-coordinates/use-cursor-coordinates.js +131 -202
- package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
- package/dist/deckgl/base-map/constants.d.ts +1 -6
- package/dist/deckgl/base-map/constants.js +1 -6
- package/dist/deckgl/base-map/constants.js.map +1 -1
- package/dist/deckgl/base-map/controls.js +2 -0
- package/dist/deckgl/base-map/controls.js.map +1 -1
- package/dist/deckgl/base-map/events.js.map +1 -1
- package/dist/deckgl/base-map/index.d.ts +2 -2
- package/dist/deckgl/base-map/index.js +10 -11
- package/dist/deckgl/base-map/index.js.map +1 -1
- package/dist/deckgl/base-map/provider.d.ts +2 -2
- package/dist/deckgl/base-map/provider.js +1 -1
- package/dist/deckgl/base-map/provider.js.map +1 -1
- package/dist/deckgl/index.d.ts +4 -4
- package/dist/deckgl/index.js +4 -4
- package/dist/deckgl/saved-viewports/index.js.map +1 -1
- package/dist/deckgl/saved-viewports/storage.js +10 -2
- package/dist/deckgl/saved-viewports/storage.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/constants.js +5 -8
- package/dist/deckgl/shapes/display-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/fiber.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/index.d.ts +18 -14
- package/dist/deckgl/shapes/display-shape-layer/index.js +63 -30
- package/dist/deckgl/shapes/display-shape-layer/index.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.js +2 -16
- package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/store.js +58 -272
- package/dist/deckgl/shapes/display-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/types.d.ts +22 -11
- package/dist/deckgl/shapes/display-shape-layer/{use-shape-selection.d.ts → use-select-shape.d.ts} +9 -9
- package/dist/deckgl/shapes/display-shape-layer/{use-shape-selection.js → use-select-shape.js} +12 -12
- package/dist/deckgl/shapes/display-shape-layer/use-select-shape.js.map +1 -0
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js +5 -66
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.d.ts +2 -65
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.js +3 -121
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/constants.js +46 -0
- package/dist/deckgl/shapes/draw-shape-layer/constants.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/events.d.ts +92 -0
- package/dist/deckgl/shapes/draw-shape-layer/events.js +56 -0
- package/dist/deckgl/shapes/draw-shape-layer/events.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/fiber.d.ts +11 -0
- package/dist/{maplibre/constants.js → deckgl/shapes/draw-shape-layer/fiber.js} +6 -12
- package/dist/deckgl/shapes/draw-shape-layer/fiber.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/index.d.ts +53 -0
- package/dist/deckgl/shapes/draw-shape-layer/index.js +95 -0
- package/dist/deckgl/shapes/draw-shape-layer/index.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js +51 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js +73 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js +87 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js +88 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js +77 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/index.js +64 -0
- package/dist/deckgl/shapes/draw-shape-layer/modes/index.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/store.js +175 -0
- package/dist/deckgl/shapes/draw-shape-layer/store.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/types.d.ts +86 -0
- package/dist/{viewport/constants.js → deckgl/shapes/draw-shape-layer/types.js} +1 -12
- package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.d.ts +82 -0
- package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.js +112 -0
- package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.js.map +1 -0
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js +147 -0
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/constants.js +41 -0
- package/dist/deckgl/shapes/edit-shape-layer/constants.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/events.d.ts +92 -0
- package/dist/deckgl/shapes/edit-shape-layer/events.js +56 -0
- package/dist/deckgl/shapes/edit-shape-layer/events.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/fiber.d.ts +13 -0
- package/dist/deckgl/shapes/edit-shape-layer/fiber.js +14 -0
- package/dist/deckgl/shapes/edit-shape-layer/index.d.ts +63 -0
- package/dist/deckgl/shapes/edit-shape-layer/index.js +162 -0
- package/dist/deckgl/shapes/edit-shape-layer/index.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js +154 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js +147 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js +87 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js +61 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js +109 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js +289 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js +121 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/store.js +194 -0
- package/dist/deckgl/shapes/edit-shape-layer/store.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/types.d.ts +93 -0
- package/dist/deckgl/shapes/edit-shape-layer/types.js +14 -0
- package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.d.ts +82 -0
- package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.js +114 -0
- package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.js.map +1 -0
- package/dist/deckgl/shapes/index.d.ts +15 -6
- package/dist/deckgl/shapes/index.js +12 -5
- package/dist/deckgl/shapes/shared/constants.d.ts +27 -32
- package/dist/deckgl/shapes/shared/constants.js +189 -25
- package/dist/deckgl/shapes/shared/constants.js.map +1 -1
- package/dist/deckgl/shapes/shared/events.d.ts +1 -20
- package/dist/deckgl/shapes/shared/events.js +1 -31
- package/dist/deckgl/shapes/shared/events.js.map +1 -1
- package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js +84 -0
- package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js.map +1 -0
- package/dist/deckgl/shapes/shared/types.d.ts +187 -28
- package/dist/deckgl/shapes/shared/types.js +55 -1
- package/dist/deckgl/shapes/shared/types.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/geometry-measurements.js +128 -0
- package/dist/deckgl/shapes/shared/utils/geometry-measurements.js.map +1 -0
- package/dist/deckgl/shapes/shared/utils/layer-config.js +50 -0
- package/dist/deckgl/shapes/shared/utils/layer-config.js.map +1 -0
- package/dist/deckgl/shapes/shared/utils/mode-utils.js +113 -0
- package/dist/deckgl/shapes/shared/utils/mode-utils.js.map +1 -0
- package/dist/deckgl/shapes/shared/utils/pick-filtering.js +57 -0
- package/dist/deckgl/shapes/shared/utils/pick-filtering.js.map +1 -0
- package/dist/deckgl/shapes/shared/utils/style-utils.d.ts +64 -0
- package/dist/deckgl/shapes/shared/utils/style-utils.js +101 -0
- package/dist/deckgl/shapes/shared/utils/style-utils.js.map +1 -0
- package/dist/deckgl/symbol-layer/fiber.js.map +1 -1
- package/dist/deckgl/symbol-layer/index.js.map +1 -1
- package/dist/deckgl/text-layer/character-sets.js.map +1 -1
- package/dist/deckgl/text-layer/default-settings.js +4 -24
- package/dist/deckgl/text-layer/default-settings.js.map +1 -1
- package/dist/deckgl/text-layer/fiber.js.map +1 -1
- package/dist/deckgl/text-layer/index.js.map +1 -1
- package/dist/deckgl/text-settings.d.ts +77 -0
- package/dist/deckgl/text-settings.js +83 -0
- package/dist/deckgl/text-settings.js.map +1 -0
- package/dist/map-cursor/events.js.map +1 -1
- package/dist/map-cursor/index.d.ts +2 -2
- package/dist/map-cursor/index.js +2 -2
- package/dist/map-cursor/store.d.ts +32 -61
- package/dist/map-cursor/store.js +165 -294
- package/dist/map-cursor/store.js.map +1 -1
- package/dist/map-cursor/use-map-cursor.d.ts +5 -2
- package/dist/map-cursor/use-map-cursor.js +33 -15
- package/dist/map-cursor/use-map-cursor.js.map +1 -1
- package/dist/map-mode/events.js.map +1 -1
- package/dist/map-mode/index.d.ts +2 -2
- package/dist/map-mode/index.js +2 -2
- package/dist/map-mode/store.d.ts +36 -37
- package/dist/map-mode/store.js +131 -237
- package/dist/map-mode/store.js.map +1 -1
- package/dist/map-mode/use-map-mode.js +6 -5
- package/dist/map-mode/use-map-mode.js.map +1 -1
- package/dist/maplibre/hooks/use-maplibre.js.map +1 -1
- package/dist/maplibre/index.d.ts +2 -2
- package/dist/maplibre/index.js +2 -2
- package/dist/shared/constants.d.ts +19 -0
- package/dist/shared/constants.js +33 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/create-map-store.d.ts +202 -0
- package/dist/shared/create-map-store.js +223 -0
- package/dist/shared/create-map-store.js.map +1 -0
- package/dist/shared/units.d.ts +39 -0
- package/dist/shared/units.js +49 -0
- package/dist/shared/units.js.map +1 -0
- package/dist/viewport/index.d.ts +3 -3
- package/dist/viewport/index.js +3 -3
- package/dist/viewport/store.d.ts +69 -0
- package/dist/viewport/store.js +125 -0
- package/dist/viewport/store.js.map +1 -0
- package/dist/viewport/types.d.ts +2 -2
- package/dist/viewport/utils.js +2 -2
- package/dist/viewport/utils.js.map +1 -1
- package/dist/viewport/viewport-size.d.ts +2 -2
- package/dist/viewport/viewport-size.js +2 -2
- package/dist/viewport/viewport-size.js.map +1 -1
- package/package.json +39 -19
- package/dist/camera/use-camera-state.d.ts +0 -153
- package/dist/camera/use-camera-state.js +0 -418
- package/dist/camera/use-camera-state.js.map +0 -1
- package/dist/deckgl/shapes/display-shape-layer/constants.d.ts +0 -44
- package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.d.ts +0 -66
- package/dist/deckgl/shapes/display-shape-layer/store.d.ts +0 -87
- package/dist/deckgl/shapes/display-shape-layer/use-shape-selection.js.map +0 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.d.ts +0 -61
- package/dist/maplibre/constants.d.ts +0 -13
- package/dist/maplibre/constants.js.map +0 -1
- package/dist/viewport/constants.d.ts +0 -11
- package/dist/viewport/constants.js.map +0 -1
- package/dist/viewport/use-viewport-state.d.ts +0 -100
- package/dist/viewport/use-viewport-state.js +0 -222
- package/dist/viewport/use-viewport-state.js.map +0 -1
package/dist/map-mode/store.js
CHANGED
|
@@ -11,68 +11,48 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
import { createMapStore, mapClear, mapDelete, mapSet } from "../shared/create-map-store.js";
|
|
14
15
|
import { MapModeEvents } from "./events.js";
|
|
15
16
|
import { Broadcast } from "@accelint/bus";
|
|
17
|
+
import { getLogger } from "@accelint/logger";
|
|
16
18
|
import { uuid } from "@accelint/core";
|
|
17
19
|
|
|
18
20
|
//#region src/map-mode/store.ts
|
|
21
|
+
/**
|
|
22
|
+
* Map Mode Store
|
|
23
|
+
*
|
|
24
|
+
* Manages mode state with ownership-based authorization.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import { modeStore } from '@accelint/map-toolkit/map-mode';
|
|
29
|
+
*
|
|
30
|
+
* function MapControls({ mapId }) {
|
|
31
|
+
* const { state, requestModeChange } = modeStore.use(mapId);
|
|
32
|
+
*
|
|
33
|
+
* return (
|
|
34
|
+
* <div>
|
|
35
|
+
* <p>Current mode: {state.mode}</p>
|
|
36
|
+
* <button onClick={() => requestModeChange('draw', 'draw-layer')}>
|
|
37
|
+
* Draw Mode
|
|
38
|
+
* </button>
|
|
39
|
+
* </div>
|
|
40
|
+
* );
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
const logger = getLogger({
|
|
45
|
+
enabled: process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test",
|
|
46
|
+
level: "warn",
|
|
47
|
+
prefix: "[MapMode]",
|
|
48
|
+
pretty: true
|
|
49
|
+
});
|
|
19
50
|
const DEFAULT_MODE = "default";
|
|
20
51
|
/**
|
|
21
52
|
* Typed event bus instance for map mode events.
|
|
22
|
-
* Provides type-safe event emission and listening for all map mode state changes.
|
|
23
53
|
*/
|
|
24
54
|
const mapModeBus = Broadcast.getInstance();
|
|
25
55
|
/**
|
|
26
|
-
* Store for map mode state keyed by instanceId
|
|
27
|
-
*/
|
|
28
|
-
const modeStore = /* @__PURE__ */ new Map();
|
|
29
|
-
/**
|
|
30
|
-
* Track React component subscribers per instanceId (for fan-out notifications).
|
|
31
|
-
* Each Set contains onStoreChange callbacks from useSyncExternalStore.
|
|
32
|
-
*/
|
|
33
|
-
const componentSubscribers = /* @__PURE__ */ new Map();
|
|
34
|
-
/**
|
|
35
|
-
* Cache of bus unsubscribe functions (1 per instanceId).
|
|
36
|
-
* This ensures we only have one bus listener per map mode instance, regardless of
|
|
37
|
-
* how many React components subscribe to it.
|
|
38
|
-
*/
|
|
39
|
-
const busUnsubscribers = /* @__PURE__ */ new Map();
|
|
40
|
-
/**
|
|
41
|
-
* Cache of subscription functions per instanceId to avoid recreating on every render
|
|
42
|
-
*/
|
|
43
|
-
const subscriptionCache = /* @__PURE__ */ new Map();
|
|
44
|
-
/**
|
|
45
|
-
* Cache of snapshot functions per instanceId to maintain referential stability
|
|
46
|
-
*/
|
|
47
|
-
const snapshotCache = /* @__PURE__ */ new Map();
|
|
48
|
-
/**
|
|
49
|
-
* Cache of server snapshot functions per instanceId to maintain referential stability.
|
|
50
|
-
* Server snapshots always return default mode since mode state is client-only.
|
|
51
|
-
*/
|
|
52
|
-
const serverSnapshotCache = /* @__PURE__ */ new Map();
|
|
53
|
-
/**
|
|
54
|
-
* Cache of requestModeChange functions per instanceId to maintain referential stability
|
|
55
|
-
*/
|
|
56
|
-
const requestModeChangeCache = /* @__PURE__ */ new Map();
|
|
57
|
-
/**
|
|
58
|
-
* Get or create mode state for a given instanceId
|
|
59
|
-
*/
|
|
60
|
-
function getOrCreateState(instanceId) {
|
|
61
|
-
if (!modeStore.has(instanceId)) modeStore.set(instanceId, {
|
|
62
|
-
mode: DEFAULT_MODE,
|
|
63
|
-
modeOwners: /* @__PURE__ */ new Map(),
|
|
64
|
-
pendingRequests: /* @__PURE__ */ new Map()
|
|
65
|
-
});
|
|
66
|
-
return modeStore.get(instanceId);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Notify all React subscribers for a given instanceId
|
|
70
|
-
*/
|
|
71
|
-
function notifySubscribers(instanceId) {
|
|
72
|
-
const subscribers = componentSubscribers.get(instanceId);
|
|
73
|
-
if (subscribers) for (const onStoreChange of subscribers) onStoreChange();
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
56
|
* Determine if a mode change request should be auto-accepted without authorization
|
|
77
57
|
*/
|
|
78
58
|
function shouldAutoAcceptRequest(state, desiredMode, requestOwner) {
|
|
@@ -85,27 +65,22 @@ function shouldAutoAcceptRequest(state, desiredMode, requestOwner) {
|
|
|
85
65
|
return false;
|
|
86
66
|
}
|
|
87
67
|
/**
|
|
88
|
-
*
|
|
68
|
+
* Approve a request and reject all others (immutable update)
|
|
89
69
|
*/
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
state.
|
|
70
|
+
function approveRequestAndRejectOthers(instanceId, state, approvedRequest, excludeAuthId, decisionOwner, reason, emitApproval, set) {
|
|
71
|
+
const requestsToReject = [];
|
|
72
|
+
for (const request of state.pendingRequests.values()) if (request.authId !== excludeAuthId) requestsToReject.push(request);
|
|
73
|
+
const newModeOwners = approvedRequest.desiredMode !== DEFAULT_MODE ? mapSet(state.modeOwners, approvedRequest.desiredMode, approvedRequest.requestOwner) : state.modeOwners;
|
|
74
|
+
set({
|
|
75
|
+
mode: approvedRequest.desiredMode,
|
|
76
|
+
pendingRequests: mapClear(),
|
|
77
|
+
modeOwners: newModeOwners
|
|
78
|
+
});
|
|
93
79
|
mapModeBus.emit(MapModeEvents.changed, {
|
|
94
|
-
previousMode,
|
|
95
|
-
currentMode:
|
|
80
|
+
previousMode: state.mode,
|
|
81
|
+
currentMode: approvedRequest.desiredMode,
|
|
96
82
|
id: instanceId
|
|
97
83
|
});
|
|
98
|
-
notifySubscribers(instanceId);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Approve a request and reject all others
|
|
102
|
-
*/
|
|
103
|
-
function approveRequestAndRejectOthers(instanceId, state, approvedRequest, excludeAuthId, decisionOwner, reason, emitApproval) {
|
|
104
|
-
const requestsToReject = [];
|
|
105
|
-
for (const request of state.pendingRequests.values()) if (request.authId !== excludeAuthId) requestsToReject.push(request);
|
|
106
|
-
state.pendingRequests.clear();
|
|
107
|
-
setMode(instanceId, state, approvedRequest.desiredMode);
|
|
108
|
-
if (approvedRequest.desiredMode !== DEFAULT_MODE) state.modeOwners.set(approvedRequest.desiredMode, approvedRequest.requestOwner);
|
|
109
84
|
if (emitApproval) mapModeBus.emit(MapModeEvents.changeDecision, {
|
|
110
85
|
authId: approvedRequest.authId,
|
|
111
86
|
approved: true,
|
|
@@ -122,16 +97,16 @@ function approveRequestAndRejectOthers(instanceId, state, approvedRequest, exclu
|
|
|
122
97
|
});
|
|
123
98
|
}
|
|
124
99
|
/**
|
|
125
|
-
* Handle pending requests when returning to default mode
|
|
100
|
+
* Handle pending requests when returning to default mode (immutable update)
|
|
126
101
|
*/
|
|
127
|
-
function handlePendingRequestsOnDefaultMode(instanceId, state, previousMode) {
|
|
102
|
+
function handlePendingRequestsOnDefaultMode(instanceId, state, previousMode, set) {
|
|
128
103
|
const firstEntry = Array.from(state.pendingRequests.values())[0];
|
|
129
104
|
if (!firstEntry) return;
|
|
130
105
|
const previousModeOwner = state.modeOwners.get(previousMode);
|
|
131
106
|
if (!previousModeOwner) return;
|
|
132
107
|
if (firstEntry.desiredMode === DEFAULT_MODE) {
|
|
133
108
|
const allRequests = Array.from(state.pendingRequests.values());
|
|
134
|
-
|
|
109
|
+
set({ pendingRequests: mapClear() });
|
|
135
110
|
for (const request of allRequests) mapModeBus.emit(MapModeEvents.changeDecision, {
|
|
136
111
|
authId: request.authId,
|
|
137
112
|
approved: false,
|
|
@@ -139,24 +114,16 @@ function handlePendingRequestsOnDefaultMode(instanceId, state, previousMode) {
|
|
|
139
114
|
reason: "Request rejected - already in requested mode",
|
|
140
115
|
id: instanceId
|
|
141
116
|
});
|
|
142
|
-
} else approveRequestAndRejectOthers(instanceId, state, firstEntry, firstEntry.authId, previousModeOwner, "Auto-accepted when mode owner returned to default", true);
|
|
117
|
+
} else approveRequestAndRejectOthers(instanceId, state, firstEntry, firstEntry.authId, previousModeOwner, "Auto-accepted when mode owner returned to default", true, set);
|
|
143
118
|
}
|
|
144
119
|
/**
|
|
145
|
-
* Handle authorization decision
|
|
146
|
-
*
|
|
147
|
-
* Processes approval/rejection decisions from mode owners. Only the current mode's owner
|
|
148
|
-
* can make authorization decisions. If a decision comes from a non-owner, a warning is
|
|
149
|
-
* logged and the decision is ignored to prevent unauthorized mode changes.
|
|
150
|
-
*
|
|
151
|
-
* @param instanceId - The unique identifier for this map instance
|
|
152
|
-
* @param state - The mode state for this instance
|
|
153
|
-
* @param payload - The authorization decision containing authId, approved status, and owner
|
|
120
|
+
* Handle authorization decision (immutable update)
|
|
154
121
|
*/
|
|
155
|
-
function handleAuthorizationDecision(instanceId, state, payload) {
|
|
122
|
+
function handleAuthorizationDecision(instanceId, state, payload, set) {
|
|
156
123
|
const { approved, authId, owner: decisionOwner } = payload;
|
|
157
124
|
const currentModeOwner = state.modeOwners.get(state.mode);
|
|
158
125
|
if (decisionOwner !== currentModeOwner) {
|
|
159
|
-
|
|
126
|
+
logger.warn(`Authorization decision from "${decisionOwner}" ignored - not the owner of mode "${state.mode}" (owner: ${currentModeOwner || "none"})`);
|
|
160
127
|
return;
|
|
161
128
|
}
|
|
162
129
|
let matchingRequestOwner = null;
|
|
@@ -167,27 +134,37 @@ function handleAuthorizationDecision(instanceId, state, payload) {
|
|
|
167
134
|
break;
|
|
168
135
|
}
|
|
169
136
|
if (!(matchingRequest && matchingRequestOwner)) return;
|
|
170
|
-
if (approved) approveRequestAndRejectOthers(instanceId, state, matchingRequest, authId, decisionOwner, "", false);
|
|
171
|
-
else state.pendingRequests
|
|
137
|
+
if (approved) approveRequestAndRejectOthers(instanceId, state, matchingRequest, authId, decisionOwner, "", false, set);
|
|
138
|
+
else set({ pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner) });
|
|
172
139
|
}
|
|
173
140
|
/**
|
|
174
|
-
* Handle mode change request logic
|
|
141
|
+
* Handle mode change request logic (immutable update)
|
|
175
142
|
*/
|
|
176
|
-
function handleModeChangeRequest(instanceId, state, desiredMode, requestOwner) {
|
|
143
|
+
function handleModeChangeRequest(instanceId, state, desiredMode, requestOwner, set) {
|
|
177
144
|
const desiredModeOwner = state.modeOwners.get(desiredMode);
|
|
178
145
|
if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
state.
|
|
146
|
+
const newModeOwners = desiredMode !== DEFAULT_MODE && !desiredModeOwner ? mapSet(state.modeOwners, desiredMode, requestOwner) : state.modeOwners;
|
|
147
|
+
const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);
|
|
148
|
+
const previousMode = state.mode;
|
|
149
|
+
set({
|
|
150
|
+
mode: desiredMode,
|
|
151
|
+
modeOwners: newModeOwners,
|
|
152
|
+
pendingRequests: newPendingRequests
|
|
153
|
+
});
|
|
154
|
+
mapModeBus.emit(MapModeEvents.changed, {
|
|
155
|
+
previousMode,
|
|
156
|
+
currentMode: desiredMode,
|
|
157
|
+
id: instanceId
|
|
158
|
+
});
|
|
182
159
|
return;
|
|
183
160
|
}
|
|
184
161
|
const authId = uuid();
|
|
185
|
-
state.pendingRequests
|
|
162
|
+
set({ pendingRequests: mapSet(state.pendingRequests, requestOwner, {
|
|
186
163
|
authId,
|
|
187
164
|
desiredMode,
|
|
188
165
|
currentMode: state.mode,
|
|
189
166
|
requestOwner
|
|
190
|
-
});
|
|
167
|
+
}) });
|
|
191
168
|
mapModeBus.emit(MapModeEvents.changeAuthorization, {
|
|
192
169
|
authId,
|
|
193
170
|
desiredMode,
|
|
@@ -196,126 +173,15 @@ function handleModeChangeRequest(instanceId, state, desiredMode, requestOwner) {
|
|
|
196
173
|
});
|
|
197
174
|
}
|
|
198
175
|
/**
|
|
199
|
-
*
|
|
200
|
-
* All React subscribers will be notified via fan-out when the bus events fire.
|
|
201
|
-
* This prevents creating N bus listeners for N React components.
|
|
202
|
-
*
|
|
203
|
-
* @param instanceId - The unique identifier for the map mode instance
|
|
176
|
+
* Map mode store
|
|
204
177
|
*/
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
213
|
-
const unsubDecision = mapModeBus.on(MapModeEvents.changeDecision, (event) => {
|
|
214
|
-
const { id, approved, authId, owner } = event.payload;
|
|
215
|
-
if (id !== instanceId) return;
|
|
216
|
-
handleAuthorizationDecision(instanceId, state, {
|
|
217
|
-
approved,
|
|
218
|
-
authId,
|
|
219
|
-
owner
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {
|
|
223
|
-
const { currentMode, previousMode, id } = event.payload;
|
|
224
|
-
if (id !== instanceId) return;
|
|
225
|
-
if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) handlePendingRequestsOnDefaultMode(instanceId, state, previousMode);
|
|
226
|
-
});
|
|
227
|
-
busUnsubscribers.set(instanceId, () => {
|
|
228
|
-
unsubRequest();
|
|
229
|
-
unsubDecision();
|
|
230
|
-
unsubChanged();
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Cleans up the bus listener if no React subscribers remain.
|
|
235
|
-
*
|
|
236
|
-
* @param instanceId - The unique identifier for the map mode instance
|
|
237
|
-
*/
|
|
238
|
-
function cleanupBusListenerIfNeeded(instanceId) {
|
|
239
|
-
const subscribers = componentSubscribers.get(instanceId);
|
|
240
|
-
if (!subscribers || subscribers.size === 0) {
|
|
241
|
-
const unsub = busUnsubscribers.get(instanceId);
|
|
242
|
-
if (unsub) {
|
|
243
|
-
unsub();
|
|
244
|
-
busUnsubscribers.delete(instanceId);
|
|
245
|
-
}
|
|
246
|
-
modeStore.delete(instanceId);
|
|
247
|
-
componentSubscribers.delete(instanceId);
|
|
248
|
-
subscriptionCache.delete(instanceId);
|
|
249
|
-
snapshotCache.delete(instanceId);
|
|
250
|
-
serverSnapshotCache.delete(instanceId);
|
|
251
|
-
requestModeChangeCache.delete(instanceId);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Creates or retrieves a cached subscription function for a given instanceId.
|
|
256
|
-
* Uses a fan-out pattern: 1 bus listener -> N React subscribers.
|
|
257
|
-
* Automatically cleans up map mode state when the last subscriber unsubscribes.
|
|
258
|
-
*
|
|
259
|
-
* @param instanceId - The unique identifier for the map mode instance
|
|
260
|
-
* @returns A subscription function for useSyncExternalStore
|
|
261
|
-
*/
|
|
262
|
-
function getOrCreateSubscription(instanceId) {
|
|
263
|
-
const subscription = subscriptionCache.get(instanceId) ?? ((onStoreChange) => {
|
|
264
|
-
getOrCreateState(instanceId);
|
|
265
|
-
ensureBusListener(instanceId);
|
|
266
|
-
let subscriberSet = componentSubscribers.get(instanceId);
|
|
267
|
-
if (!subscriberSet) {
|
|
268
|
-
subscriberSet = /* @__PURE__ */ new Set();
|
|
269
|
-
componentSubscribers.set(instanceId, subscriberSet);
|
|
270
|
-
}
|
|
271
|
-
subscriberSet.add(onStoreChange);
|
|
272
|
-
return () => {
|
|
273
|
-
const currentSubscriberSet = componentSubscribers.get(instanceId);
|
|
274
|
-
if (currentSubscriberSet) currentSubscriberSet.delete(onStoreChange);
|
|
275
|
-
cleanupBusListenerIfNeeded(instanceId);
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
subscriptionCache.set(instanceId, subscription);
|
|
279
|
-
return subscription;
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Creates or retrieves a cached snapshot function for a given instanceId.
|
|
283
|
-
* The string returned gets equality checked, so it needs to be stable or React re-renders unnecessarily.
|
|
284
|
-
*
|
|
285
|
-
* @param instanceId - The unique identifier for the map mode instance
|
|
286
|
-
* @returns A snapshot function for useSyncExternalStore
|
|
287
|
-
*/
|
|
288
|
-
function getOrCreateSnapshot(instanceId) {
|
|
289
|
-
const snapshot = snapshotCache.get(instanceId) ?? (() => {
|
|
290
|
-
const state = modeStore.get(instanceId);
|
|
291
|
-
if (!state) return DEFAULT_MODE;
|
|
292
|
-
return state.mode;
|
|
293
|
-
});
|
|
294
|
-
snapshotCache.set(instanceId, snapshot);
|
|
295
|
-
return snapshot;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Creates or retrieves a cached server snapshot function for a given instanceId.
|
|
299
|
-
* Server snapshots always return the default mode since mode state is client-only.
|
|
300
|
-
* Required for SSR/RSC compatibility with useSyncExternalStore.
|
|
301
|
-
*
|
|
302
|
-
* @param instanceId - The unique identifier for the map mode instance
|
|
303
|
-
* @returns A server snapshot function for useSyncExternalStore
|
|
304
|
-
*/
|
|
305
|
-
function getOrCreateServerSnapshot(instanceId) {
|
|
306
|
-
const serverSnapshot = serverSnapshotCache.get(instanceId) ?? (() => DEFAULT_MODE);
|
|
307
|
-
serverSnapshotCache.set(instanceId, serverSnapshot);
|
|
308
|
-
return serverSnapshot;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Creates or retrieves a cached requestModeChange function for a given instanceId.
|
|
312
|
-
* This maintains referential stability for the function reference.
|
|
313
|
-
*
|
|
314
|
-
* @param instanceId - The unique identifier for the map mode instance
|
|
315
|
-
* @returns A requestModeChange function for this instance
|
|
316
|
-
*/
|
|
317
|
-
function getOrCreateRequestModeChange(instanceId) {
|
|
318
|
-
const requestModeChange = requestModeChangeCache.get(instanceId) ?? ((desiredMode, requestOwner) => {
|
|
178
|
+
const modeStore = createMapStore({
|
|
179
|
+
defaultState: {
|
|
180
|
+
mode: DEFAULT_MODE,
|
|
181
|
+
modeOwners: /* @__PURE__ */ new Map(),
|
|
182
|
+
pendingRequests: /* @__PURE__ */ new Map()
|
|
183
|
+
},
|
|
184
|
+
actions: (instanceId) => ({ requestModeChange: (desiredMode, requestOwner) => {
|
|
319
185
|
const trimmedDesiredMode = desiredMode.trim();
|
|
320
186
|
const trimmedRequestOwner = requestOwner.trim();
|
|
321
187
|
if (!trimmedDesiredMode) throw new Error("requestModeChange requires non-empty desiredMode");
|
|
@@ -325,9 +191,47 @@ function getOrCreateRequestModeChange(instanceId) {
|
|
|
325
191
|
owner: trimmedRequestOwner,
|
|
326
192
|
id: instanceId
|
|
327
193
|
});
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
|
|
194
|
+
} }),
|
|
195
|
+
bus: (instanceId, { get, set }) => {
|
|
196
|
+
const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {
|
|
197
|
+
const { desiredMode, owner: requestOwner, id } = event.payload;
|
|
198
|
+
const state = get();
|
|
199
|
+
if (id !== instanceId || desiredMode === state.mode) return;
|
|
200
|
+
handleModeChangeRequest(instanceId, state, desiredMode, requestOwner, set);
|
|
201
|
+
});
|
|
202
|
+
const unsubDecision = mapModeBus.on(MapModeEvents.changeDecision, (event) => {
|
|
203
|
+
const { id, approved, authId, owner } = event.payload;
|
|
204
|
+
if (id !== instanceId) return;
|
|
205
|
+
handleAuthorizationDecision(instanceId, get(), {
|
|
206
|
+
approved,
|
|
207
|
+
authId,
|
|
208
|
+
owner
|
|
209
|
+
}, set);
|
|
210
|
+
});
|
|
211
|
+
const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {
|
|
212
|
+
const { currentMode, previousMode, id } = event.payload;
|
|
213
|
+
if (id !== instanceId) return;
|
|
214
|
+
const state = get();
|
|
215
|
+
if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) handlePendingRequestsOnDefaultMode(instanceId, state, previousMode, set);
|
|
216
|
+
});
|
|
217
|
+
return () => {
|
|
218
|
+
unsubRequest();
|
|
219
|
+
unsubDecision();
|
|
220
|
+
unsubChanged();
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
/**
|
|
225
|
+
* Get the current mode for a map instance
|
|
226
|
+
*/
|
|
227
|
+
function getMode(mapId) {
|
|
228
|
+
return modeStore.get(mapId).mode;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Hook for current mode value
|
|
232
|
+
*/
|
|
233
|
+
function useMode(mapId) {
|
|
234
|
+
return modeStore.useSelector(mapId, (state) => state.mode);
|
|
331
235
|
}
|
|
332
236
|
/**
|
|
333
237
|
* Get the owner of the current mode for a given map instance
|
|
@@ -335,36 +239,26 @@ function getOrCreateRequestModeChange(instanceId) {
|
|
|
335
239
|
*/
|
|
336
240
|
function getCurrentModeOwner(instanceId) {
|
|
337
241
|
const state = modeStore.get(instanceId);
|
|
338
|
-
if (!state) return;
|
|
339
242
|
return state.modeOwners.get(state.mode);
|
|
340
243
|
}
|
|
341
244
|
/**
|
|
245
|
+
* Check if a given owner is registered as the owner of any mode.
|
|
246
|
+
* This includes both active mode owners and pending mode requests.
|
|
247
|
+
* @internal - For internal map-toolkit use only
|
|
248
|
+
*/
|
|
249
|
+
function isRegisteredModeOwner(instanceId, owner) {
|
|
250
|
+
const state = modeStore.get(instanceId);
|
|
251
|
+
for (const modeOwner of state.modeOwners.values()) if (modeOwner === owner) return true;
|
|
252
|
+
if (state.pendingRequests.has(owner)) return true;
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
342
256
|
* Manually clear map mode state for a specific instanceId.
|
|
343
|
-
* This is typically not needed as cleanup happens automatically when all subscribers unmount.
|
|
344
|
-
* Use this only in advanced scenarios where manual cleanup is required.
|
|
345
|
-
*
|
|
346
|
-
* @param instanceId - The unique identifier for the map mode instance to clear
|
|
347
|
-
*
|
|
348
|
-
* @example
|
|
349
|
-
* ```tsx
|
|
350
|
-
* // Manual cleanup (rarely needed)
|
|
351
|
-
* clearMapModeState('my-map-instance');
|
|
352
|
-
* ```
|
|
353
257
|
*/
|
|
354
258
|
function clearMapModeState(instanceId) {
|
|
355
|
-
|
|
356
|
-
if (unsub) {
|
|
357
|
-
unsub();
|
|
358
|
-
busUnsubscribers.delete(instanceId);
|
|
359
|
-
}
|
|
360
|
-
modeStore.delete(instanceId);
|
|
361
|
-
componentSubscribers.delete(instanceId);
|
|
362
|
-
subscriptionCache.delete(instanceId);
|
|
363
|
-
snapshotCache.delete(instanceId);
|
|
364
|
-
serverSnapshotCache.delete(instanceId);
|
|
365
|
-
requestModeChangeCache.delete(instanceId);
|
|
259
|
+
modeStore.clear(instanceId);
|
|
366
260
|
}
|
|
367
261
|
|
|
368
262
|
//#endregion
|
|
369
|
-
export { clearMapModeState, getCurrentModeOwner,
|
|
263
|
+
export { clearMapModeState, getCurrentModeOwner, getMode, isRegisteredModeOwner, modeStore, useMode };
|
|
370
264
|
//# sourceMappingURL=store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/store.ts"],"sourcesContent":["/*\n * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n * Provides type-safe event emission and listening for all map mode state changes.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n * @internal\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * Type representing the state for a single map mode instance\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Store for map mode state keyed by instanceId\n */\nconst modeStore = new Map<UniqueId, MapModeState>();\n\n/**\n * Track React component subscribers per instanceId (for fan-out notifications).\n * Each Set contains onStoreChange callbacks from useSyncExternalStore.\n */\nconst componentSubscribers = new Map<UniqueId, Set<() => void>>();\n\n/**\n * Cache of bus unsubscribe functions (1 per instanceId).\n * This ensures we only have one bus listener per map mode instance, regardless of\n * how many React components subscribe to it.\n */\nconst busUnsubscribers = new Map<UniqueId, () => void>();\n\ntype Subscription = (onStoreChange: () => void) => () => void;\n/**\n * Cache of subscription functions per instanceId to avoid recreating on every render\n */\nconst subscriptionCache = new Map<UniqueId, Subscription>();\n\n/**\n * Cache of snapshot functions per instanceId to maintain referential stability\n */\nconst snapshotCache = new Map<UniqueId, () => string>();\n\n/**\n * Cache of server snapshot functions per instanceId to maintain referential stability.\n * Server snapshots always return default mode since mode state is client-only.\n */\nconst serverSnapshotCache = new Map<UniqueId, () => string>();\n\n/**\n * Cache of requestModeChange functions per instanceId to maintain referential stability\n */\nconst requestModeChangeCache = new Map<\n UniqueId,\n (desiredMode: string, requestOwner: string) => void\n>();\n\n/**\n * Get or create mode state for a given instanceId\n */\nfunction getOrCreateState(instanceId: UniqueId): MapModeState {\n if (!modeStore.has(instanceId)) {\n modeStore.set(instanceId, {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n });\n }\n // biome-ignore lint/style/noNonNullAssertion: State guaranteed to exist after has() check above\n return modeStore.get(instanceId)!;\n}\n\n/**\n * Notify all React subscribers for a given instanceId\n */\nfunction notifySubscribers(instanceId: UniqueId): void {\n const subscribers = componentSubscribers.get(instanceId);\n if (subscribers) {\n for (const onStoreChange of subscribers) {\n onStoreChange();\n }\n }\n}\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Set mode and emit change event\n */\nfunction setMode(\n instanceId: UniqueId,\n state: MapModeState,\n newMode: string,\n): void {\n const previousMode = state.mode;\n state.mode = newMode;\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: newMode,\n id: instanceId,\n });\n\n notifySubscribers(instanceId);\n}\n\n/**\n * Approve a request and reject all others\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Clear all pending requests BEFORE changing mode\n state.pendingRequests.clear();\n\n // Change mode\n setMode(instanceId, state, approvedRequest.desiredMode);\n\n // Store the new mode's owner (unless it's default mode)\n if (approvedRequest.desiredMode !== DEFAULT_MODE) {\n state.modeOwners.set(\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n );\n }\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n state.pendingRequests.clear();\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n );\n }\n}\n\n/**\n * Handle authorization decision\n *\n * Processes approval/rejection decisions from mode owners. Only the current mode's owner\n * can make authorization decisions. If a decision comes from a non-owner, a warning is\n * logged and the decision is ignored to prevent unauthorized mode changes.\n *\n * @param instanceId - The unique identifier for this map instance\n * @param state - The mode state for this instance\n * @param payload - The authorization decision containing authId, approved status, and owner\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n // Logs a warning if unauthorized component attempts to make decisions\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n console.warn(\n `[MapMode] Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n );\n } else {\n state.pendingRequests.delete(matchingRequestOwner);\n }\n}\n\n/**\n * Handle mode change request logic\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n setMode(instanceId, state, desiredMode);\n\n // Store the desired mode's owner unless it's default\n if (desiredMode !== DEFAULT_MODE && !desiredModeOwner) {\n state.modeOwners.set(desiredMode, requestOwner);\n }\n\n // Clear requester's pending request since mode changed successfully\n state.pendingRequests.delete(requestOwner);\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n state.pendingRequests.set(requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Ensures a single bus listener exists for the given instanceId.\n * All React subscribers will be notified via fan-out when the bus events fire.\n * This prevents creating N bus listeners for N React components.\n *\n * @param instanceId - The unique identifier for the map mode instance\n */\nfunction ensureBusListener(instanceId: UniqueId): void {\n if (busUnsubscribers.has(instanceId)) {\n return; // Already listening\n }\n\n const state = getOrCreateState(instanceId);\n\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(instanceId, state, desiredMode, requestOwner);\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(MapModeEvents.changeDecision, (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(instanceId, state, { approved, authId, owner });\n });\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(instanceId, state, previousMode);\n }\n });\n\n // Store composite cleanup function\n busUnsubscribers.set(instanceId, () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n });\n}\n\n/**\n * Cleans up the bus listener if no React subscribers remain.\n *\n * @param instanceId - The unique identifier for the map mode instance\n */\nfunction cleanupBusListenerIfNeeded(instanceId: UniqueId): void {\n const subscribers = componentSubscribers.get(instanceId);\n\n if (!subscribers || subscribers.size === 0) {\n // No more React subscribers - clean up bus listener\n const unsub = busUnsubscribers.get(instanceId);\n if (unsub) {\n unsub();\n busUnsubscribers.delete(instanceId);\n }\n\n // Clean up all state\n modeStore.delete(instanceId);\n componentSubscribers.delete(instanceId);\n subscriptionCache.delete(instanceId);\n snapshotCache.delete(instanceId);\n serverSnapshotCache.delete(instanceId);\n requestModeChangeCache.delete(instanceId);\n }\n}\n\n/**\n * Creates or retrieves a cached subscription function for a given instanceId.\n * Uses a fan-out pattern: 1 bus listener -> N React subscribers.\n * Automatically cleans up map mode state when the last subscriber unsubscribes.\n *\n * @param instanceId - The unique identifier for the map mode instance\n * @returns A subscription function for useSyncExternalStore\n */\nexport function getOrCreateSubscription(\n instanceId: UniqueId,\n): (onStoreChange: () => void) => () => void {\n const subscription =\n subscriptionCache.get(instanceId) ??\n ((onStoreChange: () => void) => {\n // Ensure state exists\n getOrCreateState(instanceId);\n\n // Ensure single bus listener exists for this instanceId\n ensureBusListener(instanceId);\n\n // Get or create the subscriber set for this map instance, then add this component's callback\n let subscriberSet = componentSubscribers.get(instanceId);\n if (!subscriberSet) {\n subscriberSet = new Set();\n componentSubscribers.set(instanceId, subscriberSet);\n }\n subscriberSet.add(onStoreChange);\n\n // Return cleanup function to remove this component's subscription\n return () => {\n const currentSubscriberSet = componentSubscribers.get(instanceId);\n if (currentSubscriberSet) {\n currentSubscriberSet.delete(onStoreChange);\n }\n\n // Clean up bus listener if this was the last React subscriber\n cleanupBusListenerIfNeeded(instanceId);\n };\n });\n\n subscriptionCache.set(instanceId, subscription);\n\n return subscription;\n}\n\n/**\n * Creates or retrieves a cached snapshot function for a given instanceId.\n * The string returned gets equality checked, so it needs to be stable or React re-renders unnecessarily.\n *\n * @param instanceId - The unique identifier for the map mode instance\n * @returns A snapshot function for useSyncExternalStore\n */\nexport function getOrCreateSnapshot(instanceId: UniqueId): () => string {\n const snapshot =\n snapshotCache.get(instanceId) ??\n (() => {\n const state = modeStore.get(instanceId);\n if (!state) {\n return DEFAULT_MODE;\n }\n return state.mode;\n });\n\n snapshotCache.set(instanceId, snapshot);\n\n return snapshot;\n}\n\n/**\n * Creates or retrieves a cached server snapshot function for a given instanceId.\n * Server snapshots always return the default mode since mode state is client-only.\n * Required for SSR/RSC compatibility with useSyncExternalStore.\n *\n * @param instanceId - The unique identifier for the map mode instance\n * @returns A server snapshot function for useSyncExternalStore\n */\nexport function getOrCreateServerSnapshot(instanceId: UniqueId): () => string {\n const serverSnapshot =\n serverSnapshotCache.get(instanceId) ?? (() => DEFAULT_MODE);\n\n serverSnapshotCache.set(instanceId, serverSnapshot);\n\n return serverSnapshot;\n}\n\n/**\n * Creates or retrieves a cached requestModeChange function for a given instanceId.\n * This maintains referential stability for the function reference.\n *\n * @param instanceId - The unique identifier for the map mode instance\n * @returns A requestModeChange function for this instance\n */\nexport function getOrCreateRequestModeChange(\n instanceId: UniqueId,\n): (desiredMode: string, requestOwner: string) => void {\n const requestModeChange =\n requestModeChangeCache.get(instanceId) ??\n ((desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n });\n\n requestModeChangeCache.set(instanceId, requestModeChange);\n\n return requestModeChange;\n}\n\n/**\n * Get the owner of the current mode for a given map instance\n * @internal - For internal map-toolkit use only\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n if (!state) {\n return undefined;\n }\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Manually clear map mode state for a specific instanceId.\n * This is typically not needed as cleanup happens automatically when all subscribers unmount.\n * Use this only in advanced scenarios where manual cleanup is required.\n *\n * @param instanceId - The unique identifier for the map mode instance to clear\n *\n * @example\n * ```tsx\n * // Manual cleanup (rarely needed)\n * clearMapModeState('my-map-instance');\n * ```\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n // Unsubscribe from bus if listening\n const unsub = busUnsubscribers.get(instanceId);\n if (unsub) {\n unsub();\n busUnsubscribers.delete(instanceId);\n }\n\n // Clear all state\n modeStore.delete(instanceId);\n componentSubscribers.delete(instanceId);\n subscriptionCache.delete(instanceId);\n snapshotCache.delete(instanceId);\n serverSnapshotCache.delete(instanceId);\n requestModeChangeCache.delete(instanceId);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/store.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n/**\n * Map Mode Store\n *\n * Manages mode state with ownership-based authorization.\n *\n * @example\n * ```tsx\n * import { modeStore } from '@accelint/map-toolkit/map-mode';\n *\n * function MapControls({ mapId }) {\n * const { state, requestModeChange } = modeStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Current mode: {state.mode}</p>\n * <button onClick={() => requestModeChange('draw', 'draw-layer')}>\n * Draw Mode\n * </button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport {\n createMapStore,\n mapClear,\n mapDelete,\n mapSet,\n} from '../shared/create-map-store';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { StoreHelpers } from '../shared/create-map-store';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[MapMode]',\n pretty: true,\n});\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * State shape for map mode\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Actions for map mode\n */\ntype MapModeActions = {\n /** Request a mode change */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Approve a request and reject all others (immutable update)\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Build immutable updates: clear pending requests, update owners\n const newModeOwners =\n approvedRequest.desiredMode !== DEFAULT_MODE\n ? mapSet(\n state.modeOwners,\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n )\n : state.modeOwners;\n\n // Immutable update: clear pending requests, update owners, change mode\n set({\n mode: approvedRequest.desiredMode,\n pendingRequests: mapClear<string, PendingRequest>(),\n modeOwners: newModeOwners,\n });\n\n // Emit mode changed event\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode: state.mode,\n currentMode: approvedRequest.desiredMode,\n id: instanceId,\n });\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode (immutable update)\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n\n // Immutable update: clear pending requests\n set({ pendingRequests: mapClear<string, PendingRequest>() });\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n set,\n );\n }\n}\n\n/**\n * Handle authorization decision (immutable update)\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n logger.warn(\n `Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n set,\n );\n } else {\n // Immutable update: remove the rejected request\n set({\n pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner),\n });\n }\n}\n\n/**\n * Handle mode change request logic (immutable update)\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n // Build immutable updates\n const newModeOwners =\n desiredMode !== DEFAULT_MODE && !desiredModeOwner\n ? mapSet(state.modeOwners, desiredMode, requestOwner)\n : state.modeOwners;\n\n // Clear requester's pending request since mode changed successfully\n const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);\n\n const previousMode = state.mode;\n\n // Immutable update\n set({\n mode: desiredMode,\n modeOwners: newModeOwners,\n pendingRequests: newPendingRequests,\n });\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: desiredMode,\n id: instanceId,\n });\n\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n // Immutable update: add pending request\n set({\n pendingRequests: mapSet(state.pendingRequests, requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n }),\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Map mode store\n */\nexport const modeStore = createMapStore<MapModeState, MapModeActions>({\n defaultState: {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n },\n\n actions: (instanceId) => ({\n requestModeChange: (desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n },\n }),\n\n bus: (instanceId, { get, set }) => {\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n const state = get();\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(\n instanceId,\n state,\n desiredMode,\n requestOwner,\n set,\n );\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(\n MapModeEvents.changeDecision,\n (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(\n instanceId,\n get(),\n { approved, authId, owner },\n set,\n );\n },\n );\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n const state = get();\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(\n instanceId,\n state,\n previousMode,\n set,\n );\n }\n });\n\n return () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current mode for a map instance\n */\nexport function getMode(mapId: UniqueId): string {\n return modeStore.get(mapId).mode;\n}\n\n/**\n * Hook for current mode value\n */\nexport function useMode(mapId: UniqueId): string {\n return modeStore.useSelector(mapId, (state) => state.mode);\n}\n\n/**\n * Get the owner of the current mode for a given map instance\n * @internal - For internal map-toolkit use only\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Check if a given owner is registered as the owner of any mode.\n * This includes both active mode owners and pending mode requests.\n * @internal - For internal map-toolkit use only\n */\nexport function isRegisteredModeOwner(\n instanceId: UniqueId,\n owner: string,\n): boolean {\n const state = modeStore.get(instanceId);\n\n // Check active mode owners\n for (const modeOwner of state.modeOwners.values()) {\n if (modeOwner === owner) {\n return true;\n }\n }\n\n // Check pending mode requests (owner is the key in pendingRequests map)\n if (state.pendingRequests.has(owner)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Manually clear map mode state for a specific instanceId.\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n modeStore.clear(instanceId);\n}\n"],"mappings":"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"}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
'use client';
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { modeStore } from "./store.js";
|
|
17
17
|
import { MapContext } from "../deckgl/base-map/provider.js";
|
|
18
|
-
import { useContext, useMemo
|
|
18
|
+
import { useContext, useMemo } from "react";
|
|
19
19
|
|
|
20
20
|
//#region src/map-mode/use-map-mode.ts
|
|
21
21
|
/**
|
|
@@ -60,11 +60,12 @@ function useMapMode(id) {
|
|
|
60
60
|
const contextId = useContext(MapContext);
|
|
61
61
|
const actualId = id ?? contextId;
|
|
62
62
|
if (!actualId) throw new Error("useMapMode requires either an id parameter or to be used within a MapProvider");
|
|
63
|
-
const mode =
|
|
63
|
+
const mode = modeStore.useSelector(actualId, (state) => state.mode);
|
|
64
|
+
const { requestModeChange } = modeStore.actions(actualId);
|
|
64
65
|
return useMemo(() => ({
|
|
65
66
|
mode,
|
|
66
|
-
requestModeChange
|
|
67
|
-
}), [mode,
|
|
67
|
+
requestModeChange
|
|
68
|
+
}), [mode, requestModeChange]);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-map-mode.js","names":[],"sources":["../../src/map-mode/use-map-mode.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"use-map-mode.js","names":[],"sources":["../../src/map-mode/use-map-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n'use client';\n\nimport 'client-only';\nimport { useContext, useMemo } from 'react';\nimport { MapContext } from '../deckgl/base-map/provider';\nimport { modeStore } from './store';\nimport type { UniqueId } from '@accelint/core';\n\n/**\n * Return value for the useMapMode hook\n */\nexport type UseMapModeReturn = {\n /** The current active map mode */\n mode: string;\n /** Function to request a mode change with ownership */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Hook to access the map mode state and actions.\n *\n * This hook uses `useSyncExternalStore` to subscribe to map mode state changes,\n * providing concurrent-safe mode state updates. Uses a fan-out pattern where\n * a single bus listener per map instance notifies N React component subscribers.\n *\n * @param id - Optional map instance ID. If not provided, will use the ID from `MapContext`.\n * @returns The current map mode and requestModeChange function\n * @throws Error if no `id` is provided and hook is used outside of `MapProvider`\n *\n * @example\n * ```tsx\n * // Inside MapProvider (within BaseMap children) - uses context\n * // Only Deck.gl layer components can be children\n * function CustomDeckLayer() {\n * const { mode, requestModeChange } = useMapMode();\n *\n * const handleClick = (info: PickingInfo) => {\n * requestModeChange('editing', 'custom-layer-id');\n * };\n *\n * return <ScatterplotLayer onClick={handleClick} />;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Outside MapProvider - pass id directly\n * function ExternalControl({ mapId }: { mapId: UniqueId }) {\n * const { mode, requestModeChange } = useMapMode(mapId);\n *\n * return <button onClick={() => requestModeChange('default', 'external')}>\n * Reset to Default (current: {mode})\n * </button>;\n * }\n * ```\n */\nexport function useMapMode(id?: UniqueId): UseMapModeReturn {\n const contextId = useContext(MapContext);\n const actualId = id ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useMapMode requires either an id parameter or to be used within a MapProvider',\n );\n }\n\n // Use selector to get just the mode string (primitive comparison works with useSyncExternalStore)\n const mode = modeStore.useSelector(actualId, (state) => state.mode);\n\n // Get actions separately (stable reference)\n const { requestModeChange } = modeStore.actions(actualId);\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n mode,\n requestModeChange,\n }),\n [mode, requestModeChange],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,SAAgB,WAAW,IAAiC;CAC1D,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,WAAW,MAAM;AAEvB,KAAI,CAAC,SACH,OAAM,IAAI,MACR,gFACD;CAIH,MAAM,OAAO,UAAU,YAAY,WAAW,UAAU,MAAM,KAAK;CAGnE,MAAM,EAAE,sBAAsB,UAAU,QAAQ,SAAS;AAGzD,QAAO,eACE;EACL;EACA;EACD,GACD,CAAC,MAAM,kBAAkB,CAC1B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-maplibre.js","names":["MapLibre"],"sources":["../../../src/maplibre/hooks/use-maplibre.ts"],"sourcesContent":["/*\n * Copyright
|
|
1
|
+
{"version":3,"file":"use-maplibre.js","names":["MapLibre"],"sources":["../../../src/maplibre/hooks/use-maplibre.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { type IControl, Map as MapLibre, type MapOptions } from 'maplibre-gl';\nimport { useEffect, useRef } from 'react';\n\n/**\n * Hook to integrate a MapLibre GL map with a Deck.gl instance.\n *\n * This hook manages the lifecycle of a MapLibre map, including initialization,\n * style updates, and cleanup. It ensures the Deck.gl control is properly added\n * to the map and handles cleanup when the component unmounts.\n *\n * @param deck - The Deck.gl IControl instance to add to the map\n * @param styleUrl - The MapLibre style URL to use for the map\n * @param options - MapLibre map options (container, center, zoom, etc.)\n * @returns A ref containing the MapLibre map instance (ref.current may be null before initialization)\n *\n * @example\n * ```tsx\n * function MapComponent() {\n * const deckglInstance = useDeckgl();\n * const container = useId();\n *\n * const mapOptions = useMemo(() => ({\n * container,\n * center: [-122.4, 37.8],\n * zoom: 12,\n * }), [container]);\n *\n * useMapLibre(\n * deckglInstance as IControl,\n * 'https://tiles.example.com/style.json',\n * mapOptions\n * );\n *\n * return <div id={container} />;\n * }\n * ```\n */\nexport function useMapLibre(\n deck: IControl | null,\n styleUrl: string,\n options: MapOptions,\n) {\n const mapRef = useRef<MapLibre | null>(null);\n // Using a ref for options to avoid re-creating the map when options object reference changes\n // The map is only created once on mount, options changes after that are ignored\n const optionsRef = useRef(options);\n // using a ref in the initial setup so that it doesn't cause a re-run of the effect on change\n const styleRef = useRef(styleUrl);\n\n // Initialize MapLibre instance once\n useEffect(() => {\n if (deck && !mapRef.current) {\n mapRef.current = new MapLibre({\n ...optionsRef.current,\n style: styleRef.current,\n });\n\n mapRef.current.once('style.load', () => {\n mapRef.current?.setProjection({ type: 'mercator' });\n mapRef.current?.addControl(deck);\n });\n\n return () => {\n if (mapRef.current) {\n mapRef.current.removeControl(deck);\n mapRef.current.remove();\n mapRef.current = null;\n }\n };\n }\n }, [deck]);\n\n // Update style when it changes\n useEffect(() => {\n if (mapRef.current) {\n mapRef.current.setStyle(styleUrl);\n }\n }, [styleUrl]);\n\n return mapRef;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,YACd,MACA,UACA,SACA;CACA,MAAM,SAAS,OAAwB,KAAK;CAG5C,MAAM,aAAa,OAAO,QAAQ;CAElC,MAAM,WAAW,OAAO,SAAS;AAGjC,iBAAgB;AACd,MAAI,QAAQ,CAAC,OAAO,SAAS;AAC3B,UAAO,UAAU,IAAIA,IAAS;IAC5B,GAAG,WAAW;IACd,OAAO,SAAS;IACjB,CAAC;AAEF,UAAO,QAAQ,KAAK,oBAAoB;AACtC,WAAO,SAAS,cAAc,EAAE,MAAM,YAAY,CAAC;AACnD,WAAO,SAAS,WAAW,KAAK;KAChC;AAEF,gBAAa;AACX,QAAI,OAAO,SAAS;AAClB,YAAO,QAAQ,cAAc,KAAK;AAClC,YAAO,QAAQ,QAAQ;AACvB,YAAO,UAAU;;;;IAItB,CAAC,KAAK,CAAC;AAGV,iBAAgB;AACd,MAAI,OAAO,QACT,QAAO,QAAQ,SAAS,SAAS;IAElC,CAAC,SAAS,CAAC;AAEd,QAAO"}
|
package/dist/maplibre/index.d.ts
CHANGED
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { INITIAL_VIEW_STATE } from "./constants.js";
|
|
14
13
|
import { useMapLibre } from "./hooks/use-maplibre.js";
|
|
15
|
-
|
|
14
|
+
import { DEFAULT_VIEW_STATE } from "../shared/constants.js";
|
|
15
|
+
export { DEFAULT_VIEW_STATE, useMapLibre };
|