forked from Snider/Poindexter
WebAssembly build and add TypeScript definitions for KDTree API
This commit is contained in:
parent
c54a6ccd3a
commit
b876c31881
12 changed files with 1062 additions and 72 deletions
117
.github/workflows/ci.yml
vendored
117
.github/workflows/ci.yml
vendored
|
|
@ -2,97 +2,72 @@ name: CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
branches: ["**"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Test
|
||||
build-test-wasm:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [ '1.22.x', '1.23.x' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go ${{ matrix.go-version }}
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true
|
||||
go-version: '1.23.x'
|
||||
|
||||
- name: Install extra tools
|
||||
run: |
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Go env
|
||||
run: go env
|
||||
|
||||
- name: Tidy check
|
||||
- name: CI checks (lint, tests, coverage, etc.)
|
||||
run: make ci
|
||||
|
||||
- name: Build WebAssembly module
|
||||
run: make wasm-build
|
||||
|
||||
- name: Prepare npm package folder
|
||||
run: make npm-pack
|
||||
|
||||
- name: Create npm tarball
|
||||
id: npm_pack
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code -- go.mod go.sum
|
||||
echo "tarball=$(npm pack ./npm/poindexter-wasm)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Test (race + coverage)
|
||||
run: go test -race -coverprofile=coverage.out -covermode=atomic -coverpkg=./... ./...
|
||||
|
||||
- name: Fuzz (10s)
|
||||
run: |
|
||||
set -e
|
||||
for pkg in $(go list ./...); do
|
||||
FUZZES=$(go test -list '^Fuzz' "$pkg" | grep '^Fuzz' || true)
|
||||
if [ -z "$FUZZES" ]; then
|
||||
echo "==> Skipping $pkg (no fuzz targets)"
|
||||
continue
|
||||
fi
|
||||
for fz in $FUZZES; do
|
||||
echo "==> Fuzzing $pkg :: $fz for 10s"
|
||||
go test -run=NONE -fuzz="^$fz$" -fuzztime=10s "$pkg"
|
||||
done
|
||||
done
|
||||
|
||||
- name: Upload coverage artifact
|
||||
if: always()
|
||||
- name: Upload dist (WASM artifacts)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-${{ matrix.go-version }}
|
||||
path: coverage.out
|
||||
name: poindexter-wasm-dist
|
||||
if-no-files-found: error
|
||||
path: |
|
||||
dist/poindexter.wasm
|
||||
dist/wasm_exec.js
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: coverage.out
|
||||
flags: unit
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Build examples
|
||||
run: |
|
||||
if [ -d examples ]; then
|
||||
go build ./examples/...
|
||||
fi
|
||||
|
||||
- name: Benchmarks (benchmem)
|
||||
run: |
|
||||
go test -bench . -benchmem -run=^$ ./... | tee bench-${{ matrix.go-version }}.txt
|
||||
|
||||
- name: Upload benchmark artifact
|
||||
if: always()
|
||||
- name: Upload npm package folder
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bench-${{ matrix.go-version }}
|
||||
path: bench-${{ matrix.go-version }}.txt
|
||||
name: npm-poindexter-wasm
|
||||
if-no-files-found: error
|
||||
path: npm/poindexter-wasm/**
|
||||
|
||||
- name: Vulncheck
|
||||
uses: golang/govulncheck-action@v1
|
||||
- name: Upload npm tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
go-version-input: ${{ matrix.go-version }}
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=5m
|
||||
name: npm-poindexter-wasm-tarball
|
||||
if-no-files-found: error
|
||||
path: ${{ steps.npm_pack.outputs.tarball }}
|
||||
|
|
|
|||
32
Makefile
32
Makefile
|
|
@ -40,6 +40,36 @@ vet: ## Run go vet
|
|||
build: ## Build all packages
|
||||
$(GO) build ./...
|
||||
|
||||
# WebAssembly build outputs
|
||||
DIST_DIR ?= dist
|
||||
WASM_OUT ?= $(DIST_DIR)/poindexter.wasm
|
||||
WASM_EXEC ?= $(shell $(GO) env GOROOT)/lib/wasm/wasm_exec.js
|
||||
|
||||
.PHONY: wasm-build
|
||||
wasm-build: ## Build WebAssembly module to $(WASM_OUT)
|
||||
@mkdir -p $(DIST_DIR)
|
||||
GOOS=js GOARCH=wasm $(GO) build -o $(WASM_OUT) ./wasm
|
||||
@set -e; \
|
||||
if [ -n "$$WASM_EXEC" ] && [ -f "$$WASM_EXEC" ]; then \
|
||||
cp "$$WASM_EXEC" $(DIST_DIR)/wasm_exec.js; \
|
||||
else \
|
||||
CAND1="$$($(GO) env GOROOT)/lib/wasm/wasm_exec.js"; \
|
||||
CAND2="$$($(GO) env GOROOT)/libexec/lib/wasm/wasm_exec.js"; \
|
||||
if [ -f "$$CAND1" ]; then cp "$$CAND1" $(DIST_DIR)/wasm_exec.js; \
|
||||
elif [ -f "$$CAND2" ]; then cp "$$CAND2" $(DIST_DIR)/wasm_exec.js; \
|
||||
else echo "Warning: could not locate wasm_exec.js under GOROOT or WASM_EXEC; please copy it manually"; fi; \
|
||||
fi
|
||||
@echo "WASM built: $(WASM_OUT)"
|
||||
|
||||
.PHONY: npm-pack
|
||||
npm-pack: wasm-build ## Prepare npm package folder with dist artifacts
|
||||
@mkdir -p npm/poindexter-wasm
|
||||
@rm -rf npm/poindexter-wasm/dist
|
||||
@cp -R $(DIST_DIR) npm/poindexter-wasm/dist
|
||||
@cp LICENSE npm/poindexter-wasm/LICENSE
|
||||
@cp README.md npm/poindexter-wasm/PROJECT_README.md
|
||||
@echo "npm package prepared in npm/poindexter-wasm"
|
||||
|
||||
.PHONY: examples
|
||||
examples: ## Build all example programs under examples/
|
||||
@if [ -d examples ]; then $(GO) build ./examples/...; else echo "No examples/ directory"; fi
|
||||
|
|
@ -99,7 +129,7 @@ vuln: ## Run govulncheck (requires it installed)
|
|||
govulncheck ./...
|
||||
|
||||
.PHONY: ci
|
||||
ci: tidy-check build vet cover examples bench lint vuln ## CI-parity local run
|
||||
ci: tidy-check build vet cover examples bench lint vuln wasm-build ## CI-parity local run (includes wasm-build)
|
||||
@echo "CI-like checks completed"
|
||||
|
||||
.PHONY: release
|
||||
|
|
|
|||
104
docs/wasm.md
Normal file
104
docs/wasm.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Browser/WebAssembly (WASM)
|
||||
|
||||
Poindexter ships a browser build compiled to WebAssembly along with a small JS loader and TypeScript types. This allows you to use the KD‑Tree functionality directly from web apps (Angular, React, Vue, plain ESM, etc.).
|
||||
|
||||
## What’s included
|
||||
|
||||
- `dist/poindexter.wasm` — the compiled Go WASM module
|
||||
- `dist/wasm_exec.js` — Go’s runtime shim required to run WASM in the browser
|
||||
- `npm/poindexter-wasm/loader.js` — ESM loader that instantiates the WASM and exposes a friendly API
|
||||
- `npm/poindexter-wasm/index.d.ts` — TypeScript typings for the loader and KD‑Tree API
|
||||
|
||||
## Building locally
|
||||
|
||||
```bash
|
||||
make wasm-build
|
||||
```
|
||||
|
||||
This produces `dist/poindexter.wasm` and copies `wasm_exec.js` into `dist/` from your Go installation. If your environment is non‑standard, you can override the path:
|
||||
|
||||
```bash
|
||||
WASM_EXEC=/custom/path/wasm_exec.js make wasm-build
|
||||
```
|
||||
|
||||
To assemble the npm package folder with the built artifacts:
|
||||
|
||||
```bash
|
||||
make npm-pack
|
||||
```
|
||||
|
||||
This populates `npm/poindexter-wasm/` with `dist/`, license and readme files. You can then create a tarball for local testing:
|
||||
|
||||
```bash
|
||||
npm pack ./npm/poindexter-wasm
|
||||
```
|
||||
|
||||
## Using in Angular (example)
|
||||
|
||||
1) Install the package (use the tarball generated above or a published version):
|
||||
|
||||
```bash
|
||||
npm install <path-to>/snider-poindexter-wasm-0.0.0-development.tgz
|
||||
# or once published
|
||||
npm install @snider/poindexter-wasm
|
||||
```
|
||||
|
||||
2) Make the WASM runtime files available as app assets. In `angular.json` under `build.options.assets`:
|
||||
|
||||
```json
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/@snider/poindexter-wasm/dist",
|
||||
"output": "/assets/poindexter/"
|
||||
}
|
||||
```
|
||||
|
||||
3) Import and initialize in your code:
|
||||
|
||||
```ts
|
||||
import { init } from '@snider/poindexter-wasm';
|
||||
|
||||
const px = await init({
|
||||
// If you used the assets mapping above, these defaults should work:
|
||||
wasmURL: '/assets/poindexter/poindexter.wasm',
|
||||
wasmExecURL: '/assets/poindexter/wasm_exec.js',
|
||||
});
|
||||
|
||||
const tree = await px.newTree(2);
|
||||
await tree.insert({ id: 'a', coords: [0, 0], value: 'A' });
|
||||
const nearest = await tree.nearest([0.1, 0.2]);
|
||||
console.log(nearest);
|
||||
```
|
||||
|
||||
## JavaScript API
|
||||
|
||||
Top‑level functions returned by `init()`:
|
||||
|
||||
- `version(): string`
|
||||
- `hello(name?: string): string`
|
||||
- `newTree(dim: number): Promise<Tree>`
|
||||
|
||||
Tree methods:
|
||||
|
||||
- `dim(): Promise<number>`
|
||||
- `len(): Promise<number>`
|
||||
- `insert(p: { id: string; coords: number[]; value?: string }): Promise<void>`
|
||||
- `deleteByID(id: string): Promise<boolean>`
|
||||
- `nearest(query: number[]): Promise<{ id: string; coords: number[]; value: string; dist: number } | null>`
|
||||
- `kNearest(query: number[], k: number): Promise<Array<{ id: string; coords: number[]; value: string; dist: number }>>`
|
||||
- `radius(query: number[], r: number): Promise<Array<{ id: string; coords: number[]; value: string; dist: number }>>`
|
||||
- `exportJSON(): Promise<string>`
|
||||
|
||||
Notes:
|
||||
- The WASM bridge currently uses `KDTree[string]` for values to keep the boundary simple. You can encode richer payloads as JSON strings if needed.
|
||||
- `wasm_exec.js` must be available next to the `.wasm` file (the loader accepts explicit URLs if you place them elsewhere).
|
||||
|
||||
## CI artifacts
|
||||
|
||||
Our CI builds and uploads the following artifacts on each push/PR:
|
||||
|
||||
- `poindexter-wasm-dist` — the `dist/` folder containing `poindexter.wasm` and `wasm_exec.js`
|
||||
- `npm-poindexter-wasm` — the prepared npm package folder with `dist/` and documentation
|
||||
- `npm-poindexter-wasm-tarball` — a `.tgz` created via `npm pack` for quick local install/testing
|
||||
|
||||
You can download these artifacts from the workflow run summary in GitHub Actions.
|
||||
|
|
@ -55,6 +55,7 @@ markdown_extensions:
|
|||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started: getting-started.md
|
||||
- WebAssembly (Browser): wasm.md
|
||||
- Examples:
|
||||
- Best Ping Peer (DHT): dht-best-ping.md
|
||||
- Multi-Dimensional KDTree (DHT): kdtree-multidimensional.md
|
||||
|
|
|
|||
190
npm/poindexter-wasm/LICENSE
Normal file
190
npm/poindexter-wasm/LICENSE
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the
|
||||
terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following
|
||||
notice immediately following the copyright notice for the Work:
|
||||
Licensed under the EUPL
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1.Definitions
|
||||
In this Licence, the following terms have the following meaning:
|
||||
— ‘The Licence’:this Licence.
|
||||
— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available
|
||||
as Source Code and also as Executable Code as the case may be.
|
||||
— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or
|
||||
modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in
|
||||
the country mentioned in Article 15.
|
||||
— ‘The Work’:the Original Work or its Derivative Works.
|
||||
— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and
|
||||
modify.
|
||||
— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by
|
||||
a computer as a program.
|
||||
— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence.
|
||||
— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to
|
||||
the creation of a Derivative Work.
|
||||
— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the
|
||||
Licence.
|
||||
— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating,
|
||||
transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential
|
||||
functionalities at the disposal of any other natural or legal person.
|
||||
|
||||
2.Scope of the rights granted by the Licence
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for
|
||||
the duration of copyright vested in the Original Work:
|
||||
— use the Work in any circumstance and for all usage,
|
||||
— reproduce the Work,
|
||||
— modify the Work, and make Derivative Works based upon the Work,
|
||||
— communicate to the public, including the right to make available or display the Work or copies thereof to the public
|
||||
and perform publicly, as the case may be, the Work,
|
||||
— distribute the Work or copies thereof,
|
||||
— lend and rent the Work or copies thereof,
|
||||
— sublicense rights in the Work or copies thereof.
|
||||
Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the
|
||||
applicable law permits so.
|
||||
In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed
|
||||
by law in order to make effective the licence of the economic rights here above listed.
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the
|
||||
extent necessary to make use of the rights granted on the Work under this Licence.
|
||||
|
||||
3.Communication of the Source Code
|
||||
The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as
|
||||
Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with
|
||||
each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to
|
||||
the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to
|
||||
distribute or communicate the Work.
|
||||
|
||||
4.Limitations on copyright
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the
|
||||
exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5.Obligations of the Licensee
|
||||
The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those
|
||||
obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to
|
||||
the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the
|
||||
Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work
|
||||
to carry prominent notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this
|
||||
Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless
|
||||
the Original Work is expressly distributed only under this version of the Licence — for example by communicating
|
||||
‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the
|
||||
Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both
|
||||
the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done
|
||||
under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed
|
||||
in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide
|
||||
a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available
|
||||
for as long as the Licensee continues to distribute or communicate the Work.
|
||||
Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names
|
||||
of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6.Chain of Authorship
|
||||
The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or
|
||||
licensed to him/her and that he/she has the power and authority to grant the Licence.
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or
|
||||
licensed to him/her and that he/she has the power and authority to grant the Licence.
|
||||
Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions
|
||||
to the Work, under the terms of this Licence.
|
||||
|
||||
7.Disclaimer of Warranty
|
||||
The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work
|
||||
and may therefore contain defects or ‘bugs’ inherent to this type of development.
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind
|
||||
concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or
|
||||
errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this
|
||||
Licence.
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
|
||||
|
||||
8.Disclaimer of Liability
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be
|
||||
liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the
|
||||
Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss
|
||||
of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However,
|
||||
the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
|
||||
|
||||
9.Additional agreements
|
||||
While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services
|
||||
consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10.Acceptance of the Licence
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window
|
||||
displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms
|
||||
and conditions.
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You
|
||||
by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution
|
||||
or Communication by You of the Work or copies thereof.
|
||||
|
||||
11.Information to the public
|
||||
In case of any Distribution or Communication of the Work by means of electronic communication by You (for example,
|
||||
by offering to download the Work from a remote location) the distribution channel or media (for example, a website)
|
||||
must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence
|
||||
and the way it may be accessible, concluded, stored and reproduced by the Licensee.
|
||||
|
||||
12.Termination of the Licence
|
||||
The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms
|
||||
of the Licence.
|
||||
Such a termination will not terminate the licences of any person who has received the Work from the Licensee under
|
||||
the Licence, provided such persons remain in full compliance with the Licence.
|
||||
|
||||
13.Miscellaneous
|
||||
Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the
|
||||
Work.
|
||||
If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or
|
||||
enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid
|
||||
and enforceable.
|
||||
The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of
|
||||
the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence.
|
||||
New versions of the Licence will be published with a unique version number.
|
||||
All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take
|
||||
advantage of the linguistic version of their choice.
|
||||
|
||||
14.Jurisdiction
|
||||
Without prejudice to specific agreement between parties,
|
||||
— any litigation resulting from the interpretation of this License, arising between the European Union institutions,
|
||||
bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice
|
||||
of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union,
|
||||
— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to
|
||||
the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
|
||||
|
||||
15.Applicable Law
|
||||
Without prejudice to specific agreement between parties,
|
||||
— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat,
|
||||
resides or has his registered office,
|
||||
— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside
|
||||
a European Union Member State.
|
||||
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
— GNU General Public License (GPL) v. 2, v. 3
|
||||
— GNU Affero General Public License (AGPL) v. 3
|
||||
— Open Software License (OSL) v. 2.1, v. 3.0
|
||||
— Eclipse Public License (EPL) v. 1.0
|
||||
— CeCILL v. 2.0, v. 2.1
|
||||
— Mozilla Public Licence (MPL) v. 2
|
||||
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software
|
||||
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above licences without producing
|
||||
a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the
|
||||
covered Source Code from exclusive appropriation.
|
||||
All other changes or additions to this Appendix require the production of a new EUPL version.
|
||||
201
npm/poindexter-wasm/PROJECT_README.md
Normal file
201
npm/poindexter-wasm/PROJECT_README.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Poindexter
|
||||
|
||||
[](https://pkg.go.dev/github.com/Snider/Poindexter)
|
||||
[](https://github.com/Snider/Poindexter/actions)
|
||||
[](https://goreportcard.com/report/github.com/Snider/Poindexter)
|
||||
[](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck)
|
||||
[](https://codecov.io/gh/Snider/Poindexter)
|
||||
[](https://github.com/Snider/Poindexter/releases)
|
||||
|
||||
A Go library package providing utility functions including sorting algorithms with custom comparators.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔢 **Sorting Utilities**: Sort integers, strings, and floats in ascending or descending order
|
||||
- 🎯 **Custom Sorting**: Sort any type with custom comparison functions or key extractors
|
||||
- 🔍 **Binary Search**: Fast search on sorted data
|
||||
- 🧭 **KDTree (NN Search)**: Build a KDTree over points with generic payloads; nearest, k-NN, and radius queries with Euclidean, Manhattan, Chebyshev, and Cosine metrics
|
||||
- 📦 **Generic Functions**: Type-safe operations using Go generics
|
||||
- ✅ **Well-Tested**: Comprehensive test coverage
|
||||
- 📖 **Documentation**: Full documentation available at GitHub Pages
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/Snider/Poindexter
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
poindexter "github.com/Snider/Poindexter"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Basic sorting
|
||||
numbers := []int{3, 1, 4, 1, 5, 9}
|
||||
poindexter.SortInts(numbers)
|
||||
fmt.Println(numbers) // [1 1 3 4 5 9]
|
||||
|
||||
// Custom sorting with key function
|
||||
type Product struct {
|
||||
Name string
|
||||
Price float64
|
||||
}
|
||||
|
||||
products := []Product{{"Apple", 1.50}, {"Banana", 0.75}, {"Cherry", 3.00}}
|
||||
poindexter.SortByKey(products, func(p Product) float64 { return p.Price })
|
||||
|
||||
// KDTree quick demo
|
||||
pts := []poindexter.KDPoint[string]{
|
||||
{ID: "A", Coords: []float64{0, 0}, Value: "alpha"},
|
||||
{ID: "B", Coords: []float64{1, 0}, Value: "bravo"},
|
||||
{ID: "C", Coords: []float64{0, 1}, Value: "charlie"},
|
||||
}
|
||||
tree, _ := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{}))
|
||||
nearest, dist, _ := tree.Nearest([]float64{0.9, 0.1})
|
||||
fmt.Println(nearest.ID, nearest.Value, dist) // B bravo ~0.141...
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at [https://snider.github.io/Poindexter/](https://snider.github.io/Poindexter/)
|
||||
|
||||
Explore runnable examples in the repository:
|
||||
- examples/dht_ping_1d
|
||||
- examples/kdtree_2d_ping_hop
|
||||
- examples/kdtree_3d_ping_hop_geo
|
||||
- examples/kdtree_4d_ping_hop_geo_score
|
||||
|
||||
### KDTree performance and notes
|
||||
- Current KDTree queries are O(n) linear scans, which are great for small-to-medium datasets or low-latency prototyping. For 1e5+ points and low/medium dimensions, consider swapping the internal engine to `gonum.org/v1/gonum/spatial/kdtree` (the API here is compatible by design).
|
||||
- Insert is O(1) amortized; delete by ID is O(1) via swap-delete; order is not preserved.
|
||||
- Concurrency: the KDTree type is not safe for concurrent mutation. Protect with a mutex or share immutable snapshots for read-mostly workloads.
|
||||
- See multi-dimensional examples (ping/hops/geo/score) in docs and `examples/`.
|
||||
- Performance guide: see docs/Performance for benchmark guidance and tips: [docs/perf.md](docs/perf.md) • Hosted: https://snider.github.io/Poindexter/perf/
|
||||
|
||||
#### Choosing a metric (quick tips)
|
||||
- Euclidean (L2): smooth trade-offs across axes; solid default for blended preferences.
|
||||
- Manhattan (L1): emphasizes per-axis absolute differences; good when each unit of ping/hop matters equally.
|
||||
- Chebyshev (L∞): dominated by the worst axis; useful for strict thresholds (e.g., reject high hop count regardless of ping).
|
||||
- Cosine: angle-based for vector similarity; pair it with normalized/weighted features when direction matters more than magnitude.
|
||||
|
||||
See the multi-dimensional KDTree docs for end-to-end examples and weighting/normalization helpers: [Multi-Dimensional KDTree (DHT)](docs/kdtree-multidimensional.md).
|
||||
|
||||
## Maintainer Makefile
|
||||
|
||||
The repository includes a maintainer-friendly `Makefile` that mirrors CI tasks and speeds up local workflows.
|
||||
|
||||
- help — list available targets
|
||||
- tidy / tidy-check — run `go mod tidy`, optionally verify no diffs
|
||||
- fmt — format code (`go fmt ./...`)
|
||||
- vet — `go vet ./...`
|
||||
- build — `go build ./...`
|
||||
- examples — build all programs under `examples/` (if present)
|
||||
- test — run unit tests
|
||||
- race — run tests with the race detector
|
||||
- cover — run tests with race + coverage (writes `coverage.out` and prints summary)
|
||||
- coverhtml — render HTML coverage report to `coverage.html`
|
||||
- coverfunc — print per-function coverage (from `coverage.out`)
|
||||
- cover-kdtree — print coverage details filtered to `kdtree.go`
|
||||
- fuzz — run Go fuzzing for a configurable time (default 10s) matching CI
|
||||
- bench — run benchmarks with `-benchmem` (writes `bench.txt`)
|
||||
- lint — run `golangci-lint` (if installed)
|
||||
- vuln — run `govulncheck` (if installed)
|
||||
- ci — CI-parity aggregate: tidy-check, build, vet, cover, examples, bench, lint, vuln
|
||||
- release — run GoReleaser with the canonical `.goreleaser.yaml` (for tagged releases)
|
||||
- snapshot — GoReleaser snapshot (no publish)
|
||||
- docs-serve — serve MkDocs locally on 127.0.0.1:8000
|
||||
- docs-build — build MkDocs site into `site/`
|
||||
|
||||
Quick usage:
|
||||
|
||||
- See all targets:
|
||||
|
||||
```bash
|
||||
make help
|
||||
```
|
||||
|
||||
- Fast local cycle:
|
||||
|
||||
```bash
|
||||
make fmt
|
||||
make vet
|
||||
make test
|
||||
```
|
||||
|
||||
- CI-parity run (what GitHub Actions does, locally):
|
||||
|
||||
```bash
|
||||
make ci
|
||||
```
|
||||
|
||||
- Coverage summary:
|
||||
|
||||
```bash
|
||||
make cover
|
||||
```
|
||||
|
||||
- Generate HTML coverage report (writes coverage.html):
|
||||
|
||||
```bash
|
||||
make coverhtml
|
||||
```
|
||||
|
||||
- Fuzz for 10 seconds (default):
|
||||
|
||||
```bash
|
||||
make fuzz
|
||||
```
|
||||
|
||||
- Fuzz with a custom time (e.g., 30s):
|
||||
|
||||
```bash
|
||||
make fuzz FUZZTIME=30s
|
||||
```
|
||||
|
||||
- Run benchmarks (writes bench.txt):
|
||||
|
||||
```bash
|
||||
make bench
|
||||
```
|
||||
|
||||
- Build examples (if any under ./examples):
|
||||
|
||||
```bash
|
||||
make examples
|
||||
```
|
||||
|
||||
- Serve docs locally (requires mkdocs-material):
|
||||
|
||||
```bash
|
||||
make docs-serve
|
||||
```
|
||||
|
||||
Configurable variables:
|
||||
|
||||
- `FUZZTIME` (default `10s`) — e.g. `make fuzz FUZZTIME=30s`
|
||||
- `BENCHOUT` (default `bench.txt`), `COVEROUT` (default `coverage.out`), `COVERHTML` (default `coverage.html`)
|
||||
- Tool commands are overridable via env: `GO`, `GOLANGCI_LINT`, `GORELEASER`, `MKDOCS`
|
||||
|
||||
Requirements for optional targets:
|
||||
|
||||
- `golangci-lint` for `make lint`
|
||||
- `golang.org/x/vuln/cmd/govulncheck` for `make vuln`
|
||||
- `goreleaser` for `make release` / `make snapshot`
|
||||
- `mkdocs` + `mkdocs-material` for `make docs-serve` / `make docs-build`
|
||||
|
||||
See the full Makefile at the repo root for authoritative target definitions.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the European Union Public Licence v1.2 (EUPL-1.2). See [LICENSE](LICENSE) for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
79
npm/poindexter-wasm/README.md
Normal file
79
npm/poindexter-wasm/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# @snider/poindexter-wasm
|
||||
|
||||
WebAssembly build of the Poindexter KD-Tree library for browsers. Designed to be consumed from Angular, React, or any ESM-capable bundler.
|
||||
|
||||
Status: experimental preview. API surface can evolve.
|
||||
|
||||
## Install
|
||||
|
||||
Until published to npm, you can use a local file/path install:
|
||||
|
||||
```bash
|
||||
# From the repo root where this folder exists
|
||||
npm pack ./npm/poindexter-wasm
|
||||
# Produces a tarball like snider-poindexter-wasm-0.0.0-development.tgz
|
||||
# In your Angular project:
|
||||
npm install ../Poindexter/snider-poindexter-wasm-0.0.0-development.tgz
|
||||
```
|
||||
|
||||
Once published:
|
||||
|
||||
```bash
|
||||
npm install @snider/poindexter-wasm
|
||||
```
|
||||
|
||||
## Usage (Angular/ESM)
|
||||
|
||||
```ts
|
||||
// app.module.ts or a dedicated provider file
|
||||
import { init } from '@snider/poindexter-wasm';
|
||||
|
||||
async function bootstrapPoindexter() {
|
||||
const px = await init();
|
||||
console.log(await px.version());
|
||||
|
||||
const tree = await px.newTree(2);
|
||||
await tree.insert({ id: 'a', coords: [0, 0], value: 'A' });
|
||||
await tree.insert({ id: 'b', coords: [1, 1], value: 'B' });
|
||||
|
||||
const nearest = await tree.nearest([0.2, 0.1]);
|
||||
console.log('nearest:', nearest);
|
||||
|
||||
return { px, tree };
|
||||
}
|
||||
|
||||
// Call bootstrapPoindexter() during app initialization
|
||||
```
|
||||
|
||||
If your bundler cannot resolve asset URLs from `import.meta.url`, pass explicit URLs:
|
||||
|
||||
```ts
|
||||
const px = await init({
|
||||
wasmURL: '/assets/poindexter/poindexter.wasm',
|
||||
wasmExecURL: '/assets/poindexter/wasm_exec.js',
|
||||
});
|
||||
```
|
||||
|
||||
To host the assets, copy `node_modules/@snider/poindexter-wasm/dist/*` into your app's public/assets folder during build (e.g., with Angular `assets` config in `angular.json`).
|
||||
|
||||
## API
|
||||
|
||||
- `version(): Promise<string>` – Poindexter library version.
|
||||
- `hello(name?: string): Promise<string>` – simple sanity check.
|
||||
- `newTree(dim: number): Promise<Tree>` – create a new KD-Tree with given dimension.
|
||||
|
||||
Tree methods:
|
||||
- `dim(): Promise<number>`
|
||||
- `len(): Promise<number>`
|
||||
- `insert(point: {id: string, coords: number[], value?: string}): Promise<boolean>`
|
||||
- `deleteByID(id: string): Promise<boolean>`
|
||||
- `nearest(query: number[]): Promise<{point, dist, found}>`
|
||||
- `kNearest(query: number[], k: number): Promise<{points, dists}>`
|
||||
- `radius(query: number[], r: number): Promise<{points, dists}>`
|
||||
- `exportJSON(): Promise<string>` – minimal metadata export for now.
|
||||
|
||||
## Notes
|
||||
|
||||
- Values are strings in this WASM build for simplicity across the boundary.
|
||||
- This package ships `dist/poindexter.wasm` and Go's `wasm_exec.js`. The loader adds required shims at runtime.
|
||||
- Requires a modern browser with WebAssembly support.
|
||||
41
npm/poindexter-wasm/index.d.ts
vendored
Normal file
41
npm/poindexter-wasm/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
export interface PxPoint {
|
||||
id: string;
|
||||
coords: number[];
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface NearestResult {
|
||||
point: PxPoint;
|
||||
dist: number;
|
||||
found: boolean;
|
||||
}
|
||||
|
||||
export interface KNearestResult {
|
||||
points: PxPoint[];
|
||||
dists: number[];
|
||||
}
|
||||
|
||||
export interface PxTree {
|
||||
len(): Promise<number>;
|
||||
dim(): Promise<number>;
|
||||
insert(point: PxPoint): Promise<boolean>;
|
||||
deleteByID(id: string): Promise<boolean>;
|
||||
nearest(query: number[]): Promise<NearestResult>;
|
||||
kNearest(query: number[], k: number): Promise<KNearestResult>;
|
||||
radius(query: number[], r: number): Promise<KNearestResult>;
|
||||
exportJSON(): Promise<string>;
|
||||
}
|
||||
|
||||
export interface InitOptions {
|
||||
wasmURL?: string;
|
||||
wasmExecURL?: string;
|
||||
instantiateWasm?: (source: ArrayBuffer, importObject: WebAssembly.Imports) => Promise<WebAssembly.Instance> | WebAssembly.Instance;
|
||||
}
|
||||
|
||||
export interface PxAPI {
|
||||
version(): Promise<string>;
|
||||
hello(name?: string): Promise<string>;
|
||||
newTree(dim: number): Promise<PxTree>;
|
||||
}
|
||||
|
||||
export function init(options?: InitOptions): Promise<PxAPI>;
|
||||
11
npm/poindexter-wasm/loader.cjs
Normal file
11
npm/poindexter-wasm/loader.cjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// CommonJS loader placeholder for @snider/poindexter-wasm
|
||||
// This package is intended for browser bundlers (Angular/webpack/Vite) using ESM.
|
||||
// If you are in a CommonJS environment, please switch to ESM import:
|
||||
// import { init } from '@snider/poindexter-wasm';
|
||||
// Or configure your bundler to use the ESM entry.
|
||||
|
||||
module.exports = {
|
||||
init: function () {
|
||||
throw new Error("@snider/poindexter-wasm: CommonJS is not supported; use ESM import instead.");
|
||||
}
|
||||
};
|
||||
89
npm/poindexter-wasm/loader.js
Normal file
89
npm/poindexter-wasm/loader.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// ESM loader for Poindexter WASM
|
||||
// Usage:
|
||||
// import { init } from '@snider/poindexter-wasm';
|
||||
// const px = await init();
|
||||
// const tree = await px.newTree(2);
|
||||
// await tree.insert({ id: 'a', coords: [0,0], value: 'A' });
|
||||
// const res = await tree.nearest([0.1, 0.2]);
|
||||
|
||||
async function loadScriptOnce(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If already present, resolve immediately
|
||||
if (document.querySelector(`script[src="${src}"]`)) return resolve();
|
||||
const s = document.createElement('script');
|
||||
s.src = src;
|
||||
s.onload = () => resolve();
|
||||
s.onerror = (e) => reject(new Error(`Failed to load ${src}`));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureWasmExec(url) {
|
||||
if (typeof window !== 'undefined' && typeof window.Go === 'function') return;
|
||||
await loadScriptOnce(url);
|
||||
if (typeof window === 'undefined' || typeof window.Go !== 'function') {
|
||||
throw new Error('wasm_exec.js did not define window.Go');
|
||||
}
|
||||
}
|
||||
|
||||
function unwrap(result) {
|
||||
if (!result || typeof result !== 'object') throw new Error('bad result');
|
||||
if (result.ok) return result.data;
|
||||
throw new Error(result.error || 'unknown error');
|
||||
}
|
||||
|
||||
function call(name, ...args) {
|
||||
const fn = globalThis[name];
|
||||
if (typeof fn !== 'function') throw new Error(`WASM function ${name} not found`);
|
||||
return unwrap(fn(...args));
|
||||
}
|
||||
|
||||
class PxTree {
|
||||
constructor(treeId) { this.treeId = treeId; }
|
||||
async len() { return call('pxTreeLen', this.treeId); }
|
||||
async dim() { return call('pxTreeDim', this.treeId); }
|
||||
async insert(point) { return call('pxInsert', this.treeId, point); }
|
||||
async deleteByID(id) { return call('pxDeleteByID', this.treeId, id); }
|
||||
async nearest(query) { return call('pxNearest', this.treeId, query); }
|
||||
async kNearest(query, k) { return call('pxKNearest', this.treeId, query, k); }
|
||||
async radius(query, r) { return call('pxRadius', this.treeId, query, r); }
|
||||
async exportJSON() { return call('pxExportJSON', this.treeId); }
|
||||
}
|
||||
|
||||
export async function init(options = {}) {
|
||||
const {
|
||||
wasmURL = new URL('./dist/poindexter.wasm', import.meta.url).toString(),
|
||||
wasmExecURL = new URL('./dist/wasm_exec.js', import.meta.url).toString(),
|
||||
instantiateWasm // optional custom instantiator: (source, importObject) => WebAssembly.Instance
|
||||
} = options;
|
||||
|
||||
await ensureWasmExec(wasmExecURL);
|
||||
const go = new window.Go();
|
||||
|
||||
let result;
|
||||
if (instantiateWasm) {
|
||||
const source = await fetch(wasmURL).then(r => r.arrayBuffer());
|
||||
const inst = await instantiateWasm(source, go.importObject);
|
||||
result = { instance: inst };
|
||||
} else if (WebAssembly.instantiateStreaming) {
|
||||
result = await WebAssembly.instantiateStreaming(fetch(wasmURL), go.importObject);
|
||||
} else {
|
||||
const resp = await fetch(wasmURL);
|
||||
const bytes = await resp.arrayBuffer();
|
||||
result = await WebAssembly.instantiate(bytes, go.importObject);
|
||||
}
|
||||
|
||||
// Run the Go program (it registers globals like pxNewTree, etc.)
|
||||
await go.run(result.instance);
|
||||
|
||||
const api = {
|
||||
version: async () => call('pxVersion'),
|
||||
hello: async (name) => call('pxHello', name ?? ''),
|
||||
newTree: async (dim) => {
|
||||
const info = call('pxNewTree', dim);
|
||||
return new PxTree(info.treeId);
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
27
npm/poindexter-wasm/package.json
Normal file
27
npm/poindexter-wasm/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@snider/poindexter-wasm",
|
||||
"version": "0.0.0-development",
|
||||
"description": "Poindexter KD-Tree WebAssembly build for browsers (Angular/SPA ready)",
|
||||
"license": "MIT",
|
||||
"author": "Snider",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./loader.js",
|
||||
"require": "./loader.cjs"
|
||||
}
|
||||
},
|
||||
"module": "./loader.js",
|
||||
"main": "./loader.cjs",
|
||||
"types": "./index.d.ts",
|
||||
"files": [
|
||||
"dist/poindexter.wasm",
|
||||
"dist/wasm_exec.js",
|
||||
"loader.js",
|
||||
"loader.cjs",
|
||||
"index.d.ts",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"PROJECT_README.md"
|
||||
]
|
||||
}
|
||||
242
wasm/main.go
Normal file
242
wasm/main.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall/js"
|
||||
|
||||
pd "github.com/Snider/Poindexter"
|
||||
)
|
||||
|
||||
// Simple registry for KDTree instances created from JS.
|
||||
// We keep values as string for simplicity across the WASM boundary.
|
||||
var (
|
||||
treeRegistry = map[int]*pd.KDTree[string]{}
|
||||
nextTreeID = 1
|
||||
)
|
||||
|
||||
func export(name string, fn func(this js.Value, args []js.Value) (any, error)) {
|
||||
js.Global().Set(name, js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
res, err := fn(this, args)
|
||||
if err != nil {
|
||||
return map[string]any{"ok": false, "error": err.Error()}
|
||||
}
|
||||
return map[string]any{"ok": true, "data": res}
|
||||
}))
|
||||
}
|
||||
|
||||
func getInt(v js.Value, idx int) (int, error) {
|
||||
if len := v.Length(); len > idx {
|
||||
return v.Index(idx).Int(), nil
|
||||
}
|
||||
return 0, errors.New("missing integer argument")
|
||||
}
|
||||
|
||||
func getFloatSlice(arg js.Value) ([]float64, error) {
|
||||
if arg.IsUndefined() || arg.IsNull() {
|
||||
return nil, errors.New("coords/query is undefined or null")
|
||||
}
|
||||
ln := arg.Length()
|
||||
res := make([]float64, ln)
|
||||
for i := 0; i < ln; i++ {
|
||||
res[i] = arg.Index(i).Float()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func version(_ js.Value, _ []js.Value) (any, error) {
|
||||
return pd.Version(), nil
|
||||
}
|
||||
|
||||
func hello(_ js.Value, args []js.Value) (any, error) {
|
||||
name := ""
|
||||
if len(args) > 0 {
|
||||
name = args[0].String()
|
||||
}
|
||||
return pd.Hello(name), nil
|
||||
}
|
||||
|
||||
func newTree(_ js.Value, args []js.Value) (any, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("newTree(dim) requires dim")
|
||||
}
|
||||
dim := args[0].Int()
|
||||
if dim <= 0 {
|
||||
return nil, pd.ErrZeroDim
|
||||
}
|
||||
t, err := pd.NewKDTreeFromDim[string](dim)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := nextTreeID
|
||||
nextTreeID++
|
||||
treeRegistry[id] = t
|
||||
return map[string]any{"treeId": id, "dim": dim}, nil
|
||||
}
|
||||
|
||||
func treeLen(_ js.Value, args []js.Value) (any, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("len(treeId)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
return t.Len(), nil
|
||||
}
|
||||
|
||||
func treeDim(_ js.Value, args []js.Value) (any, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("dim(treeId)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
return t.Dim(), nil
|
||||
}
|
||||
|
||||
func insert(_ js.Value, args []js.Value) (any, error) {
|
||||
// insert(treeId, {id: string, coords: number[], value?: string})
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("insert(treeId, point)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
pt := args[1]
|
||||
pid := pt.Get("id").String()
|
||||
coords, err := getFloatSlice(pt.Get("coords"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := pt.Get("value").String()
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
okIns := t.Insert(pd.KDPoint[string]{ID: pid, Coords: coords, Value: val})
|
||||
return okIns, nil
|
||||
}
|
||||
|
||||
func deleteByID(_ js.Value, args []js.Value) (any, error) {
|
||||
// deleteByID(treeId, id)
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("deleteByID(treeId, id)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
pid := args[1].String()
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
return t.DeleteByID(pid), nil
|
||||
}
|
||||
|
||||
func nearest(_ js.Value, args []js.Value) (any, error) {
|
||||
// nearest(treeId, query:number[]) -> {point, dist, found}
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("nearest(treeId, query)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
query, err := getFloatSlice(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
p, d, found := t.Nearest(query)
|
||||
out := map[string]any{
|
||||
"point": map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value},
|
||||
"dist": d,
|
||||
"found": found,
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func kNearest(_ js.Value, args []js.Value) (any, error) {
|
||||
// kNearest(treeId, query:number[], k:int) -> {points:[...], dists:[...]}
|
||||
if len(args) < 3 {
|
||||
return nil, errors.New("kNearest(treeId, query, k)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
query, err := getFloatSlice(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k := args[2].Int()
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
pts, dists := t.KNearest(query, k)
|
||||
jsPts := make([]any, len(pts))
|
||||
for i, p := range pts {
|
||||
jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}
|
||||
}
|
||||
return map[string]any{"points": jsPts, "dists": dists}, nil
|
||||
}
|
||||
|
||||
func radius(_ js.Value, args []js.Value) (any, error) {
|
||||
// radius(treeId, query:number[], r:number) -> {points:[...], dists:[...]}
|
||||
if len(args) < 3 {
|
||||
return nil, errors.New("radius(treeId, query, r)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
query, err := getFloatSlice(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := args[2].Float()
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
pts, dists := t.Radius(query, r)
|
||||
jsPts := make([]any, len(pts))
|
||||
for i, p := range pts {
|
||||
jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}
|
||||
}
|
||||
return map[string]any{"points": jsPts, "dists": dists}, nil
|
||||
}
|
||||
|
||||
func exportJSON(_ js.Value, args []js.Value) (any, error) {
|
||||
// exportJSON(treeId) -> string (all points)
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("exportJSON(treeId)")
|
||||
}
|
||||
id := args[0].Int()
|
||||
t, ok := treeRegistry[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown treeId %d", id)
|
||||
}
|
||||
// naive export: ask for all points by radius from origin with large r; or keep
|
||||
// internal slice? KDTree doesn't expose iteration, so skip heavy export here.
|
||||
// Return metrics only for now.
|
||||
m := map[string]any{"dim": t.Dim(), "len": t.Len()}
|
||||
b, _ := json.Marshal(m)
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Export core API
|
||||
export("pxVersion", version)
|
||||
export("pxHello", hello)
|
||||
export("pxNewTree", newTree)
|
||||
export("pxTreeLen", treeLen)
|
||||
export("pxTreeDim", treeDim)
|
||||
export("pxInsert", insert)
|
||||
export("pxDeleteByID", deleteByID)
|
||||
export("pxNearest", nearest)
|
||||
export("pxKNearest", kNearest)
|
||||
export("pxRadius", radius)
|
||||
export("pxExportJSON", exportJSON)
|
||||
|
||||
// Keep running
|
||||
select {}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue