add a typescript library to access the backend

This commit is contained in:
Damien Elmes 2020-01-06 13:10:15 +10:00
parent ac205eeb37
commit 6ebd972e4a
7 changed files with 179 additions and 0 deletions

9
tslib/.eslintrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"]
};

5
tslib/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.build
dist
package-lock.json

25
tslib/Makefile Normal file
View File

@ -0,0 +1,25 @@
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.SUFFIXES:
$(shell mkdir -p .build dist)
all: build
.build/deps: package.json
npm i
@touch $@
build: .build/deps dist/backend_pb.js
npm run build
dist/backend_pb.js: .build/deps ../proto/backend.proto
npm run proto
.PHONY: check
check:
npm run lint
npm run check-pretty

25
tslib/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "anki",
"version": "0.1.0",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.11.0",
"@typescript-eslint/parser": "^2.11.0",
"eslint": "^6.7.2",
"prettier": "^1.19.1",
"typescript": "^3.7.3"
},
"scripts": {
"prepare": "npm run proto; npm run build",
"build": "tsc --build tsconfig.json",
"proto": "pbjs -t static-module ../proto/backend.proto -o dist/backend_pb.js; pbts dist/backend_pb.js -o dist/backend_pb.d.ts",
"pretty": "prettier --write src/*.ts",
"check-pretty": "prettier --check src/*.ts",
"lint": "eslint --max-warnings=0 src/*"
},
"dependencies": {
"protobufjs": "^6.8.8"
},
"files": [
"dist/*"
]
}

77
tslib/src/backend.ts Normal file
View File

@ -0,0 +1,77 @@
import { backend_proto as pb } from "../dist/backend_pb";
import { expectNotNull } from "./tsutils";
export { pb };
const BACKEND_URL = "http://localhost:3000/request";
function responseError(err: pb.BackendError): Error {
switch (err.value) {
case "invalidInput":
return Error(`invalid input: ${err.invalidInput?.info}`);
case "templateParse":
return Error(`template parse failed: ${err.templateParse?.info}`);
default:
return Error("unexpected error response value");
}
}
/** Encode and send a request, decoding and returning the response.
* Throws on error.
*/
async function webRequest(input: pb.IBackendInput): Promise<pb.BackendOutput> {
// encode request
const err = pb.BackendInput.verify(input);
if (err) {
throw Error(err);
}
const buf = pb.BackendInput.encode(input).finish();
// send to backend
const resp = await fetch(BACKEND_URL, {
method: "POST",
body: buf,
headers: {
"Content-Type": "application/protobuf",
"Accept": "application/protobuf",
}
});
if (!resp.ok) {
throw Error(`unexpected reply: ${resp.statusText}`);
}
// get returned bytes
const respBlob = await resp.blob();
const respBuf = await new Response(respBlob).arrayBuffer();
// decode response, throwing on error/missing
const result = pb.BackendOutput.decode(new Uint8Array(respBuf));
if (result.value === undefined) {
throw Error("Unexpected vaule in backend output.");
} else if (result.value === "error") {
throw responseError(result?.error as pb.BackendError);
} else {
return result;
}
}
export async function deckTree(): Promise<pb.IDeckTreeNode[]> {
const resp = await webRequest({
deckTree: new pb.Empty()
});
return expectNotNull(resp?.deckTree?.top?.children);
}
export async function findCards(search: string): Promise<number[]> {
const resp = await webRequest({
findCards: new pb.FindCardsIn({ search })
});
return expectNotNull(resp?.findCards?.cardIds) as number[];
}
// just sort field for now
export async function browserRows(cardIds: number[]): Promise<string[]> {
const resp = await webRequest({
browserRows: new pb.BrowserRowsIn({ cardIds })
});
return expectNotNull(resp?.browserRows?.sortFields);
}

14
tslib/src/tsutils.ts Normal file
View File

@ -0,0 +1,14 @@
/** Throws if argument is null/undefined. */
export function expectNotNull<T>(val: T | null | undefined): T {
if (val === null || val === undefined) {
throw Error("Unexpected missing value.");
}
return val as T;
}
//* Throws if argument is not truthy. */
export function assert<T>(val: T): asserts val {
if (!val) {
throw Error("Assertion failed.");
}
}

24
tslib/tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"exclude": ["node_modules", "dist"],
"compilerOptions": {
/* Basic Options */
"target": "es6",
"module": "es6",
"declaration": true, /* Generates corresponding '.d.ts' file. */
"rootDir": "src",
"outDir": "dist",
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
}
}