@honua/sdk-js reference guide

This long-form guide collects the runnable demos, server compatibility contract, subpath entrypoints, protocol-specific cookbooks, MapLibre runtime, request/auth bridge, migration CLI, and other reference material that used to live in the project README. Skim the README for the 60-second tour and the docs/ directory for topic-specific docs.

Contents

Getting set up

Core surfaces

Protocol cookbooks

Migration tooling

Demo apps in depth

The committed quickstart app uses the same queryFeatures() request shape shown in the README quickstart and fails fast when compatibility is unsupported, the query returns no features, or none of the returned records survive conversion into the rendered point, line, or polygon geometry buckets.

Each example README documents its own env surface, network contract, browser telemetry hooks, run lanes, accepted data contracts, live-query narrowing rules, preprocessing rules, and browser diagnostics. The shared seeded Honua Cloud demo contract lives in docs/honua-cloud-demo-services.md, with the machine-readable manifest at examples/cloud-demo-services.json and env template at examples/cloud-demo.env.example. Use npm run test:cloud-demo:config to validate the config/docs shape without live credentials, and npm run test:cloud-demo:staging for the credential-gated cloud smoke scaffold. For the Cesium spike specifically, window.__cesiumRoutePlaybackDone is the completion signal on both success and failure, window.__cesiumRoutePlaybackError is failure-only, and window.__cesiumRoutePlaybackResult is populated only on success. Its queryRequest summary echoes fixtures/source-manifest.json#query in fixture mode and the bounded live queryFeatures() request in live mode; both describe the same outFields=["*"], outSr=4326, returnGeometry=true, and extraParams={ outSr: 4326, returnZ: true } query shape, while routeId and routeIdField remain post-query selectors instead of extra server filters, routeIdField is authoritative when configured, matching normalizes numeric route ids to strings, the success summary leaves routeId as null when the selected feature has no route-id attribute, and fixture mode keeps compatibilitySupported plus requestDurationMs as null.

The kepler example README documents the fixture metadata manifest, required dataset IDs and fields, the default incident status and replay-window filters, browser readiness/error plus replay-harness hooks, refresh stdout JSON, and the runtime style override env vars used by the demo.

Deterministic local review flows from this repo:

# Node.js 20.19.0+ at the repo root
npm install

# Run either mock server in its own terminal/session.
npm run demo:quickstart:mock
# or
npm run demo:25d:mock
# or
npm run demo:geocoding:mock

The quickstart command prints quickstartMockUrl=http://127.0.0.1:PORT, the 2.5D command prints story25dMockUrl=http://127.0.0.1:PORT, and the geocoding command prints geocodingMockUrl=http://127.0.0.1:PORT.

Live Honua flow for the 2.5D demo:

cp examples/storytelling-25d-map/.env.example examples/storytelling-25d-map/.env
npm run demo:25d

Use the linked example READMEs for the 2.5D collection contract, the Cesium live-query URL parameters, custom routeIdField matching and multi-route selection rules, terrain fallback behavior, and smoke-test globals.

Server Compatibility Baseline

Use the server compatibility contract before enabling admin/control-plane flows:

import { HonuaClient } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://your-honua-server.com" });

const status = await client.checkCompatibility();
if (!status.supported) {
  throw new Error(
    `Server ${status.compatibility?.serverVersion ?? "unknown"} is unsupported. ` +
      `Minimum: ${HonuaClient.minimumSupportedServerVersion}. ` +
      `Reasons: ${status.reasons.join("; ")}`,
  );
}

if (await client.supportsFeature("manifestApply")) {
  console.log("Manifest apply workflows are available on this server.");
}

const compatibilityContract = await client.getCompatibility();
console.log(compatibilityContract.metadataSchemas);

checkCompatibility() reports support status, supportsFeature() gates coarse capabilities from data.compatibility.features, and getCompatibility() returns the parsed data.compatibility object from GET /api/v1/admin/capabilities.

The capabilities endpoint still needs to return a JSON object with success plus data.compatibility. The parsed compatibility object must include serverVersion, releaseChannel, controlPlaneApi.major/basePath/deprecated, metadataSchemas[] entries with version and deprecated, and the boolean features map. Top-level extras such as metadataApiVersions and resourceKinds may be present on the endpoint response, but they are not returned by getCompatibility().

This SDK baseline currently expects:


Initial JavaScript SDK scaffold for the JS-first migration phase (#324).

This package currently provides:

Entrypoints

Prefer subpath entrypoints to keep Honua-first and migration layers separate:

The root entrypoint (@honua/sdk-js) remains available as an aggregate export for compatibility. Surfaces documented as subpath-only, including @honua/sdk-js/generated-app, should be imported from their named subpath.

Shared Client Contract And Exploration

HonuaFeatureLayer, HonuaMapService, and HonuaOgcFeatureCollection continue to be the runtime classes. For cross-protocol code — exploration views, visual builders, and server packaging — the SDK also exposes a protocol-neutral contract and exploration state module that wrap (not replace) those classes.

import { createDataset } from "@honua/sdk-js/contract";
import { createExplorationContext } from "@honua/sdk-js/exploration";
import { HonuaClient } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://your-honua-server.example" });

const dataset = createDataset({
  id: "parcels",
  client,
  sources: [
    {
      id: "parcels-fs",
      protocol: "geoservices-feature-service",
      locator: { url: "https://your-honua-server.example", serviceId: "parcels", layerId: 0 },
      capabilities: new Set(["query", "queryExtent", "queryObjectIds"]),
    },
  ],
});

const parcels = dataset.source("parcels-fs")!;
const result = await parcels.query({ where: "STATUS = 'ACTIVE'", pagination: { limit: 100 } });
const { extent } = await parcels.queryExtent({ where: "STATUS = 'ACTIVE'" });

const ctx = createExplorationContext({
  datasetId: dataset.id,
  sourceIds: dataset.sourceIds(),
  preset: "mapDriven",
});
ctx.bind({ id: "map", role: "map" });
if (extent) ctx.dispatch({ kind: "set-extent", extent, viewId: "map" });
console.log(`Loaded ${result.features.length} features`);

Capability misses throw HonuaCapabilityNotSupportedError (under strict policy). Exploration misuses throw HonuaExplorationContextError. Both are in the HonuaError union and pass isHonuaError(e).

Standalone Data-To-Map Bridge

mountSource(map, source, options) from @honua/sdk-js/map (@experimental) mounts any contract Source as a live, styled MapLibre layer set with no MapPackage and no Honua server — the standalone slice of the kernel ADR's connection.mount(target) contract. The bridge selects bounded GeoJSON materialization or the dynamic query-tile path from declared capabilities plus a result-size heuristic, applies geometry-appropriate default styling, wires optional click popups and hover feature-state, and returns one disposable handle whose setFilter()/refresh() diff-update in place.

import maplibregl from "maplibre-gl";
import { connect } from "@honua/sdk-js";
import { mountSource } from "@honua/sdk-js/map";

const data = await connect({ endpoint: FEATURE_LAYER_URL, protocol: "auto", authorizationScopeFingerprint: "public" });
const mounted = await mountSource(map, data.source(), {
  popup: { factory: () => new maplibregl.Popup() },
  hover: true,
  fitBounds: true,
});
console.log(mounted.diagnostics.strategy, mounted.diagnostics.reasons);
mounted.dispose();

MapLibre GL JS Runtime For MapPackage

@honua/sdk-js/runtime binds a server-produced MapPackage (from honua-io/honua-server#731) to a caller-provided maplibre-gl.Map. It composes the style from mapSpec + styleRefs[] + theme tokens, projects sourceBindings[] through the @honua/sdk-js/contract adapters, and exposes an operational API that honua-io/honua-sdk-js#22 and #29 build on. The runtime never instantiates the map (maplibre-gl stays a peer dependency) and never issues edit writes.

import maplibregl from "maplibre-gl";
import { HonuaClient } from "@honua/sdk-js/honua";
import { loadMapPackage } from "@honua/sdk-js/runtime";

const map = new maplibregl.Map({ container: "map", style: { version: 8, sources: {}, layers: [] } });
const runtime = await loadMapPackage(pkg, map, {
  client: new HonuaClient({ baseUrl: "https://your-honua-server.example" }),
  popupFactory: () => new maplibregl.Popup(),
});

runtime.setLayerVisibility("parcels-fill", true);
runtime.bindPopup("parcels-fill");
await runtime.updatePackage(nextPkg);  // diffs by stable ids, falls back to setStyle on structural change
runtime.dispose();

Binding failures throw HonuaMapPackageError with a stage of "load" | "update" | "style-compose" | "source-bind" | "view" | "popup" | "dispose" under the opt-in sourceErrorPolicy: "fail-fast". Under the default "tolerant" policy a single per-source binding failure does not abort the load — the failed source is dropped from the composed style along with any layer that referenced it, the runtime emits a source-error event for the failed source, and a source-bind telemetry error span fires. Remaining sources keep rendering. Query-time adapter errors (HonuaCapabilityNotSupportedError, HonuaHttpError, adapter-specific classes) are not wrapped — they surface on the per-Source promises from runtime.dataset and through the shared HonuaClient interceptor chain; consumers fanning a query across the dataset can broadcast a per-source rejection through runtime.reportSourceError(sourceId, error) to fold it into the same source-error channel.

For mixed-protocol compositions (parcels FeatureServer + WMS basemap + STAC overlay + OData operational layer in one map), use intersectCapabilities from @honua/sdk-js/contract to compute the weakest capability set across participating sources before fanning a call out, and rely on Result.degraded[] entries (now carrying optional sourceId) for per-source attribution. Full composition guide: docs/composition.md.

Generated App Preview Runtime

@honua/sdk-js/generated-app is the browser-safe proof slice for generated operations dashboards. It projects canonical BuildSpec, AppPackage.manifest_artifact, and MapPackage inputs into a versioned honua_generated_app_manifest.v1 manifest with the operations-dashboard.v1 profile, then binds the generated map/table or list/count/chart/filter widgets through @honua/sdk-js/runtime and the shared ExplorationContext.

import { previewGeneratedApp } from "@honua/sdk-js/generated-app";

const preview = await previewGeneratedApp(
  { appPackage, mapPackage },
  {
    mapFactory: () => ({ map }),
    mapLoadOptions: { client },
  },
);

if (preview.status === "ready") {
  renderDashboard(preview.model);
} else {
  renderPreviewErrors(preview.errors);
}

The preview response is a discriminated union: ready responses include the resolved manifest, runtime handle, render model, and errors: []; error responses include serializable HonuaGeneratedAppDiagnostic objects and never throw from previewGeneratedApp. Call loadGeneratedAppRuntime directly when the host wants exceptions. Full contract reference: docs/generated-app-runtime.md. For map-backed manifests, the host must provide a MapPackage, mapFactory, and mapLoadOptions; when manifest.mapPackageId is present, the supplied MapPackage.mapPackageId must match before map construction. Map filter bindings use the widget layerId with manifest.bindings.layerId as fallback, and failed initial feature loads dispose partially loaded map resources before returning an error result.

Install

npm install @honua/sdk-js

Local development

Repo-root installs for local development and the checked-in browser demos currently require Node.js 20.19.0 or newer because the current demo/dev toolchain dependencies set that patch-level floor. The published @honua/sdk-js package itself remains on the documented Node 20+ runtime contract.

npm install

Storytelling demo loops:

npm run demo:quickstart:mock
# or, with examples/maplibre-quickstart/.env configured:
npm run demo:quickstart

# advanced storytelling demo:
npm run demo:25d:mock
# or, with examples/storytelling-25d-map/.env configured:
npm run demo:25d

Advanced analytics demo loop:

npm run demo:kepler:install
npm run demo:kepler:dev

Open http://127.0.0.1:4175 to view the fixture-first operations replay story. The committed fixture lives under examples/kepler-analytics/public/data, and maintainers can refresh it from a live Honua environment with:

npm run demo:kepler:refresh-fixture

The repo-root refresh wrapper builds the SDK before delegating to the example-local script. Set HONUA_DEMO_BASE_URL and any optional auth or service override env vars described in the example README before running it.

Imagery and COG demo loop:

npm run demo:imagery-cog:mock
# or, with examples/imagery-cog-quickstart/.env configured:
npm run demo:imagery-cog

Node backend quickstart loop:

npm run demo:node-backend:mock
# in another terminal:
npm run demo:node-backend

Verify

npm run typecheck
npm run build
npm run demo:quickstart:typecheck
npm run demo:25d:typecheck
npm run demo:node-backend:typecheck
npm run demo:unified-ops:typecheck
npx vitest run test/quickstart-config.test.ts test/quickstart-data.test.ts
npx vitest run test/node-backend-quickstart.test.ts
npx vitest run test/unified-ops-workspace.test.ts
npx vitest run test/cesium-route-playback.test.ts
npx vitest run test/storytelling-25d-config.test.ts test/storytelling-25d-data.test.ts
npm test
npm run test:playwright:quickstart
npm run test:playwright:unified-ops
npx playwright test test/playwright/cesium-route-playback.spec.mjs
npm run test:playwright:25d
npm run test:playwright
npm run demo:kepler:smoke
npm run test:quickstart:staging # requires HONUA_STAGING_* env
npm run test:integration # connect-only; requires HONUA_INTEGRATION_BASE_URL — see docs/integration-tests.md
npm run scan:arcgis -- ../../path/to/arcgis-app
npm run migrate:arcgis -- ../../path/to/arcgis-app --write --report migration-report.json
npm run report:migration:real-samples
npm run gate:migration:real-samples
npm run gate:migration:demo-target
npm run matrix:runtime
npm run build:split-packages

npm run test:playwright and npm run demo:kepler:smoke now bootstrap the isolated examples/kepler-analytics dependencies automatically when that package has not been installed yet.

Split Package Artifacts

Generate publish-ready split packages under dist/packages/:

npm run build:split-packages
npm run verify:split-packages

Create local tarballs for all split packages:

npm run pack:split-packages

CI publish workflow:

Request/Auth Bridge

import { HonuaClient } from "@honua/sdk-js";
import { createArcGisTokenInterceptor, createEsriRequestInterceptors } from "@honua/sdk-js/esri-compat";

const client = new HonuaClient({
  baseUrl: "https://example.test",
  interceptors: [
    ...createEsriRequestInterceptors([
      {
        urls: "/rest/services/default",
        before: (params) => {
          params.requestOptions.headers = {
            ...(params.requestOptions.headers ?? {}),
            "X-Migrated-By": "honua",
          };
        },
      },
    ]),
    createArcGisTokenInterceptor({
      applyTo: "/rest/services/default",
      mode: "query",
      getToken: async () => "arcgis-token-value",
    }),
  ],
});

For first-party Honua auth, prefer an auth provider over storing long-lived secrets in SDK configuration. The provider owns secure storage, refresh, and revocation; the SDK keeps only an in-memory cache and refreshes before expiresAt enters the configured skew window.

const client = new HonuaClient({
  baseUrl: "https://example.test",
  authRefreshSkewMs: 60_000,
  auth: {
    async getCredentials({ reason }) {
      const session = await authStore.getFreshSession({ force: reason !== "initial" });
      return {
        bearerToken: session.accessToken,
        expiresAt: session.expiresAt,
      };
    },
    async revokeCredentials(credentials) {
      if (credentials.bearerToken) {
        await authStore.revoke(credentials.bearerToken);
      }
    },
  },
});

await client.refreshAuthCredentials(); // force a key/token rotation probe
await client.revokeAuthCredentials(); // revoke cached credentials and clear SDK memory

Do not persist bearer tokens, API keys, or refresh tokens inside the SDK instance. Store them in the platform credential store or your server-side session layer, return short-lived credentials from auth.getCredentials, and use clearAuthCredentials() on logout or account switch. If a request fails with 401/403, call refreshAuthCredentials("unauthorized") after your app has handled any sign-in prompt, then retry the failed operation explicitly. Network, timeout, and retry behavior still flows through the same timeoutMs, retry, and interceptor pipeline.

OGC API Features (Honua-first)

import { HonuaClient } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://example.test" });
const ogc = client.ogcFeatures();

const collections = await ogc.collections();
const parcels = ogc.collection("0");
const items = await parcels.items({ limit: 100, filter: "status = 'active'" });
const allItems = await parcels.itemsAll({ pageSize: 500, maxPages: 20 });
const feature = await parcels.item({ featureId: "123" });

OGC API Tiles, Maps, Processes, STAC

The first-party OGC client covers the OGC conformance areas that operator apps need. Everything is exposed through canonical Honua types — OGC conformance class identifiers stay internal.

import { HonuaClient } from "@honua/sdk-js/honua";
import type { IJobRun } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://example.test" });

// OGC API Tiles — vector or raster over the canonical collection-tile route
const tile = await client.ogcTiles()
  .tileset("parcels", "WebMercatorQuad")
  .tile({ tileMatrix: 5, tileRow: 9, tileCol: 12 });

// OGC API Maps — server-rendered map images
const map = await client.ogcMaps().map({
  width: 1024, height: 1024,
  bbox: [-122, 37, -120, 38],
  collections: ["parcels", "roads"],
});

// OGC API Processes — async job execution mapped onto canonical IJobRun
const job: IJobRun = await client.ogcProcesses().execute({
  processId: "buffer",
  inputs: { feature: someGeoJson, distance: 500 },
  mode: "async",
});
const { outputs } = await job.results();

// STAC API — cross-collection search, also available through Source.query()
const search = await client.stac().search({
  bbox: [-122, 37, -120, 38],
  collections: ["sentinel-2"],
  filter: "cloud_cover < 10",
  filterLang: "cql2-text",
});

STAC in the browser bundle

HonuaStacSearch is part of the browser-safe /honua surface — import it (or call client.stac()) instead of hand-writing fetch against STAC endpoints. It is exported from the @honua/sdk-js/honua subpath, so it bundles with esbuild/Vite/Rollup like the rest of the client:

// Browser app (esbuild / Vite / Rollup). No Node-only imports.
import { HonuaClient, HonuaStacSearch } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://example.test" });

// Either construct it directly…
const stac = new HonuaStacSearch({ client });
// …or get the same instance from the client:
const sameStac = client.stac();

const landing = await stac.landing();
const items = await stac.search({
  bbox: [-122, 37, -120, 38],
  collections: ["sentinel-2"],
  datetime: "2026-01-01/2026-06-01",
});

// `searchAll` paginates for you (bounded by `maxPages`):
for await (const item of stac.searchStream({ collections: ["sentinel-2"] })) {
  // …
}

See docs/ogc-api.md for the full developer reference, docs/shared-client-contract.md for the canonical Source / IJobRun model, and docs/protocol-capability-matrix.md for capability coverage.

WFS 2.0

import { createDataset, PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
import { HonuaClient } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://server.honua.io" });
const dataset = createDataset({
  id: "parcels",
  client,
  sources: [
    {
      id: "parcels-wfs",
      protocol: "wfs",
      locator: {
        url: "https://server.honua.io/wfs",
        typeName: "parcels:lot",
        // Required for applyEdits when typeName is namespace-qualified;
        // bound on <wfs:Transaction xmlns:parcels="…"> so the server can
        // resolve the schema element. Falls back to a synthetic URN
        // when omitted.
        featureNamespace: "http://parcels.example.com/ns",
      },
      capabilities: PROTOCOL_DEFAULT_CAPABILITIES.wfs,
    },
  ],
});

const wfs = dataset.source("parcels-wfs")!;
const result = await wfs.query({ where: "STATE = 'CA' AND ACRES > 10" });
const ids = await wfs.queryObjectIds({ where: "STATUS = 'ACTIVE'" });

// Stored-query discovery + execution stays under the typed escape hatch.
const root = wfs.protocol("wfs")!.root;
const storedQueries = await root.storedQueries();

Query.where compiles to FES 2.0 (comparison, IN, BETWEEN, LIKE, IS NULL, boolean combinators); Query.spatialFilter becomes a KVP bbox= for envelope-only requests or a <fes:Filter> otherwise. Filters that exceed the GET budget switch to POST GetFeature with the <fes:Filter> body. applyEdits builds a single <wfs:Transaction> (<wfs:Insert> / <wfs:Update> / <wfs:Delete>) and surfaces per-handle <fes:ResourceId> IDs onto EditOutcome.id. GeoJSON is preferred over GML through OperationsMetadata negotiation; if the server only advertises GML the canonical surface throws HonuaCapabilityNotSupportedError and points callers at Source.protocol("wfs") for the raw payload. Full reference: docs/wfs.md.

OData v4

import { createDataset, PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
import { HonuaClient } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://server.honua.io" });
const dataset = createDataset({
  id: "parcels",
  client,
  sources: [
    {
      id: "parcels-odata",
      protocol: "odata",
      // Pass `entitySet` directly — or pass `layerId` and let the
      // adapter derive `Layers(<layerId>)/Features` for layer-scoped
      // server bindings.
      locator: { url: "https://server.honua.io/odata", entitySet: "Parcels" },
      capabilities: PROTOCOL_DEFAULT_CAPABILITIES.odata,
    },
  ],
});

const parcels = dataset.source("parcels-odata")!;
const result = await parcels.query({ where: "STATE = 'CA' AND ACRES > 10" });
const ids = await parcels.queryObjectIds({ where: "STATUS = 'ACTIVE'" });

// Dialect-specific operations stay behind the typed escape hatch.
const odata = parcels.protocol("odata")!;
const meta = await odata.metadata();
// `apply` accepts a literal OData v4 `$apply` transformation string
// per OData v4 §5.1.4 — `groupby((<dim>),aggregate(<expr>))` is the
// canonical form for SQL-style group-by + sum.
const aggregated = await odata.apply(
  "groupby((STATE),aggregate(ACRES with sum as SumAcres))",
);

Query.where accepts SQL-92 / OData $filter text; the adapter rewrites the documented intersection (IS NULLeq null, <>ne, =eq, plus the SQL comparison operators >= / <= / > / <ge / le / gt / lt) and rejects operators the parity matrix marks unsupported (has, in, any, all, cast, isof). Query.outFields splits onto $select for plain field names and $expand for navigation paths — ["Owner.name"] lowers to $expand=Owner($select=name) so related properties round-trip through the canonical request envelope. Query.spatialFilter translates to geo.intersects / geo.distance against the geometry column resolved from SourceDescriptor.schema.fields (typed esriFieldTypeGeometry) first, then the lazy $metadata probe. applyEdits routes adds → POST, updates → PATCH /<entitySet>(<key>) with the full canonical body (PUT is unsupported per the parity matrix), deletes → DELETE /<entitySet>(<key>). When EditEnvelope.rollbackOnFailure: true and $metadata advertises Capabilities.BatchSupported, all operations collapse into one $batch request with a shared atomicityGroup. OData is the first adapter to lazily fetch service $metadata and intersect declared capabilities against the server's Capabilities.* annotations — see docs/protocol-capability-matrix.md for the rule and docs/decisions/odata-library-selection.md for the runtime-library posture.

Mixed Esri + OGC in one app

import { HonuaClient } from "@honua/sdk-js/honua";

const client = new HonuaClient({ baseUrl: "https://example.test" });

const parcelsLayer = client.service("transport").featureLayer(0);
const parcelsOgc = client.ogcFeatures().collection("parcels");

const [features, items] = await Promise.all([
  parcelsLayer.queryFeatures({ where: "status = 'active'", outFields: ["OBJECTID"] }),
  parcelsOgc.items({ limit: 50 }),
]);

MapServer query helpers

import { HonuaClient } from "@honua/sdk-js/honua";

const client = new HonuaClient({
  baseUrl: "https://example.test",
  timeoutMs: 15000,
  retry: { maxRetries: 2, retryStatuses: [429, 503] },
});

const mapLayer = client.mapLayer("basemap", 4);
const allMapLayerFeatures = await mapLayer.queryFeaturesAll({ pageSize: 2000, maxPages: 25 });

const mapService = client.mapService("basemap");
const allServiceLayerFeatures = await mapService.queryLayerFeaturesAll({
  layerId: 4,
  pageSize: 2000,
  maxPages: 25,
});

const related = await mapService.queryLayerRelatedRecords({
  layerId: 4,
  relationshipId: 1,
  objectIds: [1001, 1002],
});

const mapLayerRelated = await mapLayer.queryRelatedFeatures({
  relationshipId: 1,
  objectIds: [1001],
});

Streaming Pagination

import { FeatureLayerCompat, CompatEventBus } from "@honua/sdk-js/esri-compat";

const layer = new FeatureLayerCompat({
  url: "https://example.test/rest/services/transport/FeatureServer/0",
});

// Collect all pages in one call
const allFeatures = await layer.queryFeaturesAll({ pageSize: 500, maxPages: 50 });

// Stream pages one at a time with an async generator
for await (const page of layer.queryFeaturesStream({ pageSize: 500 })) {
  console.log(`Received ${page.length} features`);
}

Event Lifecycle (.on)

import { FeatureLayerCompat, CompatEventBus } from "@honua/sdk-js/esri-compat";

const eventBus = new CompatEventBus();
const layer = new FeatureLayerCompat({
  url: "https://example.test/rest/services/transport/FeatureServer/0",
  eventBus,
});

// Listen for edit completions
const handle = layer.on("edits", (result) => {
  console.log("Edits applied:", result);
});

await layer.applyEdits({ adds: [{ attributes: { name: "New" } }] });

// Clean up when done
handle.remove();

TimeSlider Integration

import { FeatureLayerCompat, TimeSliderCompat, CompatEventBus } from "@honua/sdk-js/esri-compat";

const eventBus = new CompatEventBus();

const layer = new FeatureLayerCompat({
  url: "https://example.test/rest/services/events/FeatureServer/0",
  eventBus,
});

const slider = new TimeSliderCompat({
  eventBus,
  timeExtent: { start: new Date("2024-01-01"), end: new Date("2024-06-01") },
  stops: { interval: { value: 1, unit: "days" } },
});

// Connect slider to layer — time extent changes auto-filter queries
const connection = slider.connectLayer(layer);

// Queries now include the time parameter automatically
const features = await layer.queryFeatures({ where: "1=1" });

// Disconnect when done
connection.remove();

Migration CLI

# Scan only
node dist/src/migration/cli.js scan ./src --report scan-report.json

# Widget-usage inventory + ArcGIS 6.0 readiness report (classic widgets are removed
# at 6.0, as early as Q1 2027). Detects ESM imports, AMD require([...]) arrays, and
# dynamic $arcgis.import(...) specifiers; per-widget dispositions come from the
# generated docs/widget-survival-guide.md (source: src/migration/widget-dispositions.ts).
node dist/src/migration/cli.js widgets ./src                     # human table
node dist/src/migration/cli.js widgets ./src --json              # machine-readable
node dist/src/migration/cli.js widgets ./src --markdown          # shareable report
node dist/src/migration/cli.js widgets ./src --gate 80           # exit 2 if automated share < 80%
node dist/src/migration/cli.js widgets ./src --report widget-readiness.json

# Safe codemod (dry run)
node dist/src/migration/cli.js codemod ./src --report migration-report.json

# Safe codemod (write changes)
node dist/src/migration/cli.js codemod ./src --write --report migration-report.json

# Safe codemod (explicit honua target alias)
node dist/src/migration/cli.js codemod ./src --target honua --write --report migration-report.json

# Safe codemod (write changes targeting esri-leaflet for supported subset)
node dist/src/migration/cli.js codemod ./src --target esri-leaflet --write --report migration-report.json

# Safe codemod (write + inline TODO annotations for manual sites)
node dist/src/migration/cli.js codemod ./src --write --annotate-todos --report migration-report.json

# Emit parity matrix JSON (for docs/CI dashboards)
node dist/src/migration/cli.js matrix --report parity-matrix.json

# Emit runtime parity matrix JSON (for JS API capability tracking)
node dist/src/migration/cli.js runtime-matrix --report runtime-parity-matrix.json

# Generate readiness metrics for bundled complex real-sample fixtures
node dist/src/migration/cli.js fixtures --report reports/real-sample-metrics.json

# Inspect the fixture-only Esri sample migration corpus helpers from tests/docs
# See docs/esri-sample-corpus.md and test/fixtures/esri-sample-corpus/manifest.json

# Enforce strict readiness gates for bundled real-sample fixtures
node dist/src/migration/cli.js fixtures --fail-on-manual --fail-on-unhandled --fail-on-blocked --max-manual-ratio 0 --max-manual-intervention-ratio 0 --report reports/real-sample-metrics.json

# Enforce strict readiness gates for the demo target fixture only
node dist/src/migration/cli.js fixtures --target honua --fixtures esri-demo-feature-table-relates-app --fail-on-manual --fail-on-unhandled --fail-on-blocked --max-manual-ratio 0 --max-manual-intervention-ratio 0 --report reports/demo-featuretable-primary-metrics.json

# Limit fixture metrics to a subset and esri-leaflet target mode
node dist/src/migration/cli.js fixtures --target esri-leaflet --fixtures esri-demo-feature-table-popup-interaction-app --report reports/demo-featuretable-fallback-esri-leaflet-metrics.json

# Gate in CI (non-zero exit if migration constraints fail)
node dist/src/migration/cli.js codemod ./src --fail-on-manual --fail-on-unhandled --fail-on-blocked --max-manual-ratio 0.2 --max-manual-intervention-ratio 0.3

# Compare source vs target service fidelity for one layer
node dist/src/migration/cli.js reconcile --source-base-url https://source.example --source-service-id parcels --target-base-url https://target.example --target-service-id parcels --layer-id 0 --sample-size 200 --report reconcile-report.json

# Content inventory scan from ArcGIS Online/Portal
node dist/src/migration/cli.js content scan --portal https://org.maps.arcgis.com --report ./content/scan.json

# Content export (WebMaps + hosted layers)
node dist/src/migration/cli.js content export --portal https://org.maps.arcgis.com --output-dir ./export --report ./content/export.json

# Content import into Honua admin endpoint
node dist/src/migration/cli.js content import --source ./export --target https://honua.example.com --admin-api-key $HONUA_ADMIN_API_KEY --report ./content/import.json

# Content reconcile using export + import reports
node dist/src/migration/cli.js content reconcile --source ./export --report ./content/reconcile.json

# Convert a WebMap JSON export into Honua style config and rewrite portal URLs
node dist/src/migration/cli.js content-webmap --input ./export/webmap.json --output ./export/webmap.honua.json --source-url-prefix https://org.maps.arcgis.com --target-url-prefix https://honua.example.com --report ./export/webmap.report.json

Migration Admin Scanner

HonuaClient.scanMigrationSource() wraps the stable admin scan endpoint for source-system migration planning:

import { HonuaClient } from "@honua/sdk-js/honua";
import type { MigrationSourceInventoryArtifact } from "@honua/sdk-js/honua";

const client = new HonuaClient({
  baseUrl: "https://honua.example.com",
  apiKey: process.env.HONUA_ADMIN_API_KEY,
});

const inventory: MigrationSourceInventoryArtifact = await client.scanMigrationSource({
  sourceKind: "geoservices",
  sourceUrl: "https://source.example.com/arcgis/rest/services/Parcels/FeatureServer",
  timeoutSeconds: 30,
});

if (inventory.scanCompleteness.status === "failed") {
  // HTTP 200 means Honua produced an artifact; completeness is the planning gate.
}

Set exportJson: true to request POST /api/v1/admin/import/scan?export=json; the SDK still parses the JSON artifact. The SDK also exports public artifact constants and TypeScript shapes for source inventory, migration manifest, parity evidence pack, cutover readiness, and readiness attestations. Manifest, parity, and readiness artifacts are modeled for client-side review workflows only; the SDK does not assume server routes for those artifacts.

Esri Sample Corpus (#206)

The first paired sample-app/service corpus slice is fixture-only and documented in docs/esri-sample-corpus.md. The curated manifest at test/fixtures/esri-sample-corpus/manifest.json records Esri sample source URLs as metadata only, license/terms notes, skip reasons, and expected service/Portal references. PR CI uses Honua-owned snippets and must not contact live Esri services, vendor Esri sample code, or commit Esri service data.

FeatureTable Demo Lane (#327)

Primary target:

Fallback target:

Pinned attribution metadata is tracked in test/fixtures/DEMO_TARGETS.md and fixture-local ATTRIBUTION.md files.

Runbook (codemod-only CI reproducible path):

# 1) Run primary demo lane migration (honua target)
npm run demo:migration:featuretable

# 2) Validate primary lane readiness gates and report metrics
npm run gate:migration:demo-target

# 3) Validate fallback lane deterministic esri-leaflet mapping
npm run gate:migration:esri-leaflet-target

Report metrics to capture:

Quick metric extract:

node -e 'const r=require("./reports/demo-featuretable-codemod-report.json"); console.log({elapsedMs:r.elapsedMs, manualRewriteRatio:r.migration.manualRewriteMetric.ratio, manualInterventionCount:r.migration.manualInterventionMetric.numerator, manualRewriteCount:r.migration.manualRewriteMetric.numerator});'

The codemod is intentionally conservative: