@ngis/plugin-sdk 0.4.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/LICENSE +13 -0
- package/README.md +31 -0
- package/dist/index.cjs +76 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.mjs +102 -0
- package/dist/src/api/analysis.d.ts +140 -0
- package/dist/src/api/resources.d.ts +530 -0
- package/dist/src/api/scenes.d.ts +251 -0
- package/dist/src/api/users.d.ts +22 -0
- package/dist/src/domain/algorithms/deck-layer-style.d.ts +34 -0
- package/dist/src/domain/algorithms/maplibre-style.d.ts +63 -0
- package/dist/src/domain/algorithms/raster-color-ramps.d.ts +37 -0
- package/dist/src/domain/algorithms/scene-layer-type.d.ts +25 -0
- package/dist/src/domain/algorithms/symbol-compile.d.ts +100 -0
- package/dist/src/domain/algorithms/symbol-params.d.ts +90 -0
- package/dist/src/domain/map/bounds.d.ts +8 -0
- package/dist/src/domain/map/constants.d.ts +40 -0
- package/dist/src/domain/map/layer-builders.d.ts +32 -0
- package/dist/src/domain/map/rendered-layers.d.ts +30 -0
- package/dist/src/domain/map/style-ready.d.ts +24 -0
- package/dist/src/domain/map/tile-url.d.ts +20 -0
- package/dist/src/domain/map/types.d.ts +128 -0
- package/dist/src/domain/models/deck-layer-style.d.ts +211 -0
- package/dist/src/domain/models/scene-layer-type.d.ts +23 -0
- package/dist/src/domain/models/symbol.d.ts +424 -0
- package/dist/src/lib/api.d.ts +82 -0
- package/dist/src/lib/auth-session.d.ts +22 -0
- package/dist/src/lib/raster-color-layer/RasterColorLayer.d.ts +48 -0
- package/dist/src/lib/raster-color-layer/TileGrid.d.ts +31 -0
- package/dist/src/lib/raster-color-layer/colorRamp.d.ts +18 -0
- package/dist/src/lib/raster-color-layer/shaders/raster-color.frag.d.ts +2 -0
- package/dist/src/lib/raster-color-layer/shaders/raster-color.vert.d.ts +2 -0
- package/dist/src/lib/raster-color-layer/zoomInterpolate.d.ts +2 -0
- package/dist/src/sdk/commands.d.ts +113 -0
- package/dist/src/sdk/context/keys.d.ts +148 -0
- package/dist/src/sdk/context/publishers.d.ts +198 -0
- package/dist/src/sdk/context/store.d.ts +78 -0
- package/dist/src/sdk/context/when.d.ts +69 -0
- package/dist/src/sdk/contributions/dispatch.d.ts +78 -0
- package/dist/src/sdk/contributions/panel-id.d.ts +25 -0
- package/dist/src/sdk/contributions/registry.d.ts +199 -0
- package/dist/src/sdk/contributions/symbol-renderers.d.ts +107 -0
- package/dist/src/sdk/contributions/types.d.ts +288 -0
- package/dist/src/sdk/contributions/views-bridge.d.ts +92 -0
- package/dist/src/sdk/define-plugin.d.ts +17 -0
- package/dist/src/sdk/facets/storage.d.ts +152 -0
- package/dist/src/sdk/facets/types.d.ts +370 -0
- package/dist/src/sdk/host-api.d.ts +256 -0
- package/dist/src/sdk/map-api.d.ts +288 -0
- package/dist/src/sdk/panels.d.ts +130 -0
- package/dist/src/sdk/plugin.d.ts +145 -0
- package/dist/src/sdk/rail-tools.d.ts +58 -0
- package/dist/src/sdk/scopes.d.ts +158 -0
- package/dist/src/systems/renderer/DeckSceneRenderer.d.ts +26 -0
- package/dist/src/systems/renderer/FlowFieldLayer.d.ts +68 -0
- package/dist/src/systems/renderer/LayerRendererRegistry.d.ts +132 -0
- package/dist/src/systems/renderer/authenticated-deck-load.d.ts +4 -0
- package/dist/src/systems/renderer/deck/deck-layer-builders.d.ts +29 -0
- package/dist/src/systems/renderer/deck/deck-layer-helpers.d.ts +21 -0
- package/dist/src/systems/renderer/deck/mvt-point-aggregation-layer.d.ts +107 -0
- package/dist/src/systems/renderer/flow-field/flow-field-resources.d.ts +84 -0
- package/dist/src/systems/renderer/flow-field/flow-field-shaders.d.ts +4 -0
- package/dist/src/systems/renderer/symbol/SpriteRegistry.d.ts +34 -0
- package/dist/src/systems/renderer/symbol/entries.d.ts +67 -0
- package/dist/src/types/analysis.d.ts +205 -0
- package/dist/src/types/common.d.ts +17 -0
- package/dist/src/types/extensions.d.ts +560 -0
- package/dist/src/types/gis.d.ts +242 -0
- package/dist/src/types/market-social.d.ts +378 -0
- package/package.json +47 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { NgisHostAPI } from "./host-api";
|
|
2
|
+
import type { NgisMapAPI } from "./map-api";
|
|
3
|
+
import type { NgisContributionCommandTarget } from "./contributions/dispatch";
|
|
4
|
+
/** Context handed to a command's `invoke` — the full map facade, same object `NgisPluginHost.api`
|
|
5
|
+
* carries into `activate(host)`. Grows on demand, mirroring `NgisPluginHost`. */
|
|
6
|
+
export interface CommandInvokeContext {
|
|
7
|
+
api: NgisMapAPI;
|
|
8
|
+
/**
|
|
9
|
+
* What the contribution was invoked **over** — the layer or scene resource a
|
|
10
|
+
* `layer/context` / `resource/context` menu was opened on (Stage 13 X9c,
|
|
11
|
+
* `V-120`).
|
|
12
|
+
*
|
|
13
|
+
* `undefined` for a `statusItems` click, a `tools` launch, and every codeenv
|
|
14
|
+
* `scene.plugin(extId).invoke(...)` — none of those has a target, and a
|
|
15
|
+
* command must be able to tell "no target" from "a target I did not read".
|
|
16
|
+
*
|
|
17
|
+
* **Why the ctx and not `args`.** `args` is the *caller's* data channel: a
|
|
18
|
+
* scene script's mailbox row already puts arbitrary values there, and a
|
|
19
|
+
* command invocable from both codeenv and a menu could not tell a host-
|
|
20
|
+
* supplied target from its own arguments. The ctx is the *host's* channel —
|
|
21
|
+
* it already carries `api` — so adding a member here cannot change the shape
|
|
22
|
+
* any existing command receives, while merging into `args` would.
|
|
23
|
+
*/
|
|
24
|
+
target?: NgisContributionCommandTarget;
|
|
25
|
+
/**
|
|
26
|
+
* The **command-owning** plugin's own 0.4 facade — the same scope-enforced
|
|
27
|
+
* object `activate(host)` received, resolved through the host's
|
|
28
|
+
* `createGisPluginHostFactory` and never assembled (`V-121`).
|
|
29
|
+
*
|
|
30
|
+
* Owner, not caller: derived from `commandPluginId(id)`, because v1 allows
|
|
31
|
+
* one plugin's contribution to invoke another's command and the ctx belongs
|
|
32
|
+
* to the code that runs.
|
|
33
|
+
*
|
|
34
|
+
* `undefined` when the host cannot resolve an owning plugin for the id — a
|
|
35
|
+
* host that installed only `api`, or an id whose namespace matches no
|
|
36
|
+
* registered plugin. Optional, so every existing `{ api }` ctx still
|
|
37
|
+
* type-checks and a 0.3-era command is unaffected.
|
|
38
|
+
*/
|
|
39
|
+
host?: NgisHostAPI;
|
|
40
|
+
}
|
|
41
|
+
/** A plugin-declared, externally-invocable command (§19). `invoke` may be sync or async; its return
|
|
42
|
+
* value (or thrown error) is exactly what the codeenv plugin-invocation executor reports back to the
|
|
43
|
+
* scene script as the mailbox's `DONE` result (or `FAILED` error, verbatim). */
|
|
44
|
+
export interface CommandRegistration {
|
|
45
|
+
/** Namespaced id — **must contain a `.`** (validated at registration), mirrors the `ext_id`/rail-tool
|
|
46
|
+
* charset convention so a command id can never collide with a first-party reserved namespace. */
|
|
47
|
+
id: string;
|
|
48
|
+
title?: string;
|
|
49
|
+
/** Declarative JSON Schema — not enforced client-side beyond basic shape; informs the package
|
|
50
|
+
* manifest's capability summary and any future invocation UI. */
|
|
51
|
+
paramsSchema?: object;
|
|
52
|
+
invoke: (args: unknown, ctx: CommandInvokeContext) => unknown | Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The declaring plugin id, derived from the command id.
|
|
56
|
+
*
|
|
57
|
+
* Command ids are namespaced ({@link GisCommandRegistry.register} throws without a `.`) and, by the
|
|
58
|
+
* same convention `NgisPluginManifest.id` follows, the namespace *is* the plugin: `ngis.workflow.run`
|
|
59
|
+
* is `ngis.workflow`'s. F13.2 wants `pluginId` on `NgisCommandSummary` so "a plugin can see that a
|
|
60
|
+
* command is not its own"; deriving it from the id keeps that true without adding a field to the
|
|
61
|
+
* frozen `CommandRegistration` shape (E7's, not Stage 13's).
|
|
62
|
+
*
|
|
63
|
+
* Lives here rather than in `facets/commands.ts` (X4b's original home) because X8 made it the
|
|
64
|
+
* derivation *two* layers depend on — the summary's `pluginId` and the ctx's `host` owner — and the
|
|
65
|
+
* id convention it reads is this module's. `facets/commands.ts` re-exports it, so its published
|
|
66
|
+
* spelling is unchanged.
|
|
67
|
+
*/
|
|
68
|
+
export declare function commandPluginId(commandId: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Resolves one plugin's scope-enforced 0.4 facade, or `null` when the host knows no such plugin.
|
|
71
|
+
*
|
|
72
|
+
* `null` rather than a minted facade is the whole contract (`V-121`): `createGisPluginHostFactory`
|
|
73
|
+
* is a memoizing factory that will happily build a host for an id nobody registered — one with no
|
|
74
|
+
* install row and, under F13.12, no grants — which would refuse every call for a reason the author
|
|
75
|
+
* cannot see. A resolver must answer from the set of plugins the host actually knows.
|
|
76
|
+
*/
|
|
77
|
+
export type CommandHostResolver = (pluginId: string) => NgisHostAPI | null;
|
|
78
|
+
/**
|
|
79
|
+
* The ctx one invocation receives, with the owning plugin's facade attached when there is one
|
|
80
|
+
* (`V-121`).
|
|
81
|
+
*
|
|
82
|
+
* Every invoke site builds its ctx through here so the owner derivation happens once: X9c's `V-120`
|
|
83
|
+
* had to be fixed twice because two sites built the same object independently.
|
|
84
|
+
*
|
|
85
|
+
* `host` is spread conditionally, exactly as `target` is: an unresolvable owner leaves **no `host`
|
|
86
|
+
* key at all**, so "this host installed no 0.4 facade" stays distinguishable from `host: undefined`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildCommandInvokeContext(commandId: string, base: CommandInvokeContext, resolveHost?: CommandHostResolver | null): CommandInvokeContext;
|
|
89
|
+
type Listener = () => void;
|
|
90
|
+
type Disposer = () => void;
|
|
91
|
+
/**
|
|
92
|
+
* Registry instance. A process-wide singleton (`gisCommandRegistry`) is what the plugin runtime and
|
|
93
|
+
* the codeenv plugin-invocation executor use; the factory is exported so tests build isolated
|
|
94
|
+
* instances (mirrors `createGisRailToolRegistry`).
|
|
95
|
+
*/
|
|
96
|
+
export interface GisCommandRegistry {
|
|
97
|
+
/** Register a command. Throws if `def.id` does not contain a `.`. Returns an unregister function
|
|
98
|
+
* (idempotent; never removes a different def re-registered under the same id). */
|
|
99
|
+
register(def: CommandRegistration): Disposer;
|
|
100
|
+
/** Registered commands in registration order — cached snapshot between mutations (safe as a
|
|
101
|
+
* `useSyncExternalStore` snapshot). */
|
|
102
|
+
list(): readonly CommandRegistration[];
|
|
103
|
+
/** Look a command up by id — the executor's primary entry point. */
|
|
104
|
+
get(id: string): CommandRegistration | null;
|
|
105
|
+
subscribe(listener: Listener): Disposer;
|
|
106
|
+
}
|
|
107
|
+
export declare function createGisCommandRegistry(): GisCommandRegistry;
|
|
108
|
+
/** Process-wide dynamic command registry for the `/gis` workspace. */
|
|
109
|
+
export declare const gisCommandRegistry: GisCommandRegistry;
|
|
110
|
+
/** Reactive list of registered commands — for a future invocation-management/debug surface, mirroring
|
|
111
|
+
* `useDynamicRailTools`. */
|
|
112
|
+
export declare function useGisCommands(registry?: GisCommandRegistry): readonly CommandRegistration[];
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-key vocabulary and value semantics (Stage 13, F13.6 / XD8;
|
|
3
|
+
* Stage 14, F14.11).
|
|
4
|
+
*
|
|
5
|
+
* The context store is the host's published view of "what is true right now",
|
|
6
|
+
* addressed by dotted keys — except for the two plugin-scoped namespaces, which
|
|
7
|
+
* F14.11 re-delimits with `:` (see `toPluginContextKey`). Contributions gate on
|
|
8
|
+
* it through `when` / `enabledWhen` expressions; a plugin reads it through
|
|
9
|
+
* `host.context`.
|
|
10
|
+
*
|
|
11
|
+
* Two rules bind everything in this folder:
|
|
12
|
+
*
|
|
13
|
+
* 1. **A key a plugin references but the host does not publish evaluates to
|
|
14
|
+
* `undefined` — and therefore falsy — it never throws.** Contributions come
|
|
15
|
+
* from third-party manifests; a typo in a `when` clause must degrade to
|
|
16
|
+
* "not shown", not to a broken workspace.
|
|
17
|
+
* 2. **Values are JSON-serializable scalars or flat arrays of scalars**
|
|
18
|
+
* (`NgisContextValue`). The whole 0.4 surface is worker-ready (F13.1 L6);
|
|
19
|
+
* a context value must survive `structuredClone`/RPC unchanged.
|
|
20
|
+
*/
|
|
21
|
+
/** F13.2, verbatim. */
|
|
22
|
+
export type NgisContextValue = string | number | boolean | null | readonly (string | number)[];
|
|
23
|
+
/**
|
|
24
|
+
* The guaranteed key set (F13.6). X1 publishes exactly these; everything else
|
|
25
|
+
* is either a `config:<pluginId>:<key>` / `plugin:<pluginId>:<key>` namespace
|
|
26
|
+
* entry (F14.11) or absent.
|
|
27
|
+
*
|
|
28
|
+
* Every guaranteed key is **seeded with a default** at publisher start
|
|
29
|
+
* (`seedGuaranteedContextKeys`) so that "the scene has no layers" and "the
|
|
30
|
+
* context store has not booted yet" are distinguishable: the former publishes
|
|
31
|
+
* `0`, the latter leaves the key absent.
|
|
32
|
+
*/
|
|
33
|
+
export declare const GUARANTEED_CONTEXT_KEYS: readonly ["scene.id", "scene.type", "scene.isOwner", "scene.resourceCount", "scene.layerCount", "selection.layerId", "selection.featureCount", "draw.mode", "resources.types", "resources.count", "tasks.activeCount", "tasks.hasFailed", "user.role"];
|
|
34
|
+
export type GuaranteedContextKey = (typeof GUARANTEED_CONTEXT_KEYS)[number];
|
|
35
|
+
/**
|
|
36
|
+
* Namespaces a plugin may not write into via `host.context.set`.
|
|
37
|
+
*
|
|
38
|
+
* `plugin` and `config` are listed too: `set` auto-namespaces to
|
|
39
|
+
* `plugin:<pluginId>:<key>`, so a caller passing `plugin.foo.bar` is either
|
|
40
|
+
* confused about the contract or trying to write into another plugin's
|
|
41
|
+
* namespace. F13.2 requires that to be **rejected, not silently re-prefixed**.
|
|
42
|
+
*
|
|
43
|
+
* `target` is reserved by F14.7 even though nothing populates it yet: it is a
|
|
44
|
+
* *per-invocation overlay*, answered by a reader wrapper rather than written to
|
|
45
|
+
* the store, so a plugin that grabbed the head first would shadow a namespace
|
|
46
|
+
* the host is about to own.
|
|
47
|
+
*/
|
|
48
|
+
export declare const RESERVED_CONTEXT_NAMESPACES: readonly ["scene", "selection", "draw", "resources", "tasks", "user", "target", "config", "plugin"];
|
|
49
|
+
/**
|
|
50
|
+
* The four `NgisResourceType` values (F13.2). **`"table"` is never emitted in
|
|
51
|
+
* v1** (XD7) — see `classifySceneResourceType`.
|
|
52
|
+
*
|
|
53
|
+
* X1 and X2 both needed this union and independently declared identical copies;
|
|
54
|
+
* F13.2 places it in the resources facet, so `facets/types.ts` is the one source
|
|
55
|
+
* of truth and this is a type-only re-export. Resolved at the W1 merge.
|
|
56
|
+
*/
|
|
57
|
+
export type { NgisResourceType } from "../facets/types";
|
|
58
|
+
/** The three values `resources.types` may contain in v1 (XD7). */
|
|
59
|
+
export declare const V1_EMITTED_RESOURCE_TYPES: readonly ["vector", "raster", "file"];
|
|
60
|
+
/**
|
|
61
|
+
* Thrown by `host.context.set` for a key that is empty, malformed, or carries
|
|
62
|
+
* a reserved namespace prefix.
|
|
63
|
+
*
|
|
64
|
+
* Typed rather than a bare `Error` because the caller is plugin code: it must
|
|
65
|
+
* be able to tell "you may not write there" from a transport failure.
|
|
66
|
+
*/
|
|
67
|
+
export declare class NgisContextKeyError extends Error {
|
|
68
|
+
readonly code: "invalid-key";
|
|
69
|
+
readonly key: string;
|
|
70
|
+
constructor(key: string, message: string);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `true` when `key` is a well-formed dotted key — i.e. when it is spellable in
|
|
74
|
+
* a when-expression. `-` is a legal ident character because extension ids
|
|
75
|
+
* routinely contain one (`ngis.sample-coordinate-pin`), and the grammar has no
|
|
76
|
+
* subtraction operator for it to collide with.
|
|
77
|
+
*
|
|
78
|
+
* **This is deliberately colon-free, and it diverges from the `when` grammar's
|
|
79
|
+
* `key` production as of F14.11.** What it validates is the **bare** key a
|
|
80
|
+
* plugin passes to `host.context.set` — the host adds the namespace and the
|
|
81
|
+
* delimiter, so a bare key containing `:` is exactly the mistake
|
|
82
|
+
* `toPluginContextKey` must reject. The tokenizer, by contrast, has to *read*
|
|
83
|
+
* the namespaced form. Two different jobs, two different productions.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isValidContextKey(key: string): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* `true` when `key` can be namespaced into a **plugin**-scoped context key —
|
|
88
|
+
* i.e. the non-throwing form of `toPluginContextKey`'s check, reserved head
|
|
89
|
+
* included.
|
|
90
|
+
*
|
|
91
|
+
* It is **not** `toConfigContextKey`'s check any more: F15.14/`V-241` dropped
|
|
92
|
+
* the reserved-head rejection there, so the config publisher asks
|
|
93
|
+
* `isValidContextKey` instead. Two different jobs — this one guards the key a
|
|
94
|
+
* plugin hands to `host.context.set`, where a reserved head means the author
|
|
95
|
+
* believes they can write host state.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isScopableContextKey(key: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Validates a plugin-supplied key and returns its canonical namespaced form
|
|
100
|
+
* `plugin:<pluginId>:<key>` (**F14.11**).
|
|
101
|
+
*
|
|
102
|
+
* The delimiter is `:` rather than `.` because the dotted join was not uniquely
|
|
103
|
+
* decodable — plugin `a.b` + key `c.d` and plugin `a.b.c` + key `d` both spell
|
|
104
|
+
* `plugin.a.b.c.d` (`X9-I-5`). `:` occurs in neither an extension id
|
|
105
|
+
* (`^[a-z0-9-]+(\.[a-z0-9-]+)+$`) nor a key segment, so splitting on the first
|
|
106
|
+
* two `:` yields exactly three fields and the key may still contain dots.
|
|
107
|
+
*
|
|
108
|
+
* @throws {NgisContextKeyError} on an empty/malformed key or one that already
|
|
109
|
+
* carries a reserved namespace prefix.
|
|
110
|
+
*/
|
|
111
|
+
export declare function toPluginContextKey(pluginId: string, key: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* The canonical `config:<pluginId>:<key>` spelling (fed by `host.storage`,
|
|
114
|
+
* W2/X3).
|
|
115
|
+
*
|
|
116
|
+
* **Validates shape, and — as of F15.14 — nothing else (`V-241`).** F14.11 gave
|
|
117
|
+
* it `toPluginContextKey`'s whole check, reserved head included, so that the two
|
|
118
|
+
* spellings of the dual-write could not disagree about what they accepted. With
|
|
119
|
+
* the dotted spelling gone, that reason is gone: `config:a.b:scene` collides
|
|
120
|
+
* with `scene.id`, with `config:a.b.scene:…` and with nothing else, because the
|
|
121
|
+
* namespace and the delimiter are both fixed by the host. A settings property
|
|
122
|
+
* named `scene` is an author's word for their own setting, not an attempt to
|
|
123
|
+
* write host state — the sibling's check exists to catch *that*, and this
|
|
124
|
+
* function is never reached from `host.context.set`.
|
|
125
|
+
*
|
|
126
|
+
* @throws {NgisContextKeyError} on an empty or malformed key.
|
|
127
|
+
*/
|
|
128
|
+
export declare function toConfigContextKey(pluginId: string, key: string): string;
|
|
129
|
+
/**
|
|
130
|
+
* Value equality for the store's change guard and for the `==` operator.
|
|
131
|
+
*
|
|
132
|
+
* - Arrays compare element-wise (a re-derived `resources.types` array is a new
|
|
133
|
+
* reference on every store update but usually the same content — without
|
|
134
|
+
* this guard every unrelated scene-store write would wake every subscriber).
|
|
135
|
+
* - `undefined` and `null` are the same value. An absent key is "nothing", and
|
|
136
|
+
* `someKey == null` reading `true` for an unpublished key is the behaviour an
|
|
137
|
+
* expression author expects.
|
|
138
|
+
*/
|
|
139
|
+
export declare function contextValueEquals(a: NgisContextValue | undefined, b: NgisContextValue | undefined): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Truthiness for a bare-key test (`when: "scene.isOwner"`).
|
|
142
|
+
*
|
|
143
|
+
* Deliberately **not** JavaScript truthiness for arrays: `[]` is truthy in JS,
|
|
144
|
+
* but `when: "resources.types"` plainly means "the scene has some resources".
|
|
145
|
+
* An empty array is falsy here, and that is a contract of the grammar rather
|
|
146
|
+
* than an implementation detail.
|
|
147
|
+
*/
|
|
148
|
+
export declare function isContextValueTruthy(value: NgisContextValue | undefined): boolean;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guaranteed-key publishers (Stage 13, F13.6).
|
|
3
|
+
*
|
|
4
|
+
* Every publisher here is **dependency-injected and store-agnostic**: it takes
|
|
5
|
+
* a `{ getState, subscribe }` handle (the shape a Zustand store already has)
|
|
6
|
+
* or a facet-shaped observer, never an import of `@/store/*`. Two reasons:
|
|
7
|
+
*
|
|
8
|
+
* - `src/sdk/**` does not depend on `src/store/**` anywhere else — the C8 map
|
|
9
|
+
* facade takes injected bindings from `GisWorkspace` for exactly this
|
|
10
|
+
* reason, and the context store is the same kind of seam.
|
|
11
|
+
* - It is what makes "key-publisher tests against fake stores" a two-line
|
|
12
|
+
* fixture instead of a Zustand harness.
|
|
13
|
+
*
|
|
14
|
+
* Subscriptions use the **equality-guarded pattern** at
|
|
15
|
+
* `GisWorkspace.tsx:164-167`: compare the specific slice references between
|
|
16
|
+
* `state` and `prevState` and do nothing when they are identical, so an
|
|
17
|
+
* unrelated store write (a loading flag, a style draft) costs one reference
|
|
18
|
+
* compare rather than a re-derivation and a notification.
|
|
19
|
+
*/
|
|
20
|
+
import type { NgisUnsubscribe } from "../map-api";
|
|
21
|
+
import { type NgisContextValue, type NgisResourceType } from "./keys";
|
|
22
|
+
import type { NgisContextStore } from "./store";
|
|
23
|
+
/** The subset of a Zustand store a publisher uses. */
|
|
24
|
+
export interface ContextStoreSource<T> {
|
|
25
|
+
getState(): T;
|
|
26
|
+
subscribe(listener: (state: T, prevState: T) => void): () => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Writes the default for every guaranteed key.
|
|
30
|
+
*
|
|
31
|
+
* Without this, "no scene is open" and "the context store has not booted" look
|
|
32
|
+
* identical to a `when` clause, and a plugin gating on `scene.layerCount > 0`
|
|
33
|
+
* would behave differently during the first frame than after it. Publishers
|
|
34
|
+
* overwrite these immediately; the defaults are the resting state.
|
|
35
|
+
*/
|
|
36
|
+
export declare function seedGuaranteedContextKeys(store: NgisContextStore): void;
|
|
37
|
+
export interface SceneContextResource {
|
|
38
|
+
/**
|
|
39
|
+
* `SceneResourceDto.geometryType`. The scene store hydrates it from
|
|
40
|
+
* `DatasetInfo` (`scene-store.ts` `getDatasetGeometry`): the literal
|
|
41
|
+
* `"Raster"` for a RASTER dataset, the vector geometry type for a VECTOR
|
|
42
|
+
* one, and `null` for everything else — which is the signal
|
|
43
|
+
* `classifySceneResourceType` reads.
|
|
44
|
+
*/
|
|
45
|
+
geometryType?: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface SceneContextScene {
|
|
48
|
+
id: string;
|
|
49
|
+
ownerId?: string | null;
|
|
50
|
+
type?: string | null;
|
|
51
|
+
}
|
|
52
|
+
/** The slice of `createSceneStore()`'s state the scene publisher reads. */
|
|
53
|
+
export interface SceneContextSlice {
|
|
54
|
+
sceneId: string | null;
|
|
55
|
+
scene: SceneContextScene | null;
|
|
56
|
+
resources: readonly SceneContextResource[];
|
|
57
|
+
layers: readonly unknown[];
|
|
58
|
+
selectedLayerId: string | null;
|
|
59
|
+
}
|
|
60
|
+
/** The slice of the auth store the publishers read. */
|
|
61
|
+
export interface AuthContextSlice {
|
|
62
|
+
user: {
|
|
63
|
+
id?: string;
|
|
64
|
+
role?: string;
|
|
65
|
+
} | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Scene-resource → `NgisResourceType` (XD7).
|
|
69
|
+
*
|
|
70
|
+
* **Never returns `"table"`.** The reserved fourth value needs
|
|
71
|
+
* `NgisFile.fileType`, i.e. a `GET /api/files/{fileId}` per resource, and no
|
|
72
|
+
* v1 dogfood plugin needs the distinction. `'table' in resources.types` is on
|
|
73
|
+
* the W3 grep-proof list precisely so nobody writes a plugin against a value
|
|
74
|
+
* this function cannot produce.
|
|
75
|
+
*
|
|
76
|
+
* Classification is a projection of `DatasetInfo.datasetType` as the scene
|
|
77
|
+
* store already recorded it: `RASTER` → `"raster"` (the store writes the
|
|
78
|
+
* literal `"Raster"` as the geometry type), any other non-empty geometry type
|
|
79
|
+
* → `"vector"`, absent → `"file"` (which covers `GENERIC` and a dataset whose
|
|
80
|
+
* metadata fetch failed). The authoritative join lives in X2's
|
|
81
|
+
* `host.resources` facet; wiring this publisher to it is backlog row **V-21**.
|
|
82
|
+
*/
|
|
83
|
+
export declare function classifySceneResourceType(resource: SceneContextResource): NgisResourceType;
|
|
84
|
+
/**
|
|
85
|
+
* Distinct resource types, in the fixed `vector → raster → file` order.
|
|
86
|
+
*
|
|
87
|
+
* Deterministic ordering is not cosmetic: the store's equality guard compares
|
|
88
|
+
* arrays element-wise, so an unstable order would report a change on every
|
|
89
|
+
* scene-store write.
|
|
90
|
+
*/
|
|
91
|
+
export declare function deriveResourceTypes(resources: readonly SceneContextResource[], classify?: (resource: SceneContextResource) => NgisResourceType): string[];
|
|
92
|
+
export interface SceneContextPublisherOptions {
|
|
93
|
+
/**
|
|
94
|
+
* Override the resource classifier. The seam X2/X3 use to swap in the
|
|
95
|
+
* dataset-backed join without editing this file.
|
|
96
|
+
*/
|
|
97
|
+
classifyResource?: (resource: SceneContextResource) => NgisResourceType;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Publishes `scene.*`, `selection.layerId` and `resources.*`.
|
|
101
|
+
*
|
|
102
|
+
* `scene.isOwner` needs both stores, so the auth handle is optional but
|
|
103
|
+
* strongly recommended: without it ownership is unknowable and the key stays
|
|
104
|
+
* `false`, which is the safe reading (a read-only viewer).
|
|
105
|
+
*
|
|
106
|
+
* Note that `selection.layerId` is the **layer-list** selection
|
|
107
|
+
* (`selectedLayerId`), while `selection.featureCount` comes from the feature
|
|
108
|
+
* highlight (`publishHighlightContextKeys`). Those are two distinct host
|
|
109
|
+
* selection surfaces today — `NgisSelectionFacet` is highlight-only since
|
|
110
|
+
* 0.2.0 (decision D10) — and F13.6 does not require them to be the same
|
|
111
|
+
* selection. Recorded as a decision rather than papered over.
|
|
112
|
+
*/
|
|
113
|
+
export declare function publishSceneContextKeys(store: NgisContextStore, scene: ContextStoreSource<SceneContextSlice>, auth?: ContextStoreSource<AuthContextSlice>, options?: SceneContextPublisherOptions): NgisUnsubscribe;
|
|
114
|
+
export interface TaskContextTask {
|
|
115
|
+
status: string;
|
|
116
|
+
}
|
|
117
|
+
export interface TaskContextSlice {
|
|
118
|
+
tasks: readonly TaskContextTask[];
|
|
119
|
+
}
|
|
120
|
+
/** Publishes `tasks.activeCount` and `tasks.hasFailed`. */
|
|
121
|
+
export declare function publishTaskContextKeys(store: NgisContextStore, tasks: ContextStoreSource<TaskContextSlice>): NgisUnsubscribe;
|
|
122
|
+
/**
|
|
123
|
+
* `user.role` is **presentational only and never an authorization decision.**
|
|
124
|
+
*
|
|
125
|
+
* The server stays authoritative for every capability: the market's ADMIN
|
|
126
|
+
* surface is established by calling the review queue and handling `403`, not
|
|
127
|
+
* by reading a role off the client. This key exists so a contribution can say
|
|
128
|
+
* "don't show this to a non-admin", which is a UI affordance, not a gate. A
|
|
129
|
+
* plugin that treats it as one is wrong, and the value is trivially forgeable
|
|
130
|
+
* from the console anyway (F13.6, ground-truth correction 7).
|
|
131
|
+
*/
|
|
132
|
+
export declare function publishUserContextKeys(store: NgisContextStore, auth: ContextStoreSource<AuthContextSlice>): NgisUnsubscribe;
|
|
133
|
+
/** The `NgisDrawFacet` members the publisher needs (`host.api.draw`). */
|
|
134
|
+
export interface DrawContextSource {
|
|
135
|
+
getActiveMode(): string | null;
|
|
136
|
+
onDrawChange(listener: () => void): NgisUnsubscribe;
|
|
137
|
+
}
|
|
138
|
+
/** Publishes `draw.mode`. */
|
|
139
|
+
export declare function publishDrawContextKeys(store: NgisContextStore, draw: DrawContextSource): NgisUnsubscribe;
|
|
140
|
+
/** The `NgisHighlightFacet` members the publisher needs (`host.api.selection.highlight`). */
|
|
141
|
+
export interface HighlightContextSource {
|
|
142
|
+
get(): {
|
|
143
|
+
featureIds?: readonly string[];
|
|
144
|
+
} | null;
|
|
145
|
+
onChange(listener: (highlight: unknown) => void): NgisUnsubscribe;
|
|
146
|
+
}
|
|
147
|
+
/** Publishes `selection.featureCount` from the enumerated feature highlight. */
|
|
148
|
+
export declare function publishHighlightContextKeys(store: NgisContextStore, highlight: HighlightContextSource): NgisUnsubscribe;
|
|
149
|
+
/**
|
|
150
|
+
* The `config:<pluginId>:<key>` key source — **the W2 seam, frozen now**.
|
|
151
|
+
*
|
|
152
|
+
* F13.6 assigns `config:*` to `host.storage`, which is X3's in W2. X1 ships
|
|
153
|
+
* the interface and an empty implementation so that contributions can already
|
|
154
|
+
* reference `config:*` keys (they evaluate falsy, as an unpublished key must)
|
|
155
|
+
* and X3 wires a real source without touching this file or the store.
|
|
156
|
+
*/
|
|
157
|
+
export interface NgisContextConfigSource {
|
|
158
|
+
/** `pluginId` → flat map of that plugin's scalar settings. */
|
|
159
|
+
read(): Readonly<Record<string, Readonly<Record<string, NgisContextValue>>>>;
|
|
160
|
+
subscribe(listener: () => void): NgisUnsubscribe;
|
|
161
|
+
}
|
|
162
|
+
/** The stub source: no config keys, no changes. Replaced by X3 in W2. */
|
|
163
|
+
export declare const emptyContextConfigSource: NgisContextConfigSource;
|
|
164
|
+
/**
|
|
165
|
+
* Publishes `config:<pluginId>:<key>`, removing keys the source drops.
|
|
166
|
+
*
|
|
167
|
+
* **A settings key that cannot be namespaced is skipped, not thrown on.** The
|
|
168
|
+
* keys here are the top-level members of a plugin's stored settings JSON —
|
|
169
|
+
* arbitrary author-chosen text, reaching this loop through
|
|
170
|
+
* `flattenConfigValues` with no ident check. `{"my key": 1}` would make
|
|
171
|
+
* `toConfigContextKey` throw *inside* the loop and kill `config:*` publication
|
|
172
|
+
* for **every** plugin, not just the one with the odd blob. Skipping is also
|
|
173
|
+
* what this path already does one level down: `flattenConfigValues` drops a
|
|
174
|
+
* value `NgisContextValue` cannot hold, and an absent key is falsy by design
|
|
175
|
+
* (F13.6).
|
|
176
|
+
*
|
|
177
|
+
* The guard is `isValidContextKey`, not `isScopableContextKey`: F15.14/`V-241`
|
|
178
|
+
* relaxed `toConfigContextKey`'s reserved-head rejection, so `{"scene": "x"}`
|
|
179
|
+
* now publishes as `config:<pluginId>:scene` instead of being dropped. It is a
|
|
180
|
+
* setting called `scene`, and the fixed `config:` namespace means it can reach
|
|
181
|
+
* nothing else.
|
|
182
|
+
*/
|
|
183
|
+
export declare function publishConfigContextKeys(store: NgisContextStore, source: NgisContextConfigSource): NgisUnsubscribe;
|
|
184
|
+
export interface GuaranteedContextPublisherDeps {
|
|
185
|
+
scene?: ContextStoreSource<SceneContextSlice>;
|
|
186
|
+
auth?: ContextStoreSource<AuthContextSlice>;
|
|
187
|
+
tasks?: ContextStoreSource<TaskContextSlice>;
|
|
188
|
+
draw?: DrawContextSource;
|
|
189
|
+
highlight?: HighlightContextSource;
|
|
190
|
+
config?: NgisContextConfigSource;
|
|
191
|
+
classifyResource?: (resource: SceneContextResource) => NgisResourceType;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Seeds every guaranteed key and starts whichever publishers the host can
|
|
195
|
+
* supply sources for. Missing sources leave their keys at the seeded default —
|
|
196
|
+
* an absent publisher must never leave a guaranteed key undefined.
|
|
197
|
+
*/
|
|
198
|
+
export declare function startGuaranteedContextPublishers(store: NgisContextStore, deps: GuaranteedContextPublisherDeps): NgisUnsubscribe;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context-key store (Stage 13, F13.6 / XD8).
|
|
3
|
+
*
|
|
4
|
+
* Host-published keys in, gated contributions out. Three properties are
|
|
5
|
+
* contractual, not incidental:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Compile once.** An expression is parsed the first time it is seen and
|
|
8
|
+
* cached by its source text — including a *failed* parse, so a malformed
|
|
9
|
+
* `when` in a manifest costs one tokenizer run for the life of the session,
|
|
10
|
+
* not one per key change. `getCompileCount()` exists so a test can observe
|
|
11
|
+
* that rather than a comment asserting it.
|
|
12
|
+
* 2. **Batched per microtask.** Publishers write many keys per store update
|
|
13
|
+
* (a scene load moves `scene.id`, `scene.type`, `scene.layerCount`,
|
|
14
|
+
* `resources.*` at once). Writes accumulate and flush on one microtask, so
|
|
15
|
+
* N key changes cost exactly one notification and one re-evaluation.
|
|
16
|
+
* 3. **Dependency-indexed.** A watched expression re-evaluates only when a key
|
|
17
|
+
* in its own read-set changed. With dozens of contributions from a handful
|
|
18
|
+
* of plugins, the alternative — re-run everything on every change — is what
|
|
19
|
+
* makes declarative gating too slow to be the default.
|
|
20
|
+
*
|
|
21
|
+
* Writes are equality-guarded (`contextValueEquals`): a publisher re-deriving
|
|
22
|
+
* `resources.types` into a fresh array with identical content is not a change,
|
|
23
|
+
* and never wakes a subscriber.
|
|
24
|
+
*/
|
|
25
|
+
import type { NgisUnsubscribe } from "../map-api";
|
|
26
|
+
import { type NgisContextValue } from "./keys";
|
|
27
|
+
import { type CompiledWhenExpression } from "./when";
|
|
28
|
+
/** The changed-key payload. A key that was removed reads back as `null`. */
|
|
29
|
+
export type ContextChangeListener = (changed: Readonly<Record<string, NgisContextValue>>) => void;
|
|
30
|
+
export interface NgisContextStore {
|
|
31
|
+
get(key: string): NgisContextValue | undefined;
|
|
32
|
+
/** Frozen snapshot; the same reference until the next write (`useSyncExternalStore`-safe). */
|
|
33
|
+
getAll(): Readonly<Record<string, NgisContextValue>>;
|
|
34
|
+
/** `undefined` removes the key. Equality-guarded; batched to a microtask. */
|
|
35
|
+
set(key: string, value: NgisContextValue | undefined): void;
|
|
36
|
+
setMany(entries: Readonly<Record<string, NgisContextValue | undefined>>): void;
|
|
37
|
+
/**
|
|
38
|
+
* Fires when any of `keys` changes. `null` subscribes to every key — the
|
|
39
|
+
* dev inspector's mode, not something a contribution should use.
|
|
40
|
+
*/
|
|
41
|
+
subscribeKeys(keys: readonly string[] | null, listener: ContextChangeListener): NgisUnsubscribe;
|
|
42
|
+
/** Compiles (or returns the cached compilation of) an expression. */
|
|
43
|
+
compile(expression: string): CompiledWhenExpression;
|
|
44
|
+
/** Compile-and-evaluate. Throws `NgisWhenSyntaxError` on malformed input. */
|
|
45
|
+
evaluate(expression: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Re-evaluates `expression` whenever a key in its read-set changes, calling
|
|
48
|
+
* `listener` only when the boolean result actually flips. Does **not** fire
|
|
49
|
+
* on subscribe — call `evaluate` for the initial value.
|
|
50
|
+
*/
|
|
51
|
+
watch(expression: string, listener: (value: boolean) => void): NgisUnsubscribe;
|
|
52
|
+
/** Test seam: how many times an expression has been parsed. */
|
|
53
|
+
getCompileCount(): number;
|
|
54
|
+
}
|
|
55
|
+
export interface NgisContextStoreOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Compiler override. Production uses `compileWhenExpression`; tests wrap it
|
|
58
|
+
* to count parses and evaluations, which is how "compile once" and "one
|
|
59
|
+
* re-evaluation per microtask" are *observed* rather than asserted.
|
|
60
|
+
*/
|
|
61
|
+
compile?: (expression: string) => CompiledWhenExpression;
|
|
62
|
+
/**
|
|
63
|
+
* Flush scheduler. Defaults to `queueMicrotask`. A test may pass a manual
|
|
64
|
+
* scheduler to drive flushes deterministically.
|
|
65
|
+
*/
|
|
66
|
+
schedule?: (flush: () => void) => void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createNgisContextStore(options?: NgisContextStoreOptions): NgisContextStore;
|
|
69
|
+
/**
|
|
70
|
+
* The host-wide context store.
|
|
71
|
+
*
|
|
72
|
+
* A module-level singleton for the same reason `workspacePanelRegistry` and
|
|
73
|
+
* `gisCommandRegistry` are: there is exactly one host, plugin facades are
|
|
74
|
+
* created per plugin but must all see the same world, and the dev inspector
|
|
75
|
+
* needs to reach it without a prop chain. Tests use `createNgisContextStore()`
|
|
76
|
+
* for isolation.
|
|
77
|
+
*/
|
|
78
|
+
export declare const ngisContextStore: NgisContextStore;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When-grammar v1 — lexer, recursive-descent parser, and compiler (F13.6 / XD8).
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* expr := or
|
|
6
|
+
* or := and ( "||" and )*
|
|
7
|
+
* and := cmp ( "&&" cmp )*
|
|
8
|
+
* cmp := unary ( ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" ) unary )?
|
|
9
|
+
* unary := "!" unary | primary
|
|
10
|
+
* primary := "(" expr ")" | literal | key
|
|
11
|
+
* literal := 'single-quoted string' | number | true | false | null
|
|
12
|
+
* key := ident ( "." ident )* // bare key => truthiness test
|
|
13
|
+
* | ( "plugin" | "config" ) ":" ident ( "." ident )* ":" ident ( "." ident )*
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The second `key` alternative is **F14.11** and is additive: `:` was previously
|
|
17
|
+
* a lex error, so no existing valid expression changes meaning. It is positional
|
|
18
|
+
* and not a charset widening — `:` is legal at exactly two offsets of a key
|
|
19
|
+
* whose head is the bare ident `plugin` or `config`, which is what keeps `a:b`,
|
|
20
|
+
* `plugin:a`, `plugin:a:b:c` and `scene:id` lex errors. Implementing the clause
|
|
21
|
+
* as "add `:` to `isIdentPart`" would admit all four.
|
|
22
|
+
*
|
|
23
|
+
* Note the deliberate divergence from `isValidContextKey` (`keys.ts`): that
|
|
24
|
+
* function validates the **bare** key a plugin hands to `host.context.set`,
|
|
25
|
+
* which must stay colon-free because the host supplies the delimiter. This
|
|
26
|
+
* production reads the namespaced result.
|
|
27
|
+
*
|
|
28
|
+
* XD8 **amended** design-v2's asymmetric `&& || ! == != < >= in` set to add
|
|
29
|
+
* `>` and `<=`: an author writing `x <= 3` and getting a parse error for no
|
|
30
|
+
* reason is a footgun, and symmetry costs two parser cases.
|
|
31
|
+
*
|
|
32
|
+
* `cmp` takes **at most one** comparison — `a == b == c` is a syntax error, not
|
|
33
|
+
* a left-associative chain. That is the grammar as frozen, and XB1's validator
|
|
34
|
+
* plus X4's parser must round-trip the same text, so it is not negotiable here.
|
|
35
|
+
*
|
|
36
|
+
* **Compile once.** `compileWhenExpression` returns a closure plus the set of
|
|
37
|
+
* keys it reads; the store caches by expression text and never re-parses. The
|
|
38
|
+
* read-set is what makes dependency-indexed batching possible: an expression
|
|
39
|
+
* re-evaluates only when a key it actually mentions changes.
|
|
40
|
+
*/
|
|
41
|
+
import { type NgisContextValue } from "./keys";
|
|
42
|
+
/** How a compiled expression reads the world. Missing key => `undefined`. */
|
|
43
|
+
export type ContextReader = (key: string) => NgisContextValue | undefined;
|
|
44
|
+
export interface CompiledWhenExpression {
|
|
45
|
+
/** The source text, verbatim — the cache key and the error-message subject. */
|
|
46
|
+
readonly expression: string;
|
|
47
|
+
/** Every key the expression reads, de-duplicated, in first-seen order. */
|
|
48
|
+
readonly keys: readonly string[];
|
|
49
|
+
/** Evaluates against a reader. Never throws for missing or odd-typed keys. */
|
|
50
|
+
evaluate(read: ContextReader): boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A malformed expression. Typed so a contribution registrar can refuse one
|
|
54
|
+
* manifest entry with a precise reason instead of failing the whole plugin —
|
|
55
|
+
* and so the refusal is distinguishable from a host bug.
|
|
56
|
+
*/
|
|
57
|
+
export declare class NgisWhenSyntaxError extends Error {
|
|
58
|
+
readonly code: "when-syntax";
|
|
59
|
+
readonly expression: string;
|
|
60
|
+
/** 0-based offset into `expression` where parsing stopped. */
|
|
61
|
+
readonly position: number;
|
|
62
|
+
constructor(expression: string, position: number, message: string);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Compiles a when-grammar v1 expression into a closure plus its read-set.
|
|
66
|
+
*
|
|
67
|
+
* @throws {NgisWhenSyntaxError} on malformed input.
|
|
68
|
+
*/
|
|
69
|
+
export declare function compileWhenExpression(expression: string): CompiledWhenExpression;
|