├── lib.go ├── ts ├── .npmrc ├── src │ ├── cf │ │ ├── index.ts │ │ ├── cf-sys-abstraction.ts │ │ └── cf-env-actions.ts │ ├── web │ │ ├── index.ts │ │ ├── web-env-actions.ts │ │ └── web-sys-abstraction.ts │ ├── test │ │ ├── index.ts │ │ ├── mock-logger.ts │ │ ├── mock-logger.test.ts │ │ ├── test-exit-handler.ts │ │ └── log-write-stream.ts │ ├── deno │ │ ├── index.ts │ │ ├── deno-env-actions.ts │ │ ├── deno-file-service.ts │ │ └── deno-sys-abstraction.ts │ ├── version.ts │ ├── cf-test-main.ts │ ├── node │ │ ├── index.ts │ │ ├── mock-file-service.test.ts │ │ ├── mock-file-service.ts │ │ ├── node-env-actions.ts │ │ ├── node-file-service.ts │ │ ├── node-sys-abstraction.test.ts │ │ └── node-sys-abstraction.ts │ ├── utils │ │ ├── string2stream.test.ts │ │ ├── index.ts │ │ ├── string2stream.ts │ │ ├── fanout-write-stream.ts │ │ ├── stream2string.test.ts │ │ ├── stream2string.ts │ │ ├── stream-test-helper.ts │ │ ├── stream-map.ts │ │ ├── get-params-result.ts │ │ ├── stripper.ts │ │ ├── console-write-stream.ts │ │ ├── rebuffer.ts │ │ ├── stripper.test.ts │ │ ├── stream-map.test.ts │ │ ├── rebuffer.test.ts │ │ ├── relative-path.ts │ │ └── relative-path.test.ts │ ├── txt-en-decoder.ts │ ├── time.ts │ ├── file-service.ts │ ├── future.ts │ ├── crypto.test.ts │ ├── index.ts │ ├── future.test.ts │ ├── sys-abstraction.ts │ ├── option.ts │ ├── bin2text.ts │ ├── runtime.ts │ ├── log-writer-impl.ts │ ├── bin2text.test.ts │ ├── log-level-impl.ts │ ├── result.test.ts │ ├── base-sys-abstraction.test.ts │ ├── http_header.test.ts │ ├── crypto.ts │ ├── result.ts │ ├── http_header.ts │ ├── resolve-once.ts │ ├── sys-env.test.ts │ ├── tracer.ts │ ├── sys-env.ts │ ├── base-sys-abstraction.ts │ ├── logger.ts │ ├── tracer.test.ts │ └── resolve-once.test.ts ├── .prettierignore ├── .prettierrc ├── smoke │ ├── it.sh │ └── smoke.ts ├── vitest.config.ts ├── wrangler.test.toml ├── vitest.workspace.ts ├── vitest.node.config.ts ├── jsr.json ├── setup-jsr-json.cjs ├── vitest.cf-runtime.config.ts ├── vitest.browser.config.ts ├── patch-version.cjs ├── prepare-pubdir.sh ├── eslint.config.js └── package.json ├── go.mod ├── .gitignore ├── go.sum ├── go ├── optional_test.go ├── result_test.go ├── optional.go └── result.go ├── .github ├── dependabot.yml └── workflows │ └── ng-nodejs-ci.yaml └── README.md /lib.go: -------------------------------------------------------------------------------- 1 | package cement 2 | -------------------------------------------------------------------------------- /ts/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /ts/src/cf/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cf-sys-abstraction.js"; 2 | -------------------------------------------------------------------------------- /ts/src/web/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./web-sys-abstraction.js"; 2 | -------------------------------------------------------------------------------- /ts/src/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./log-write-stream.js"; 2 | export * from "./mock-logger.js"; 3 | -------------------------------------------------------------------------------- /ts/src/deno/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deno-sys-abstraction.js"; 2 | export * from "./deno-file-service.js"; 3 | -------------------------------------------------------------------------------- /ts/src/version.ts: -------------------------------------------------------------------------------- 1 | export const VERSION: string = Object.keys({ 2 | __packageVersion__: "xxxx", 3 | })[0]; 4 | -------------------------------------------------------------------------------- /ts/src/cf-test-main.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | fetch(): Response { 3 | return new Response("Hello World!"); 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /ts/.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/src/generated/ 3 | **/pubdir/ 4 | **/dist/ 5 | **/package-lock.json 6 | **/pnpm-lock.yaml 7 | -------------------------------------------------------------------------------- /ts/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 132, 4 | "singleQuote": false, 5 | "semi": true, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /ts/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./node-file-service.js"; 2 | export * from "./node-sys-abstraction.js"; 3 | export * from "./mock-file-service.js"; 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mabels/cement 2 | 3 | go 1.22.0 4 | 5 | require gotest.tools/v3 v3.5.1 6 | 7 | require github.com/google/go-cmp v0.5.9 // indirect 8 | -------------------------------------------------------------------------------- /ts/smoke/it.sh: -------------------------------------------------------------------------------- 1 | 2 | cd smoke 3 | rm -f package.json; 4 | pnpm init 5 | pnpm install -f ../pubdir/adviser-cement-*.tgz 6 | npx tsx ./smoke.ts 7 | deno run --allow-read ./smoke.ts 8 | rm package.json pnpm-lock.yaml 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/dist/ 2 | .vscode/ 3 | **/pubdir/ 4 | **/*.tgz 5 | **/generated-ts/ 6 | **/node_modules/ 7 | **/src/generated/ 8 | **/smoke/generated/ 9 | **/smoke/pnpm-lock.yaml 10 | **/smoke/package.json 11 | **/__screenshots__/ 12 | -------------------------------------------------------------------------------- /ts/src/utils/string2stream.test.ts: -------------------------------------------------------------------------------- 1 | import { utils } from "@adviser/cement"; 2 | 3 | it("string2stream", async () => { 4 | const inStr = utils.string2stream("Hello World!"); 5 | expect(await utils.stream2string(inStr)).toBe("Hello World!"); 6 | }); 7 | -------------------------------------------------------------------------------- /ts/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | // plugins: [tsconfigPaths()], 5 | test: { 6 | include: ["src/**/*test.?(c|m)[jt]s?(x)"], 7 | globals: true, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /ts/wrangler.test.toml: -------------------------------------------------------------------------------- 1 | name = "cf-storage" 2 | main = "src/cf-test-main.ts" 3 | compatibility_date = "2024-04-19" 4 | compatibility_flags = ["nodejs_compat"] 5 | # upload_source_maps = true 6 | 7 | [observability] 8 | enabled = true 9 | head_sampling_rate = 1 10 | 11 | -------------------------------------------------------------------------------- /ts/vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from "vitest/config"; 2 | 3 | import node from "./vitest.node.config.js"; 4 | import browser from "./vitest.browser.config.js"; 5 | import cfRuntime from "./vitest.cf-runtime.config.js"; 6 | 7 | export default defineWorkspace([node, browser, cfRuntime]); 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 2 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 4 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 5 | -------------------------------------------------------------------------------- /ts/vitest.node.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import tsconfigPaths from "vite-tsconfig-paths"; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | name: "node", 8 | include: ["src/**/*test.?(c|m)[jt]s?(x)"], 9 | globals: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /ts/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./rebuffer.js"; 2 | export * from "./stream-map.js"; 3 | export * from "./stream2string.js"; 4 | export * from "./string2stream.js"; 5 | export * from "./console-write-stream.js"; 6 | export * from "./fanout-write-stream.js"; 7 | export * from "./get-params-result.js"; 8 | export * from "./stripper.js"; 9 | -------------------------------------------------------------------------------- /go/optional_test.go: -------------------------------------------------------------------------------- 1 | package cement 2 | 3 | import "testing" 4 | 5 | func TestOptionDefault(t *testing.T) { 6 | val := struct { 7 | opt Optional[int] 8 | }{} 9 | if val.opt.IsNone() != true { 10 | t.Fatal("Expected None") 11 | } 12 | } 13 | 14 | func TestOptionNone(t *testing.T) { 15 | val := struct { 16 | opt Optional[int] 17 | }{ 18 | opt: None[int](), 19 | } 20 | if val.opt.IsNone() != true { 21 | t.Fatal("Expected None") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ts/jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adviser/cement", 3 | "version": "0.0.0-dev-c", 4 | "exports": { 5 | ".": "./src/index.ts", 6 | "./web": "./src/web/index.ts", 7 | "./node": "./src/node/index.ts", 8 | "./deno": "./src/node/index.ts" 9 | }, 10 | "imports": { 11 | "yaml": "npm:yaml@^2.5.1", 12 | "ts-essentials": "npm:ts-essentials@^10.0.2" 13 | }, 14 | "license": "AFL-2.0", 15 | "publish": { 16 | "include": ["src/**/*.ts", "README.md", "LICENSE"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ts/setup-jsr-json.cjs: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const fileToPatch = process.argv[process.argv.length - 1]; 4 | 5 | const jsrJson = JSON.parse(fs.readFileSync(fileToPatch).toString()); 6 | 7 | for (const key of Object.keys(jsrJson.exports)) { 8 | jsrJson.exports[key] = jsrJson.exports[key].replace(/src\//, ""); 9 | } 10 | jsrJson.publish.include = jsrJson.publish.include.map((i) => i.replace(/src\//, "")); 11 | 12 | fs.writeFileSync(fileToPatch, JSON.stringify(jsrJson, undefined, 2) + "\n"); 13 | -------------------------------------------------------------------------------- /ts/vitest.cf-runtime.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from "vite-tsconfig-paths"; 2 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 3 | 4 | export default defineWorkersConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | name: "cf-runtime", 8 | globals: true, 9 | poolOptions: { 10 | workers: { 11 | wrangler: { configPath: "./wrangler.test.toml" }, 12 | }, 13 | }, 14 | include: ["src/**/*test.?(c|m)[jt]s?(x)"], 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /ts/src/utils/string2stream.ts: -------------------------------------------------------------------------------- 1 | import { TxtEnDecoder, Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 2 | 3 | export function string2stream(str: string, ende: TxtEnDecoder = Utf8EnDecoderSingleton()): ReadableStream { 4 | return uint8array2stream(ende.encode(str)); 5 | } 6 | 7 | export function uint8array2stream(str: Uint8Array): ReadableStream { 8 | return new ReadableStream({ 9 | start(controller): void { 10 | controller.enqueue(str); 11 | controller.close(); 12 | }, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /ts/vitest.browser.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths()], 7 | test: { 8 | name: "browser", 9 | include: ["src/**/*test.?(c|m)[jt]s?(x)"], 10 | globals: true, 11 | browser: { 12 | enabled: true, 13 | headless: process.env.BROWSER === "safari" ? false : true, 14 | provider: "webdriverio", 15 | name: process.env.BROWSER || "chrome", // browser name is required 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/ts" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | open-pull-requests-limit: 20 13 | -------------------------------------------------------------------------------- /ts/src/txt-en-decoder.ts: -------------------------------------------------------------------------------- 1 | export interface TxtEnDecoder { 2 | encode(str: string): Uint8Array; 3 | decode(data: Uint8Array): string; 4 | } 5 | 6 | const encoder = new TextEncoder(); 7 | const decoder = new TextDecoder(); 8 | 9 | export class Utf8EnDecoder implements TxtEnDecoder { 10 | encode(str: string): Uint8Array { 11 | return encoder.encode(str); 12 | } 13 | decode(data: Uint8Array): string { 14 | return decoder.decode(data); 15 | } 16 | } 17 | 18 | const utf8EnDecoder = new Utf8EnDecoder(); 19 | export function Utf8EnDecoderSingleton(): TxtEnDecoder { 20 | return utf8EnDecoder; 21 | } 22 | -------------------------------------------------------------------------------- /ts/patch-version.cjs: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const ghref = process.env.GITHUB_REF || "a/v0.0.0-smoke"; 3 | // refs/tags/v0.2.30 4 | const lastPart = ghref.split("/").slice(-1)[0]; 5 | let version = "0.0.0-smoke-ci"; 6 | if (lastPart.match(/^v/)) { 7 | version = lastPart.replace(/^v/, ""); 8 | } 9 | const fileToPatch = process.argv[process.argv.length - 1]; 10 | console.error(`Patch ${fileToPatch} version to ${version}(${process.env.GITHUB_REF})`); 11 | const packageJson = JSON.parse(fs.readFileSync(fileToPatch).toString()); 12 | packageJson.version = version; 13 | fs.writeFileSync(fileToPatch, JSON.stringify(packageJson, undefined, 2) + "\n"); 14 | -------------------------------------------------------------------------------- /ts/src/time.ts: -------------------------------------------------------------------------------- 1 | export abstract class Time { 2 | abstract Now(add?: number): Date; 3 | abstract Sleep(duration: Duration): Promise; 4 | TimeSince(start: Date): Duration { 5 | const now = this.Now(); 6 | return now.getTime() - start.getTime(); 7 | } 8 | } 9 | 10 | export type Duration = number; 11 | 12 | export enum TimeUnits { 13 | Microsecond = 1, 14 | // eslint-disable-next-line @typescript-eslint/prefer-literal-enum-member 15 | Second = 1000 * Microsecond, 16 | // eslint-disable-next-line @typescript-eslint/prefer-literal-enum-member 17 | Minute = 60 * Second, 18 | // eslint-disable-next-line @typescript-eslint/prefer-literal-enum-member 19 | Hour = 60 * Minute, 20 | } 21 | -------------------------------------------------------------------------------- /ts/src/file-service.ts: -------------------------------------------------------------------------------- 1 | export interface NamedWritableStream { 2 | readonly name: string; 3 | readonly stream: WritableStream; 4 | } 5 | 6 | export interface FileService { 7 | readonly baseDir: string; 8 | create(fname: string): Promise; 9 | readFileString(fname: string): Promise; 10 | writeFileString(fname: string, content: string): Promise; 11 | 12 | abs(fname: string): string; 13 | 14 | join(...paths: string[]): string; 15 | 16 | relative(from: string, to?: string): string; 17 | 18 | dirname(fname: string): string; 19 | basename(fname: string): string; 20 | 21 | // nodeImport(fname: string): string; 22 | 23 | isAbsolute(fname: string): boolean; 24 | } 25 | -------------------------------------------------------------------------------- /ts/src/future.ts: -------------------------------------------------------------------------------- 1 | export class Future { 2 | readonly #promise: Promise; 3 | #resolveFn: (value: T) => void = () => { 4 | throw new Error("This Promise is not working as expected."); 5 | }; 6 | #rejectFn: (reason: unknown) => void = () => { 7 | throw new Error("This Promise is not working as expected."); 8 | }; 9 | 10 | constructor() { 11 | this.#promise = new Promise((resolve, reject) => { 12 | this.#resolveFn = resolve; 13 | this.#rejectFn = reject; 14 | }); 15 | } 16 | 17 | async asPromise(): Promise { 18 | return this.#promise; 19 | } 20 | 21 | resolve(value: T): void { 22 | this.#resolveFn(value); 23 | } 24 | reject(reason: unknown): void { 25 | this.#rejectFn(reason); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ts/src/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { toCryptoRuntime } from "./crypto.js"; 2 | 3 | it("not extractable import -> export", async () => { 4 | const cp = toCryptoRuntime(); 5 | const key = cp.randomBytes(32); 6 | const x = await cp.importKey("raw", key, "AES-CBC", false, ["encrypt"]); 7 | try { 8 | await cp.exportKey("raw", x); 9 | assert(false, "should not reach here"); 10 | } catch (ie) { 11 | const e = ie as Error; 12 | expect(e.message).toMatch(/(key is not extractable|non-extractable|the underlying object)/i); 13 | } 14 | }); 15 | 16 | it("extractable import -> export", async () => { 17 | const cp = toCryptoRuntime(); 18 | const key = cp.randomBytes(32); 19 | const x = await cp.importKey("raw", key, "AES-CBC", true, ["encrypt"]); 20 | expect(new Uint8Array((await cp.exportKey("raw", x)) as ArrayBuffer)).toEqual(key); 21 | }); 22 | -------------------------------------------------------------------------------- /go/result_test.go: -------------------------------------------------------------------------------- 1 | package cement 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gotest.tools/v3/assert" 8 | ) 9 | 10 | func TestResultOK(t *testing.T) { 11 | result := Ok(1) 12 | assert.Equal(t, result.IsOk(), true) 13 | assert.Equal(t, result.Ok(), 1) 14 | assert.Equal(t, result.Unwrap(), 1) 15 | 16 | assert.Equal(t, result.IsErr(), false) 17 | } 18 | 19 | func TestResultError(t *testing.T) { 20 | result := Err[int](fmt.Errorf("xxx")) 21 | assert.Equal(t, result.IsOk(), false) 22 | assert.Equal(t, result.Err().Error(), "xxx") 23 | assert.Equal(t, result.UnwrapErr().Error(), "xxx") 24 | 25 | assert.Equal(t, result.IsErr(), true) 26 | assert.Equal(t, result.IsErr(), true) 27 | } 28 | 29 | func TestIsResult(t *testing.T) { 30 | assert.Equal(t, Is[int](Ok(1)), true) 31 | assert.Equal(t, Is[int](Err[int]("xxx")), true) 32 | assert.Equal(t, Is[int](44), false) 33 | } 34 | -------------------------------------------------------------------------------- /ts/prepare-pubdir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bashi 2 | 3 | pnpm run build 4 | rm -rf pubdir 5 | mkdir -p pubdir 6 | 7 | cp -pr ../.gitignore ../README.md ../LICENSE dist/ts pubdir/ 8 | 9 | (cd dist/pkg && cp -pr . ../../pubdir/) 10 | (cd src/ && cp -pr . ../pubdir/src/) 11 | cp package.json pubdir/ 12 | cp ../README.md ../LICENSE pubdir/src/ 13 | cp ./jsr.json ./pubdir/src/ 14 | (cd pubdir/src && rm -f test/test-exit-handler.* ./utils/stream-test-helper.ts) 15 | find pubdir/src -name __screenshots__ -print | xargs rm -rf thisIsnotFound 16 | find pubdir/src -name "*.test.ts" -print | xargs rm -f thisIsnotFound 17 | 18 | node ./patch-version.cjs ./pubdir/package.json 19 | node ./patch-version.cjs ./pubdir/src/jsr.json 20 | 21 | node ./setup-jsr-json.cjs ./pubdir/src/jsr.json 22 | 23 | (cd pubdir && pnpm pack) 24 | 25 | (cd pubdir/src && deno publish --dry-run --unstable-sloppy-imports --allow-dirty) 26 | -------------------------------------------------------------------------------- /ts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./file-service.js"; 2 | export * from "./logger-impl.js"; 3 | export * from "./logger.js"; 4 | export * from "./sys-abstraction.js"; 5 | export * from "./sys-env.js"; 6 | export * from "./time.js"; 7 | export * from "./test/index.js"; 8 | export * from "./txt-en-decoder.js"; 9 | export * from "./log-level-impl.js"; 10 | export * from "./result.js"; 11 | export * from "./option.js"; 12 | export * from "./future.js"; 13 | export * from "./tracer.js"; 14 | export * from "./resolve-once.js"; 15 | export * from "./runtime.js"; 16 | export * from "./uri.js"; 17 | export * from "./crypto.js"; 18 | export * from "./base-sys-abstraction.js"; 19 | export * from "./bin2text.js"; 20 | export * from "./version.js"; 21 | export * from "./http_header.js"; 22 | export * as utils from "./utils/index.js"; 23 | // ugly 24 | import * as utils from "./utils/index.js"; 25 | export const param = utils.param; 26 | -------------------------------------------------------------------------------- /ts/src/node/mock-file-service.test.ts: -------------------------------------------------------------------------------- 1 | import { runtimeFn } from "@adviser/cement"; 2 | import { MockFileService } from "@adviser/cement/node"; 3 | 4 | describe("MockFileService", () => { 5 | if (runtimeFn().isNodeIsh || runtimeFn().isDeno) { 6 | let MFS: MockFileService; 7 | beforeAll(async () => { 8 | const { MockFileService } = await import("./mock-file-service.js"); 9 | MFS = new MockFileService(); 10 | }); 11 | it("writeFileString", async () => { 12 | const f = MFS; 13 | const absFname = f.abs("test"); 14 | await f.writeFileString("test", "hello"); 15 | expect(f.files).toEqual({ 16 | [absFname]: { 17 | content: "hello", 18 | name: absFname, 19 | }, 20 | test: { 21 | name: absFname, 22 | content: "hello", 23 | }, 24 | }); 25 | }); 26 | } else { 27 | it.skip("nothing in browser", () => { 28 | expect(1).toEqual(1); 29 | }); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /go/optional.go: -------------------------------------------------------------------------------- 1 | package cement 2 | 3 | type Optional[T any] struct{ t *T } 4 | 5 | func (o Optional[T]) IsNone() bool { 6 | return o.t == nil 7 | } 8 | 9 | func (o Optional[T]) IsSome() bool { 10 | return o.t != nil 11 | } 12 | 13 | func (o Optional[T]) Value() T { 14 | return *o.t 15 | } 16 | 17 | func (o Optional[T]) Ref() *T { 18 | return o.t 19 | } 20 | 21 | func OptionalToPtr[T any](t Optional[T]) *T { 22 | if t.IsNone() { 23 | return nil 24 | } 25 | return t.Ref() 26 | } 27 | 28 | func OptionalFromPtr[T any](t *T) Optional[T] { 29 | if t == nil { 30 | return None[T]() 31 | } 32 | return Some[T](*t) 33 | } 34 | 35 | func None[T any]() Optional[T] { 36 | return Optional[T]{ 37 | t: nil, 38 | } 39 | } 40 | 41 | // type some[T any] struct { 42 | // t T 43 | // } 44 | 45 | func Some[T any](t T) Optional[T] { 46 | return Optional[T]{t: &t} 47 | } 48 | 49 | // func (o some[T]) IsNone() bool { 50 | // return false 51 | // } 52 | 53 | // func (o some[T]) Value() T { 54 | // return o.t 55 | // } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cement 2 | 3 | An Agnostic Platform Wrapper 4 | 5 | which Provides: 6 | 7 | - SysAbstraction 8 | ```typescript 9 | export interface SysAbstraction { 10 | Time(): Time; 11 | Stdout(): WritableStream; 12 | Stderr(): WritableStream; 13 | NextId(): string; 14 | Random0ToValue(value: number): number; 15 | System(): SystemService; 16 | FileSystem(): FileService; 17 | } 18 | ``` 19 | - Logger inspired from golang zlogger 20 | ```typescript 21 | export interface LoggerInterface { 22 | Module(key: string): R; 23 | SetDebug(...modules: (string | string[])[]): R; 24 | 25 | Str(key: string, value: string): R; 26 | Error(): R; 27 | Warn(): R; 28 | Debug(): R; 29 | Log(): R; 30 | WithLevel(level: Level): R; 31 | 32 | Err(err: unknown): R; // could be Error, or something which coerces to string 33 | Info(): R; 34 | Timestamp(): R; 35 | Any(key: string, value: unknown): R; 36 | Dur(key: string, nsec: number): R; 37 | Uint64(key: string, value: number): R; 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /ts/eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | export default tseslint.config( 5 | eslint.configs.recommended, 6 | // ...tseslint.configs.recommended, 7 | tseslint.configs.recommendedTypeChecked, 8 | { 9 | languageOptions: { 10 | parserOptions: { 11 | projectService: true, 12 | tsconfigRootDir: import.meta.dirname, 13 | }, 14 | }, 15 | }, 16 | ...tseslint.configs.strict, 17 | ...tseslint.configs.stylistic, 18 | { 19 | ignores: [ 20 | "eslint.config.js", 21 | "jest.config.js", 22 | "**/dist/", 23 | "**/pubdir/", 24 | "**/node_modules/", 25 | "patch-version.cjs", 26 | "setup-jsr-json.cjs", 27 | "lib.deno.d.ts", 28 | ], 29 | }, 30 | { 31 | rules: { 32 | "no-console": ["warn"], 33 | "@typescript-eslint/explicit-function-return-type": "error", 34 | "@typescript-eslint/no-inferrable-types": "error", 35 | "@typescript-eslint/no-deprecated": "error", 36 | }, 37 | }, 38 | ); 39 | -------------------------------------------------------------------------------- /ts/src/future.test.ts: -------------------------------------------------------------------------------- 1 | import { Future } from "@adviser/cement"; 2 | 3 | it("Create Future Happy", async () => { 4 | const future = new Future(); 5 | const promise = future.asPromise(); 6 | expect(promise).toBeInstanceOf(Promise); 7 | 8 | const asynced = new Promise((resolve, reject) => { 9 | promise.then(resolve).catch(reject); 10 | }); 11 | future.resolve("Hello World"); 12 | future.resolve("1 Ignores"); 13 | future.resolve("2 Ignores"); 14 | 15 | expect(await asynced).toBe("Hello World"); 16 | }); 17 | 18 | it("Create Future Sad", async () => { 19 | const future = new Future(); 20 | const promise = future.asPromise(); 21 | expect(promise).toBeInstanceOf(Promise); 22 | 23 | const asynced = new Promise((resolve, reject) => { 24 | promise.then(resolve).catch(reject); 25 | }); 26 | future.reject("Sad World"); 27 | future.reject("1 Ignores"); 28 | future.reject("2 Ignores"); 29 | 30 | try { 31 | await asynced; 32 | expect("Why").toBe("Sad World"); 33 | } catch (error) { 34 | expect(error).toBe("Sad World"); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /ts/smoke/smoke.ts: -------------------------------------------------------------------------------- 1 | import { WebSysAbstraction } from "@adviser/cement/web"; 2 | import { NodeSysAbstraction } from "@adviser/cement/node"; 3 | import { DenoSysAbstraction } from "@adviser/cement/deno"; 4 | import { VERSION, LoggerImpl, Result, Option, Level, runtimeFn } from "@adviser/cement"; 5 | 6 | function main(): void { 7 | const none = Option.None(); 8 | const result = Result.Ok(none); 9 | if (!result.isOk()) { 10 | throw new Error("Result is Err"); 11 | } 12 | const log = new LoggerImpl() 13 | .EnableLevel(Level.DEBUG) 14 | .With() 15 | .Str("runtime", globalThis.Deno ? "Deno" : "Node") 16 | .Str("version", VERSION) 17 | .Str("runtime-version", !globalThis.Deno ? process?.version : globalThis.Deno.version.deno) 18 | .Logger(); 19 | const rt = runtimeFn(); 20 | if (rt.isNodeIsh) { 21 | const sys = NodeSysAbstraction(); 22 | log.Info().Str("id", sys.NextId()).Msg("Node-Alright"); 23 | } 24 | if (rt.isDeno) { 25 | const sys = DenoSysAbstraction(); 26 | log.Info().Str("id", sys.NextId()).Msg("Node-Alright"); 27 | } 28 | { 29 | const sys = WebSysAbstraction(); 30 | log.Info().Str("id", sys.NextId()).Msg("Web-Alright"); 31 | } 32 | } 33 | main(); 34 | -------------------------------------------------------------------------------- /ts/src/node/mock-file-service.ts: -------------------------------------------------------------------------------- 1 | import { NamedWritableStream } from "../file-service.js"; 2 | import { NodeFileService } from "./node-file-service.js"; 3 | 4 | export interface FileCollector { 5 | readonly name: string; 6 | content: string; 7 | } 8 | 9 | export class MockFileService extends NodeFileService { 10 | readonly files = {} as Record; 11 | 12 | // override abs(fname: string): string { 13 | // return this.join("/mock/", fname); 14 | // } 15 | 16 | override create(fname: string): Promise { 17 | let oName = fname; 18 | if (!this.isAbsolute(fname)) { 19 | oName = this.abs(fname); 20 | } 21 | 22 | const fc = { 23 | name: oName, 24 | content: "", 25 | }; 26 | this.files[oName] = fc; 27 | this.files[fname] = fc; 28 | const decoder = new TextDecoder(); 29 | 30 | return Promise.resolve({ 31 | name: oName, 32 | stream: new WritableStream({ 33 | write(chunk): void { 34 | fc.content = fc.content + decoder.decode(chunk); 35 | }, 36 | close(): void { 37 | // do nothing 38 | }, 39 | abort(): void { 40 | throw new Error("not implemented"); 41 | }, 42 | }), 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ts/src/utils/fanout-write-stream.ts: -------------------------------------------------------------------------------- 1 | export class FanoutWriteStream implements WritableStreamDefaultWriter { 2 | readonly _writers: WritableStreamDefaultWriter[]; 3 | readonly ready: Promise; 4 | readonly closed: Promise; 5 | readonly desiredSize: number | null = null; 6 | constructor(writers: WritableStreamDefaultWriter[]) { 7 | this._writers = writers; 8 | this.ready = Promise.all(this._writers.map((w) => w.ready)).then(() => undefined); 9 | this.closed = Promise.all(this._writers.map((w) => w.closed)).then(() => undefined); 10 | } 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | abort(reason?: any): Promise { 14 | return Promise.all(this._writers.map((w) => w.abort(reason))).then(() => { 15 | /* do nothing */ 16 | }); 17 | } 18 | close(): Promise { 19 | return Promise.all(this._writers.map((w) => w.close())).then(() => { 20 | /* do nothing */ 21 | }); 22 | } 23 | releaseLock(): void { 24 | this._writers.map((w) => w.releaseLock()); 25 | } 26 | 27 | write(chunk?: Uint8Array): Promise { 28 | return Promise.all(this._writers.map((w) => w.write(chunk))).then(() => { 29 | /* do nothing */ 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ts/src/web/web-env-actions.ts: -------------------------------------------------------------------------------- 1 | import { ResolveOnce } from "../resolve-once.js"; 2 | import { EnvActions, EnvFactoryOpts, Env } from "../sys-env.js"; 3 | 4 | const once = new ResolveOnce(); 5 | export class BrowserEnvActions implements EnvActions { 6 | readonly env: Map = new Map(); 7 | readonly opts: Partial; 8 | 9 | static new(opts: Partial): EnvActions { 10 | return once.once(() => new BrowserEnvActions(opts)); 11 | } 12 | 13 | private constructor(opts: Partial) { 14 | this.opts = opts; 15 | } 16 | 17 | get(key: string): string | undefined { 18 | return this.env.get(key); 19 | } 20 | set(key: string, value?: string): void { 21 | if (value) { 22 | this.env.set(key, value); 23 | } 24 | } 25 | delete(key: string): void { 26 | this.env.delete(key); 27 | } 28 | keys(): string[] { 29 | return Array.from(this.env.keys()); 30 | } 31 | active(): boolean { 32 | return true; // that should work on every runtime 33 | } 34 | 35 | register(env: Env): Env { 36 | const sym = Symbol.for(this.opts.symbol || "CP_ENV"); 37 | const browser = globalThis as unknown as Record; 38 | browser[sym] = env; 39 | return env; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ts/src/sys-abstraction.ts: -------------------------------------------------------------------------------- 1 | import { FileService } from "./file-service.js"; 2 | import { Env } from "./sys-env.js"; 3 | import { Time } from "./time.js"; 4 | 5 | export enum TimeMode { 6 | REAL = "real", 7 | CONST = "const", 8 | STEP = "step", 9 | } 10 | 11 | export enum RandomMode { 12 | CONST = "const", 13 | STEP = "step", 14 | RANDOM = "random", 15 | } 16 | 17 | export enum IDMode { 18 | UUID = "uuid", 19 | CONST = "const", 20 | STEP = "step", 21 | } 22 | 23 | export function String2TimeMode(s?: string): TimeMode { 24 | switch (s?.toLowerCase()) { 25 | case "real": 26 | return TimeMode.REAL; 27 | case "const": 28 | return TimeMode.CONST; 29 | case "step": 30 | return TimeMode.STEP; 31 | default: 32 | return TimeMode.REAL; 33 | } 34 | } 35 | 36 | export type VoidFunc = () => void | Promise; 37 | 38 | export interface SystemService { 39 | Env(): Env; 40 | Args(): string[]; 41 | OnExit(hdl: VoidFunc): VoidFunc; 42 | Exit(code: number): void; 43 | } 44 | 45 | export interface SysAbstraction { 46 | Time(): Time; 47 | Stdout(): WritableStream; 48 | Stderr(): WritableStream; 49 | NextId(): string; 50 | Random0ToValue(value: number): number; 51 | System(): SystemService; 52 | FileSystem(): FileService; 53 | } 54 | -------------------------------------------------------------------------------- /ts/src/utils/stream2string.test.ts: -------------------------------------------------------------------------------- 1 | import { stream2string } from "./stream2string.js"; 2 | 3 | it("stream2string", async () => { 4 | expect( 5 | await stream2string( 6 | new ReadableStream({ 7 | start(controller): void { 8 | const encoder = new TextEncoder(); 9 | controller.enqueue(encoder.encode("Hello")); 10 | controller.enqueue(encoder.encode(" ")); 11 | controller.enqueue(encoder.encode("World")); 12 | controller.enqueue(encoder.encode("!")); 13 | controller.close(); 14 | }, 15 | }), 16 | ), 17 | ).toBe("Hello World!"); 18 | }); 19 | 20 | it("stream2string maxSize", async () => { 21 | const instr = "Hello World!"; 22 | for (let i = 0; i < instr.length; i++) { 23 | expect( 24 | await stream2string( 25 | new ReadableStream({ 26 | start(controller): void { 27 | const encoder = new TextEncoder(); 28 | controller.enqueue(encoder.encode("Hello")); 29 | controller.enqueue(encoder.encode(" ")); 30 | controller.enqueue(encoder.encode("World")); 31 | controller.enqueue(encoder.encode("!")); 32 | controller.close(); 33 | }, 34 | }), 35 | i, 36 | ), 37 | ).toBe(instr.slice(0, i)); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /ts/src/test/mock-logger.ts: -------------------------------------------------------------------------------- 1 | import { LevelHandlerImpl } from "../log-level-impl.js"; 2 | import { Logger } from "../logger.js"; 3 | import { LoggerImpl } from "../logger-impl.js"; 4 | import { SysAbstraction } from "../sys-abstraction.js"; 5 | import { LogCollector } from "./log-write-stream.js"; 6 | 7 | export interface MockLoggerReturn { 8 | readonly logger: Logger; 9 | readonly logCollector: LogCollector; 10 | } 11 | 12 | export function MockLogger(params?: { 13 | readonly sys?: SysAbstraction; 14 | readonly pass?: WritableStreamDefaultWriter; 15 | moduleName?: string | string[]; 16 | readonly disableDebug?: boolean; 17 | }): MockLoggerReturn { 18 | const lc = new LogCollector(params?.pass); 19 | let modNames = ["MockLogger"]; 20 | if (typeof params?.moduleName === "string") { 21 | modNames = [params?.moduleName]; 22 | } else if (Array.isArray(params?.moduleName)) { 23 | modNames = [...params.moduleName, ...modNames]; 24 | } 25 | const logger = new LoggerImpl({ 26 | out: lc, 27 | sys: params?.sys, 28 | levelHandler: new LevelHandlerImpl(), 29 | }) 30 | .With() 31 | .Module(modNames[0]) 32 | .Logger(); 33 | if (!params?.disableDebug) { 34 | logger.SetDebug(...modNames); 35 | } 36 | return { 37 | logCollector: lc, 38 | logger, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /ts/src/option.ts: -------------------------------------------------------------------------------- 1 | export abstract class Option { 2 | static Some(t: T): Option { 3 | return new Some(t); 4 | } 5 | 6 | static None(): Option { 7 | return new None(); 8 | } 9 | 10 | static Is(t: unknown): t is Option { 11 | return t instanceof Option; 12 | } 13 | 14 | static From(t?: T): Option { 15 | if (!t) { 16 | return new None(); 17 | } 18 | return new Some(t); 19 | } 20 | 21 | IsNone(): boolean { 22 | return this.is_none(); 23 | } 24 | 25 | IsSome(): boolean { 26 | return this.is_some(); 27 | } 28 | Unwrap(): T { 29 | return this.unwrap(); 30 | } 31 | 32 | abstract is_none(): boolean; 33 | abstract is_some(): boolean; 34 | abstract unwrap(): T; 35 | } 36 | 37 | export class Some extends Option { 38 | private _t: T; 39 | constructor(_t: T) { 40 | super(); 41 | this._t = _t; 42 | } 43 | 44 | is_none(): boolean { 45 | return false; 46 | } 47 | is_some(): boolean { 48 | return true; 49 | } 50 | unwrap(): T { 51 | return this._t; 52 | } 53 | } 54 | 55 | export class None extends Option { 56 | is_none(): boolean { 57 | return true; 58 | } 59 | is_some(): boolean { 60 | return false; 61 | } 62 | unwrap(): T { 63 | throw new Error("None.unwrap"); 64 | } 65 | } 66 | 67 | export type WithoutOption = T extends Option ? U : T; 68 | -------------------------------------------------------------------------------- /ts/src/cf/cf-sys-abstraction.ts: -------------------------------------------------------------------------------- 1 | import { BaseSysAbstraction, WrapperSysAbstraction, WrapperSysAbstractionParams } from "../base-sys-abstraction.js"; 2 | import { SysAbstraction, SystemService, VoidFunc } from "../sys-abstraction.js"; 3 | import { Env, envFactory } from "../sys-env.js"; 4 | import { Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 5 | import { WebFileService } from "../web/web-sys-abstraction.js"; 6 | 7 | export class CFSystemService implements SystemService { 8 | Env(): Env { 9 | return envFactory(); 10 | } 11 | Args(): string[] { 12 | throw new Error("Args-Method not implemented."); 13 | } 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | OnExit(hdl: VoidFunc): VoidFunc { 16 | throw new Error("OnExit-Method not implemented."); 17 | } 18 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 19 | Exit(code: number): void { 20 | throw new Error("Exit-Method not implemented."); 21 | } 22 | } 23 | 24 | let my: BaseSysAbstraction | undefined = undefined; 25 | export function CFSysAbstraction(param?: WrapperSysAbstractionParams): SysAbstraction { 26 | if (!my) { 27 | my = new BaseSysAbstraction({ 28 | TxtEnDecoder: param?.TxtEnDecoder || Utf8EnDecoderSingleton(), 29 | FileSystem: new WebFileService(), 30 | SystemService: new CFSystemService(), 31 | }); 32 | } 33 | return new WrapperSysAbstraction(my, param); 34 | } 35 | -------------------------------------------------------------------------------- /ts/src/bin2text.ts: -------------------------------------------------------------------------------- 1 | export function bin2text(hex: ArrayBufferView, lineFn: (line: string) => void, size = 0): void { 2 | const arr = new Uint8Array(hex.buffer, hex.byteOffset, hex.byteLength); 3 | let cutted = " "; 4 | if (size == 0) { 5 | size = arr.length; 6 | } 7 | size = Math.min(size, arr.length); 8 | const cols = 16; 9 | for (let line = 0; line < size; line += cols) { 10 | if (line + cols <= size || arr.length == size) { 11 | // normal line 12 | } else { 13 | line = arr.length - (arr.length % cols); 14 | size = arr.length; 15 | cutted = ">>"; 16 | } 17 | const l: string[] = [line.toString(16).padStart(4, "0"), cutted]; 18 | for (let col = 0; col < cols; col++) { 19 | if (line + col < size) { 20 | l.push(arr[line + col].toString(16).padStart(2, "0")); 21 | } else { 22 | l.push(" "); 23 | } 24 | // l.push((col > 0 && col % 4 === 3) ? " " : " "); 25 | l.push(" "); 26 | } 27 | for (let col = 0; col < cols; col++) { 28 | if (line + col < size) { 29 | const ch = arr[line + col]; 30 | l.push(ch >= 32 && ch < 127 ? String.fromCharCode(ch) : "."); 31 | } 32 | } 33 | lineFn(l.join("")); 34 | } 35 | } 36 | 37 | export function bin2string(hex: ArrayBufferView, size = 0): string { 38 | const collector: string[] = []; 39 | bin2text( 40 | hex, 41 | (line) => { 42 | collector.push(line); 43 | }, 44 | size, 45 | ); 46 | return collector.join("\n"); 47 | } 48 | -------------------------------------------------------------------------------- /ts/src/utils/stream2string.ts: -------------------------------------------------------------------------------- 1 | export async function stream2string(stream?: ReadableStream | null, maxSize?: number): Promise { 2 | if (!stream) { 3 | return Promise.resolve(""); 4 | } 5 | const reader = stream.getReader(); 6 | let res = ""; 7 | const decoder = new TextDecoder(); 8 | let rSize = 0; 9 | while (typeof maxSize === "undefined" || rSize < maxSize) { 10 | try { 11 | const read = await reader.read(); 12 | if (read.done) { 13 | break; 14 | } 15 | if (maxSize && rSize + read.value.length > maxSize) { 16 | read.value = read.value.slice(0, maxSize - rSize); 17 | } 18 | const block = decoder.decode(read.value, { stream: true }); 19 | rSize += read.value.length; 20 | res += block; 21 | } catch (err) { 22 | return Promise.reject(err as Error); 23 | } 24 | } 25 | return Promise.resolve(res); 26 | } 27 | 28 | export async function stream2uint8array(stream?: ReadableStream | null): Promise { 29 | if (!stream) { 30 | return Promise.resolve(new Uint8Array()); 31 | } 32 | const reader = stream.getReader(); 33 | let res = new Uint8Array(); 34 | // eslint-disable-next-line no-constant-condition 35 | while (1) { 36 | try { 37 | const { done, value } = await reader.read(); 38 | if (done) { 39 | break; 40 | } 41 | res = new Uint8Array([...res, ...value]); 42 | } catch (err) { 43 | return Promise.reject(err as Error); 44 | } 45 | } 46 | return Promise.resolve(res); 47 | } 48 | -------------------------------------------------------------------------------- /ts/src/node/node-env-actions.ts: -------------------------------------------------------------------------------- 1 | import { ResolveOnce } from "../resolve-once.js"; 2 | import { runtimeFn } from "../runtime.js"; 3 | import { Env, EnvActions, EnvFactoryOpts } from "../sys-env.js"; 4 | 5 | const once = new ResolveOnce(); 6 | export class NodeEnvActions implements EnvActions { 7 | readonly #node = globalThis as unknown as { process: { env: Record } }; 8 | 9 | static new(opts: Partial): EnvActions { 10 | return once.once(() => new NodeEnvActions(opts)); 11 | } 12 | 13 | readonly opts: Partial; 14 | private constructor(opts: Partial) { 15 | this.opts = opts; 16 | } 17 | 18 | register(env: Env): Env { 19 | for (const key of env.keys()) { 20 | this._env[key] = env.get(key) || ""; 21 | } 22 | return env; 23 | } 24 | 25 | active(): boolean { 26 | return runtimeFn().isNodeIsh; 27 | // typeof this.#node === "object" && typeof this.#node.process === "object" && typeof this.#node.process.env === "object"; 28 | } 29 | readonly _env: Record = this.active() ? this.#node.process.env : {}; 30 | keys(): string[] { 31 | return Object.keys(this._env); 32 | } 33 | get(key: string): string | undefined { 34 | return this._env[key]; 35 | } 36 | set(key: string, value?: string): void { 37 | if (value) { 38 | this._env[key] = value; 39 | } 40 | } 41 | delete(key: string): void { 42 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 43 | delete this._env[key]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ts/src/runtime.ts: -------------------------------------------------------------------------------- 1 | export interface Runtime { 2 | isNodeIsh: boolean; 3 | isBrowser: boolean; 4 | isDeno: boolean; 5 | isReactNative: boolean; 6 | isCFWorker: boolean; 7 | } 8 | 9 | function isSet(value: string, ref: Record = globalThis): boolean { 10 | const [head, ...tail] = value.split("."); 11 | if (["object", "function"].includes(typeof ref) && ref && ["object", "function"].includes(typeof ref[head]) && ref[head]) { 12 | if (tail.length <= 1) { 13 | return true; 14 | } 15 | return isSet(tail.join("."), ref[head] as Record); 16 | } 17 | return false; 18 | } 19 | 20 | // caches.default or WebSocketPair 21 | 22 | export function runtimeFn(): Runtime { 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | const gt: any = globalThis; 25 | let isReactNative = 26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 27 | isSet("navigator.product") && typeof gt["navigator"] === "object" && gt["navigator"]["product"] === "ReactNative"; 28 | let isNodeIsh = false; 29 | if (!isSet("Deno")) { 30 | isNodeIsh = isSet("process.versions.node") && !isReactNative; 31 | } 32 | let isDeno = isSet("Deno"); 33 | const isCFWorker = isSet("caches.default") && isSet("WebSocketPair"); 34 | if (isCFWorker) { 35 | isDeno = false; 36 | isNodeIsh = false; 37 | isReactNative = false; 38 | } 39 | return { 40 | isNodeIsh, 41 | isBrowser: !(isNodeIsh || isDeno || isCFWorker || isReactNative), 42 | isDeno, 43 | isReactNative, 44 | isCFWorker, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /ts/src/deno/deno-env-actions.ts: -------------------------------------------------------------------------------- 1 | import { ResolveOnce } from "../resolve-once.js"; 2 | import { runtimeFn } from "../runtime.js"; 3 | import { Env, EnvActions, EnvFactoryOpts } from "../sys-env.js"; 4 | 5 | interface DenoEnv { 6 | get: (key: string) => string | undefined; 7 | toObject: () => Record; 8 | set: (key: string, value: string) => void; 9 | has: (key: string) => boolean; 10 | delete: (key: string) => void; 11 | } 12 | 13 | const once = new ResolveOnce(); 14 | export class DenoEnvActions implements EnvActions { 15 | readonly #deno = globalThis as unknown as { 16 | Deno: { 17 | env: DenoEnv; 18 | }; 19 | }; 20 | 21 | static new(opts: Partial): EnvActions { 22 | return once.once(() => new DenoEnvActions(opts)); 23 | } 24 | 25 | get _env(): DenoEnv { 26 | return this.#deno.Deno.env; 27 | } 28 | 29 | readonly opts: Partial; 30 | private constructor(opts: Partial) { 31 | this.opts = opts; 32 | } 33 | 34 | register(env: Env): Env { 35 | for (const key of env.keys()) { 36 | this._env.set(key, env.get(key) || ""); 37 | } 38 | return env; 39 | } 40 | active(): boolean { 41 | return runtimeFn().isDeno; 42 | } 43 | keys(): string[] { 44 | return Object.keys(this._env.toObject()); 45 | } 46 | get(key: string): string | undefined { 47 | return this._env.get(key); 48 | } 49 | set(key: string, value?: string): void { 50 | if (value) { 51 | this._env.set(key, value); 52 | } 53 | } 54 | delete(key: string): void { 55 | this._env.delete(key); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /go/result.go: -------------------------------------------------------------------------------- 1 | package cement 2 | 3 | import "fmt" 4 | 5 | type Result[T any] interface { 6 | IsOk() bool 7 | IsErr() bool 8 | Err() error 9 | Ok() T 10 | Unwrap() T 11 | UnwrapErr() error 12 | } 13 | 14 | type ResultOK[T any] struct { 15 | t T 16 | } 17 | 18 | func (r ResultOK[T]) Unwrap() T { 19 | return r.Ok() 20 | } 21 | 22 | func (r ResultOK[T]) UnwrapErr() error { 23 | return r.Err() 24 | } 25 | 26 | func (r ResultOK[T]) IsOk() bool { 27 | return true 28 | } 29 | func (r ResultOK[T]) IsErr() bool { 30 | return !r.IsOk() 31 | } 32 | 33 | func (r ResultOK[T]) Err() error { 34 | panic("Result is Ok") 35 | } 36 | func (r ResultOK[T]) Ok() T { 37 | return r.t 38 | } 39 | 40 | type ResultError[T any] struct { 41 | t error 42 | } 43 | 44 | func (r ResultError[T]) IsOk() bool { 45 | return false 46 | } 47 | func (r ResultError[T]) IsErr() bool { 48 | return !r.IsOk() 49 | } 50 | 51 | func (r ResultError[T]) Ok() T { 52 | panic(fmt.Errorf("Result is Err:%v", r.t.Error())) 53 | } 54 | 55 | func (r ResultError[T]) Unwrap() T { 56 | return r.Ok() 57 | } 58 | 59 | func (r ResultError[T]) UnwrapErr() error { 60 | return r.Err() 61 | } 62 | 63 | func (r ResultError[T]) Err() error { 64 | return r.t 65 | } 66 | 67 | func Ok[T any](t T) Result[T] { 68 | return ResultOK[T]{ 69 | t: t, 70 | } 71 | } 72 | 73 | func Err[T any](t any) Result[T] { 74 | e, ok := t.(error) 75 | if ok { 76 | return ResultError[T]{t: e} 77 | } 78 | s, ok := t.(string) 79 | if ok { 80 | return ResultError[T]{t: fmt.Errorf(s)} 81 | } 82 | panic("Err must be error or string") 83 | } 84 | 85 | func Is[T any](t any) bool { 86 | switch t.(type) { 87 | case ResultOK[T], ResultError[T]: 88 | return true 89 | default: 90 | return false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ts/src/cf/cf-env-actions.ts: -------------------------------------------------------------------------------- 1 | import { ResolveOnce } from "../resolve-once.js"; 2 | import { runtimeFn } from "../runtime.js"; 3 | import { EnvActions, EnvImpl, EnvFactoryOpts, Env } from "../sys-env.js"; 4 | 5 | const once = new ResolveOnce(); 6 | export class CFEnvActions implements EnvActions { 7 | readonly injectOnRegister: Record = {}; 8 | readonly cfEnv: Map; 9 | env?: EnvImpl; 10 | static new(opts: Partial): EnvActions { 11 | return once.once(() => new CFEnvActions(opts)); 12 | } 13 | static inject(o: Record): void { 14 | const env = CFEnvActions.new({}) as CFEnvActions; 15 | for (const key in o) { 16 | const value = o[key]; 17 | if (typeof value === "string") { 18 | if (env.env) { 19 | env.env.set(key, value); 20 | } else { 21 | env.injectOnRegister[key] = value; 22 | } 23 | } 24 | } 25 | } 26 | private constructor(env: Partial) { 27 | this.cfEnv = new Map(Object.entries(env.presetEnv || {})); 28 | } 29 | active(): boolean { 30 | return runtimeFn().isCFWorker; 31 | } 32 | register(env: Env): Env { 33 | this.env = env as EnvImpl; 34 | for (const key in this.injectOnRegister) { 35 | env.set(key, this.injectOnRegister[key]); 36 | } 37 | return env; 38 | } 39 | get(key: string): string | undefined { 40 | return this.cfEnv.get(key); 41 | } 42 | set(key: string, value?: string): void { 43 | if (value) { 44 | this.cfEnv.set(key, value); 45 | } 46 | } 47 | delete(key: string): void { 48 | this.cfEnv.delete(key); 49 | } 50 | keys(): string[] { 51 | return Array.from(this.cfEnv.keys()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ts/src/utils/stream-test-helper.ts: -------------------------------------------------------------------------------- 1 | import type { Mock } from "vitest"; 2 | 3 | interface mockValue { 4 | done: boolean; 5 | value: Uint8Array | undefined; 6 | fillCalls: number; 7 | reBufferCalls: number; 8 | } 9 | export interface streamingTestState { 10 | readonly sendChunks: number; 11 | readonly sendChunkSize: number; 12 | fillCalls: number; 13 | CollectorFn: Mock<(mv: mockValue) => void>; 14 | } 15 | 16 | export async function receiveFromStream(reb: ReadableStream, state: streamingTestState): Promise { 17 | return new Promise((resolve, reject) => { 18 | let reBufferCalls = 0; 19 | const reader = reb.getReader(); 20 | function pump(): void { 21 | reader 22 | .read() 23 | .then(({ done, value }) => { 24 | state.CollectorFn({ done, value, fillCalls: state.fillCalls, reBufferCalls }); 25 | reBufferCalls++; 26 | if (done) { 27 | resolve(); 28 | return; 29 | } 30 | pump(); 31 | }) 32 | .catch(reject); 33 | } 34 | pump(); 35 | }); 36 | } 37 | 38 | export async function sendToStream(reb: WritableStream, state: streamingTestState): Promise { 39 | return new Promise((resolve, reject) => { 40 | const writer = reb.getWriter(); 41 | function pump(i: number): void { 42 | if (i >= state.sendChunks) { 43 | writer.close().then(resolve).catch(reject); 44 | return; 45 | } 46 | writer.ready 47 | .then(() => { 48 | state.fillCalls++; 49 | writer 50 | .write(new Uint8Array(Array(state.sendChunkSize).fill(i))) 51 | .then(() => { 52 | pump(i + 1); 53 | }) 54 | .catch(reject); 55 | }) 56 | .catch(reject); 57 | } 58 | pump(0); 59 | }); 60 | } 61 | 62 | // it("does nothing", () => {}); 63 | -------------------------------------------------------------------------------- /ts/src/utils/stream-map.ts: -------------------------------------------------------------------------------- 1 | export interface StreamMap { 2 | Map(s: T, idx: number): U | Promise; 3 | readonly Close?: () => void; 4 | } 5 | export function streamMap(s: ReadableStream, sm: StreamMap): ReadableStream { 6 | const state = { reader: s.getReader(), streamMap: sm, idx: 0 }; 7 | return new ReadableStream({ 8 | async pull(controller): Promise { 9 | const { done, value } = await state.reader.read(); 10 | if (done) { 11 | if (state.streamMap.Close) { 12 | state.streamMap.Close(); 13 | } 14 | controller.close(); 15 | return; 16 | } 17 | const promiseOrU = state.streamMap.Map(value, state.idx++); 18 | let mapped: U; 19 | if (promiseOrU instanceof Promise || typeof (promiseOrU as { then: () => void }).then === "function") { 20 | mapped = await promiseOrU; 21 | } else { 22 | mapped = promiseOrU; 23 | } 24 | controller.enqueue(mapped); 25 | }, 26 | }); 27 | } 28 | 29 | export async function devnull(a: ReadableStream): Promise { 30 | const reader = a.getReader(); 31 | let cnt = 0; 32 | while (true) { 33 | const { done } = await reader.read(); 34 | if (done) { 35 | break; 36 | } 37 | cnt++; 38 | } 39 | return cnt; 40 | } 41 | 42 | export function array2stream(a: T[]): ReadableStream { 43 | let i = 0; 44 | return new ReadableStream({ 45 | pull(controller): void { 46 | if (i >= a.length) { 47 | controller.close(); 48 | return; 49 | } 50 | controller.enqueue(a[i]); 51 | i++; 52 | }, 53 | }); 54 | } 55 | 56 | export async function stream2array(a: ReadableStream): Promise { 57 | const ret: T[] = []; 58 | const reader = a.getReader(); 59 | while (true) { 60 | const { done, value } = await reader.read(); 61 | if (done) { 62 | break; 63 | } 64 | ret.push(value); 65 | } 66 | return ret; 67 | } 68 | -------------------------------------------------------------------------------- /ts/src/log-writer-impl.ts: -------------------------------------------------------------------------------- 1 | export class LogWriterStream { 2 | readonly _out: WritableStream; 3 | readonly _toFlush: (() => Promise)[] = []; 4 | 5 | constructor(out: WritableStream) { 6 | this._out = out; 7 | } 8 | 9 | write(encoded: Uint8Array): void { 10 | const my = async (): Promise => { 11 | // const val = Math.random(); 12 | // console.log(">>>My:", encoded) 13 | try { 14 | const writer = this._out.getWriter(); 15 | await writer.ready; 16 | await writer.write(encoded); 17 | writer.releaseLock(); 18 | } catch (err) { 19 | // eslint-disable-next-line no-console 20 | console.error("Chunk error:", err); 21 | } 22 | // console.log("<< void)[] = []; 30 | _flush(toFlush: (() => Promise)[] | undefined = undefined, done?: () => void): void { 31 | if (done) { 32 | this._flushDoneFns.push(done); 33 | } 34 | 35 | if (this._toFlush.length == 0) { 36 | // console.log("Flush is stopped", this._toFlush.length) 37 | this._flushIsRunning = false; 38 | this._flushDoneFns.forEach((fn) => fn()); 39 | this._flushDoneFns = []; 40 | return; 41 | } 42 | 43 | if (!toFlush && this._toFlush.length == 1 && !this._flushIsRunning) { 44 | this._flushIsRunning = true; 45 | // console.log("Flush is started", this._toFlush.length) 46 | } else if (!toFlush) { 47 | // console.log("flush queue check but is running", this._toFlush.length) 48 | return; 49 | } 50 | 51 | // console.log(">>>Msg:", this._toFlush.length) 52 | const my = this._toFlush.shift(); 53 | my?.() 54 | .catch((e) => { 55 | // eslint-disable-next-line no-console 56 | console.error("Flush error:", e); 57 | }) 58 | .finally(() => { 59 | // console.log("<< string; 21 | export type KeysParam = (string | MsgFn | Record)[]; 22 | 23 | export function getParamsResult( 24 | keys: KeysParam, 25 | getParam: { getParam: (key: string) => string | undefined }, 26 | ): Result> { 27 | const keyDef = keys.flat().reduce( 28 | (acc, i) => { 29 | if (typeof i === "string") { 30 | acc.push({ key: i, def: undefined, isOptional: false }); 31 | } else if (typeof i === "object") { 32 | acc.push( 33 | ...Object.keys(i).map((k) => ({ 34 | key: k, 35 | def: typeof i[k] === "string" ? i[k] : undefined, 36 | isOptional: i[k] === param.OPTIONAL, 37 | })), 38 | ); 39 | } 40 | return acc; 41 | }, 42 | [] as { key: string; def?: string; isOptional: boolean }[], 43 | ); 44 | //.filter((k) => typeof k === "string"); 45 | const msgFn = 46 | keys.find((k) => typeof k === "function") || 47 | ((...keys: string[]): string => { 48 | const msg = keys.join(","); 49 | return `missing parameters: ${msg}`; 50 | }); 51 | const errors: string[] = []; 52 | const result: Record = {}; 53 | for (const kd of keyDef) { 54 | const val = getParam.getParam(kd.key); 55 | if (val === undefined) { 56 | if (typeof kd.def === "string") { 57 | result[kd.key] = kd.def; 58 | } else { 59 | if (!kd.isOptional) { 60 | errors.push(kd.key); 61 | } 62 | } 63 | } else { 64 | result[kd.key] = val; 65 | } 66 | } 67 | if (errors.length) { 68 | return Result.Err(msgFn(...errors)); 69 | } 70 | return Result.Ok(result); 71 | } 72 | -------------------------------------------------------------------------------- /ts/src/utils/stripper.ts: -------------------------------------------------------------------------------- 1 | export type StripCommand = string | RegExp; 2 | 3 | export function stripper | S, S>( 4 | strip: StripCommand | StripCommand[], 5 | obj: T, 6 | ): T extends ArrayLike ? Record[] : Record { 7 | const strips = Array.isArray(strip) ? strip : [strip]; 8 | const restrips = strips.map((s) => { 9 | if (typeof s === "string") { 10 | const escaped = s.replace(/[-\\[\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\^\\$\\|]/g, "\\$&"); 11 | return new RegExp(`^${escaped}$`); 12 | } 13 | return s; 14 | }); 15 | return localStripper(undefined, restrips, obj) as T extends ArrayLike 16 | ? Record[] 17 | : Record; 18 | } 19 | 20 | function localStripper(path: string | undefined, restrips: RegExp[], obj: T): unknown { 21 | if (typeof obj !== "object" || obj === null) { 22 | return obj; 23 | } 24 | if (Array.isArray(obj)) { 25 | return obj.map((i) => localStripper(path, restrips, i)); 26 | } 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | const ret = { ...obj } as Record; 29 | const matcher = (key: string, nextPath: string): boolean => { 30 | for (const re of restrips) { 31 | if (re.test(key) || re.test(nextPath)) { 32 | return true; 33 | } 34 | } 35 | return false; 36 | }; 37 | for (const key in ret) { 38 | if (Object.prototype.hasOwnProperty.call(ret, key)) { 39 | let nextPath: string; 40 | if (path) { 41 | nextPath = [path, key].join("."); 42 | } else { 43 | nextPath = key; 44 | } 45 | if (matcher(key, nextPath)) { 46 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 47 | delete ret[key]; 48 | continue; 49 | } 50 | if (typeof ret[key] === "object") { 51 | if (Array.isArray(ret[key])) { 52 | ret[key] = ret[key].reduce((acc: unknown[], v, i) => { 53 | const toDelete = matcher(key, `${nextPath}[${i}]`); 54 | if (!toDelete) { 55 | acc.push(localStripper(`${nextPath}[${i}]`, restrips, v)); 56 | } 57 | return acc; 58 | }, []); 59 | } else { 60 | ret[key] = localStripper(nextPath, restrips, ret[key]); 61 | } 62 | } 63 | } 64 | } 65 | return ret; 66 | } 67 | -------------------------------------------------------------------------------- /ts/src/utils/console-write-stream.ts: -------------------------------------------------------------------------------- 1 | export class ConsoleWriterStreamDefaultWriter implements WritableStreamDefaultWriter { 2 | readonly desiredSize: number | null = null; 3 | readonly decoder: TextDecoder = new TextDecoder(); 4 | 5 | closed: Promise; 6 | ready: Promise; 7 | readonly _stream: ConsoleWriterStream; 8 | 9 | constructor(private stream: ConsoleWriterStream) { 10 | this._stream = stream; 11 | this.ready = Promise.resolve(undefined); 12 | this.closed = Promise.resolve(undefined); 13 | } 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any 15 | abort(reason?: any): Promise { 16 | throw new Error("Method not implemented."); 17 | } 18 | async close(): Promise { 19 | // noop 20 | } 21 | releaseLock(): void { 22 | this._stream.locked = false; 23 | this.ready = Promise.resolve(undefined); 24 | this.closed = Promise.resolve(undefined); 25 | } 26 | write(chunk?: Uint8Array): Promise { 27 | let strObj: string | { level: string } = this.decoder.decode(chunk).trimEnd(); 28 | let output = "log"; 29 | try { 30 | strObj = JSON.parse(strObj) as { level: string }; 31 | output = strObj.level; 32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 33 | } catch (e) { 34 | /* noop */ 35 | } 36 | switch (output) { 37 | case "error": 38 | // eslint-disable-next-line no-console 39 | console.error(strObj); 40 | break; 41 | case "warn": 42 | // eslint-disable-next-line no-console 43 | console.warn(strObj); 44 | break; 45 | default: 46 | // eslint-disable-next-line no-console 47 | console.log(strObj); 48 | } 49 | return Promise.resolve(); 50 | } 51 | } 52 | 53 | export class ConsoleWriterStream implements WritableStream { 54 | locked = false; 55 | _writer?: WritableStreamDefaultWriter; 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars 57 | abort(reason?: any): Promise { 58 | throw new Error("Method not implemented."); 59 | } 60 | close(): Promise { 61 | return Promise.resolve(); 62 | } 63 | getWriter(): WritableStreamDefaultWriter { 64 | if (this.locked) { 65 | throw new Error("Stream is locked"); 66 | } 67 | this.locked = true; 68 | if (!this._writer) { 69 | this._writer = new ConsoleWriterStreamDefaultWriter(this); 70 | } 71 | return this._writer; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ts/src/utils/rebuffer.ts: -------------------------------------------------------------------------------- 1 | import { array2stream, stream2array } from "./stream-map.js"; 2 | 3 | interface ReChunkResult { 4 | readonly rest: Uint8Array; 5 | readonly chunk: Uint8Array; 6 | } 7 | 8 | export async function rebufferArray(a: Uint8Array[], chunkSize: number): Promise { 9 | return stream2array(rebuffer(array2stream(a), chunkSize)); 10 | } 11 | 12 | function reChunk(cs: Uint8Array[], chunkSize: number): ReChunkResult { 13 | const len = cs.reduce((acc, v) => acc + v.length, 0); 14 | const last = cs[cs.length - 1]; 15 | const lastOfs = len - last.length; 16 | // console.log("reChunk", len, lastOfs, last.length, chunkSize, chunkSize - lastOfs) 17 | const rest = last.subarray(chunkSize - lastOfs); 18 | cs[cs.length - 1] = last.subarray(0, chunkSize - lastOfs); 19 | const chunk = new Uint8Array(chunkSize); 20 | let ofs = 0; 21 | for (const c of cs) { 22 | chunk.set(c, ofs); 23 | ofs += c.length; 24 | } 25 | return { rest, chunk }; 26 | } 27 | 28 | interface pumpState { 29 | readonly reader: ReadableStreamDefaultReader; 30 | tmp: Uint8Array[]; 31 | tmpLen: number; 32 | readonly chunkSize: number; 33 | } 34 | 35 | function pump(ps: pumpState, controller: ReadableStreamDefaultController, next: () => void): void { 36 | ps.reader 37 | .read() 38 | .then(({ done, value }) => { 39 | if (done) { 40 | if (ps.tmpLen > 0) { 41 | controller.enqueue(reChunk(ps.tmp, ps.tmpLen).chunk); 42 | } 43 | controller.close(); 44 | next(); 45 | return; 46 | } 47 | if (ps.tmpLen + value.length > ps.chunkSize) { 48 | ps.tmp.push(value); 49 | const res = reChunk(ps.tmp, ps.chunkSize); 50 | controller.enqueue(res.chunk); 51 | ps.tmp = [res.rest]; 52 | ps.tmpLen = res.rest.length; 53 | next(); 54 | return; 55 | } else if (value.length) { 56 | ps.tmp.push(value); 57 | ps.tmpLen += value.length; 58 | } 59 | pump(ps, controller, next); 60 | }) 61 | .catch((err) => { 62 | controller.error(err); 63 | next(); 64 | }); 65 | } 66 | 67 | export function rebuffer(a: ReadableStream, chunkSize: number): ReadableStream { 68 | const state: pumpState = { 69 | reader: a.getReader(), 70 | tmp: [], 71 | tmpLen: 0, 72 | chunkSize, 73 | }; 74 | return new ReadableStream({ 75 | async pull(controller): Promise { 76 | return new Promise((resolve) => { 77 | pump(state, controller, resolve); 78 | }); 79 | }, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /ts/src/test/mock-logger.test.ts: -------------------------------------------------------------------------------- 1 | import { LogWriteStream, MockLogger } from "@adviser/cement"; 2 | 3 | describe("logger", () => { 4 | it("with logcollector", async () => { 5 | const l = MockLogger(); 6 | l.logger.Debug().Str("bla1", "blub1").Msg("hello1"); 7 | l.logger.Info().Str("bla2", "blub2").Msg("hello2"); 8 | await l.logger.Flush(); 9 | expect(l.logCollector.Logs()).toEqual([ 10 | { level: "debug", bla1: "blub1", msg: "hello1", module: "MockLogger" }, 11 | { level: "info", bla2: "blub2", msg: "hello2", module: "MockLogger" }, 12 | ]); 13 | }); 14 | 15 | it("with logcollector disableDebug", async () => { 16 | const l = MockLogger({ 17 | disableDebug: true, 18 | }); 19 | l.logger.Debug().Str("bla1", "blub1").Msg("hello1"); 20 | l.logger.Info().Str("bla2", "blub2").Msg("hello2"); 21 | await l.logger.Flush(); 22 | expect(l.logCollector.Logs()).toEqual([{ level: "info", bla2: "blub2", msg: "hello2", module: "MockLogger" }]); 23 | }); 24 | 25 | it("with logcollector moduleName", async () => { 26 | const l = MockLogger({ 27 | moduleName: "test", 28 | }); 29 | l.logger.Debug().Str("bla1", "blub1").Msg("hello1"); 30 | l.logger.Info().Str("bla2", "blub2").Msg("hello2"); 31 | await l.logger.Flush(); 32 | expect(l.logCollector.Logs()).toEqual([ 33 | { level: "debug", bla1: "blub1", msg: "hello1", module: "test" }, 34 | { level: "info", bla2: "blub2", msg: "hello2", module: "test" }, 35 | ]); 36 | }); 37 | 38 | it("with logcollector [moduleName]", async () => { 39 | const l = MockLogger({ 40 | moduleName: ["test", "wurst"], 41 | }); 42 | l.logger.Debug().Str("bla1", "blub1").Msg("hello1"); 43 | l.logger.With().Module("wurst").Logger().Debug().Str("bla2", "blub2").Msg("hello2"); 44 | await l.logger.Flush(); 45 | expect(l.logCollector.Logs()).toEqual([ 46 | { level: "debug", bla1: "blub1", msg: "hello1", module: "test" }, 47 | { level: "debug", bla2: "blub2", msg: "hello2", module: "wurst" }, 48 | ]); 49 | }); 50 | 51 | it("tee in logcolletor", async () => { 52 | const lc2Buffer: Uint8Array[] = []; 53 | const lc2 = new LogWriteStream(lc2Buffer); 54 | const l = MockLogger({ 55 | pass: lc2, 56 | }); 57 | l.logger.Error().Msg("should been shown in console"); 58 | await l.logger.Flush(); 59 | expect(l.logCollector.Logs()).toEqual([{ level: "error", msg: "should been shown in console", module: "MockLogger" }]); 60 | expect(lc2Buffer.length).toBe(1); 61 | expect(JSON.parse(new TextDecoder().decode(lc2Buffer[0]))).toEqual({ 62 | level: "error", 63 | msg: "should been shown in console", 64 | module: "MockLogger", 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /ts/src/node/node-file-service.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | import process from "node:process"; 4 | import { FileService, NamedWritableStream } from "../file-service.js"; 5 | import { TxtEnDecoder, Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 6 | 7 | export class NodeFileService implements FileService { 8 | readonly baseDir: string; 9 | constructor(baseDir: string = process.cwd()) { 10 | this.baseDir = this.abs(baseDir); 11 | } 12 | 13 | // nodeImport(fname: string): string { 14 | // // console.log('nodeImport:'+ fname); 15 | // if (path.isAbsolute(fname)) { 16 | // return fname; 17 | // } else { 18 | // return "./" + path.normalize(fname); 19 | // } 20 | // } 21 | 22 | readFileString(fname: string): Promise { 23 | return fs.promises.readFile(fname, { encoding: "utf-8" }); 24 | } 25 | 26 | dirname(fname: string): string { 27 | return path.dirname(fname); 28 | } 29 | basename(fname: string): string { 30 | return path.basename(fname); 31 | } 32 | 33 | join(...paths: string[]): string { 34 | return path.join(...paths); 35 | } 36 | 37 | relative(from: string, to?: string): string { 38 | if (to === undefined) { 39 | to = from; 40 | from = process.cwd(); 41 | } 42 | const ret = path.relative(from, to); 43 | // console.log('relative:'+ from + " -> " + to + "= " + ret); 44 | return ret; 45 | } 46 | 47 | abs(fname: string): string { 48 | if (path.isAbsolute(fname)) { 49 | return fname; 50 | } else { 51 | const cwd = process.cwd(); 52 | return path.resolve(cwd, fname); 53 | } 54 | } 55 | 56 | isAbsolute(fname: string): boolean { 57 | return path.isAbsolute(fname); 58 | } 59 | 60 | async writeFileString(fname: string, content: string, ende: TxtEnDecoder = Utf8EnDecoderSingleton()): Promise { 61 | const o = await this.create(fname); 62 | const wr = o.stream.getWriter(); 63 | await wr.write(ende.encode(content)); 64 | await wr.close(); 65 | } 66 | 67 | async create(fname: string): Promise { 68 | let oName = fname; 69 | if (!path.isAbsolute(fname)) { 70 | oName = this.abs(fname); 71 | } 72 | 73 | const base = path.dirname(oName); 74 | await fs.promises.mkdir(base, { recursive: true }); 75 | const out = fs.createWriteStream(oName); 76 | return { 77 | name: oName, 78 | stream: new WritableStream({ 79 | write(chunk): void { 80 | out.write(chunk); 81 | }, 82 | close(): void { 83 | out.close(); 84 | }, 85 | abort(): void { 86 | throw new Error("not implemented"); 87 | }, 88 | }), 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ts/src/bin2text.test.ts: -------------------------------------------------------------------------------- 1 | import { bin2string, bin2text } from "./bin2text.js"; 2 | 3 | it("bin2hex empty", () => { 4 | expect(bin2string(new Uint8Array())).toEqual(""); 5 | }); 6 | 7 | it("bin2hex one", () => { 8 | expect(bin2string(new Uint8Array([64]))).toBe("0000 40 @"); 9 | }); 10 | 11 | it("bin2hex 12", () => { 12 | const collector: string[] = []; 13 | bin2text(new Uint8Array([1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc]), (line) => { 14 | collector.push(line); 15 | }); 16 | expect(collector).toEqual(["0000 01 02 03 04 04 05 06 07 08 09 0a 0b 0c ............."]); 17 | }); 18 | 19 | it("bin2hex cut", () => { 20 | const collector: string[] = []; 21 | bin2text( 22 | new Uint8Array(new Array(255).fill(1).map((_, i) => i)), 23 | (line) => { 24 | collector.push(line); 25 | }, 26 | 52, 27 | ); 28 | expect(collector).toEqual([ 29 | "0000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ................", 30 | "0010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f ................", 31 | "0020 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f !\"#$%&'()*+,-./", 32 | "00f0>>f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ...............", 33 | ]); 34 | }); 35 | 36 | it("bin2hex long", () => { 37 | const collector: string[] = []; 38 | bin2text(new Uint8Array(new Array(256).fill(1).map((_, i) => i % 0xff)), (line) => { 39 | collector.push(line); 40 | }); 41 | expect(collector).toEqual([ 42 | "0000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ................", 43 | "0010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f ................", 44 | "0020 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f !\"#$%&'()*+,-./", 45 | "0030 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 0123456789:;<=>?", 46 | "0040 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f @ABCDEFGHIJKLMNO", 47 | "0050 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f PQRSTUVWXYZ[\\]^_", 48 | "0060 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f `abcdefghijklmno", 49 | "0070 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f pqrstuvwxyz{|}~.", 50 | "0080 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f ................", 51 | "0090 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f ................", 52 | "00a0 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af ................", 53 | "00b0 b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf ................", 54 | "00c0 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf ................", 55 | "00d0 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df ................", 56 | "00e0 e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef ................", 57 | "00f0 f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe 00 ................", 58 | ]); 59 | }); 60 | -------------------------------------------------------------------------------- /ts/src/test/test-exit-handler.ts: -------------------------------------------------------------------------------- 1 | import { runtimeFn } from "../runtime.js"; 2 | import { SysAbstraction } from "../sys-abstraction.js"; 3 | 4 | const gts = globalThis as unknown /*Deno*/ as { 5 | Deno: { 6 | args: string[]; 7 | pid: number; 8 | kill(pid: number, signal: string): void; 9 | }; 10 | process: { 11 | argv: string[]; 12 | pid: number; 13 | kill(pid: number, signal: string): void; 14 | }; 15 | }; 16 | 17 | async function main(): Promise { 18 | const modPath = runtimeFn().isDeno 19 | ? new URL("../deno/deno-sys-abstraction.ts", import.meta.url).pathname 20 | : new URL("../node/node-sys-abstraction.ts", import.meta.url).pathname; 21 | // console.log("modPath", modPath); 22 | const sa = (await import(modPath)) as { 23 | DenoSysAbstraction: () => SysAbstraction; 24 | NodeSysAbstraction: () => SysAbstraction; 25 | }; 26 | 27 | const my = runtimeFn().isDeno ? sa.DenoSysAbstraction() : sa.NodeSysAbstraction(); 28 | 29 | const process = runtimeFn().isDeno ? gts.Deno : gts.process; 30 | 31 | const rargs = (runtimeFn().isDeno ? gts.Deno?.args : gts.process?.argv) || []; 32 | 33 | const larg = rargs[rargs.length - 1]; 34 | // eslint-disable-next-line no-console 35 | console.log( 36 | JSON.stringify({ 37 | larg, 38 | pid: process.pid, 39 | }), 40 | ); 41 | 42 | my.System().OnExit(async () => { 43 | await my.Time().Sleep(100); 44 | // eslint-disable-next-line no-console 45 | console.log( 46 | JSON.stringify({ 47 | larg, 48 | pid: process.pid, 49 | msg: "Called OnExit 1", 50 | }), 51 | ); 52 | }); 53 | my.System().OnExit(async () => { 54 | await my.Time().Sleep(200); 55 | // eslint-disable-next-line no-console 56 | console.log( 57 | JSON.stringify({ 58 | larg, 59 | pid: process.pid, 60 | msg: "Called OnExit 2", 61 | }), 62 | ); 63 | }); 64 | 65 | switch (larg) { 66 | case "sigint": 67 | await my.Time().Sleep(100); 68 | process.kill(process.pid, "SIGINT"); 69 | await my.Time().Sleep(1000000); 70 | break; 71 | case "sigquit": 72 | await my.Time().Sleep(100); 73 | process.kill(process.pid, "SIGQUIT"); 74 | await my.Time().Sleep(1000000); 75 | break; 76 | case "sigterm": 77 | await my.Time().Sleep(100); 78 | process.kill(process.pid, "SIGTERM"); 79 | await my.Time().Sleep(1000000); 80 | break; 81 | case "throw": 82 | await my.Time().Sleep(100); 83 | throw new Error("throwing"); 84 | case "sleep": 85 | await my.Time().Sleep(3000); 86 | break; 87 | case "exit24": 88 | default: 89 | my.System().Exit(24); 90 | } 91 | return; 92 | } 93 | 94 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 95 | main(); 96 | -------------------------------------------------------------------------------- /ts/src/log-level-impl.ts: -------------------------------------------------------------------------------- 1 | import { LevelHandler, Level } from "./logger.js"; 2 | import { Option } from "./option.js"; 3 | 4 | export class LevelHandlerImpl implements LevelHandler { 5 | readonly _globalLevels: Set = new Set([Level.INFO, Level.ERROR, Level.WARN]); 6 | readonly _modules: Map> = new Map>(); 7 | 8 | ignoreAttr: Option = Option.Some(/^_/); 9 | isStackExposed = false; 10 | enableLevel(level: Level, ...modules: string[]): void { 11 | if (modules.length == 0) { 12 | this._globalLevels.add(level); 13 | return; 14 | } 15 | this.forModules( 16 | level, 17 | (p) => { 18 | this._modules.set(p, new Set([...this._globalLevels, level])); 19 | }, 20 | ...modules, 21 | ); 22 | } 23 | disableLevel(level: Level, ...modules: string[]): void { 24 | if (modules.length == 0) { 25 | this._globalLevels.delete(level); 26 | return; 27 | } 28 | this.forModules( 29 | level, 30 | (p) => { 31 | this._modules.delete(p); 32 | }, 33 | ...modules, 34 | ); 35 | } 36 | 37 | setExposeStack(enable?: boolean): void { 38 | this.isStackExposed = !!enable; 39 | } 40 | 41 | setIgnoreAttr(re?: RegExp): void { 42 | this.ignoreAttr = Option.From(re); 43 | } 44 | 45 | forModules(level: Level, fnAction: (p: string) => void, ...modules: (string | string[])[]): void { 46 | for (const m of modules.flat()) { 47 | if (typeof m !== "string") { 48 | continue; 49 | } 50 | const parts = m 51 | .split(",") 52 | .map((s) => s.trim()) 53 | .filter((s) => s.length); 54 | for (const p of parts) { 55 | fnAction(p); 56 | } 57 | } 58 | } 59 | setDebug(...modules: (string | string[])[]): void { 60 | this.forModules( 61 | Level.DEBUG, 62 | (p) => { 63 | this._modules.set(p, new Set([...this._globalLevels, Level.DEBUG])); 64 | }, 65 | ...modules, 66 | ); 67 | } 68 | isEnabled(ilevel: unknown, module: unknown): boolean { 69 | const level = ilevel as Level; // what if it's not a level? 70 | if (typeof module === "string") { 71 | const levels = this._modules.get(module); 72 | if (levels && levels.has(level)) { 73 | return true; 74 | } 75 | } 76 | const wlevel = this._modules.get("*"); 77 | if (wlevel && typeof level === "string") { 78 | if (wlevel.has(level)) { 79 | return true; 80 | } 81 | } 82 | if (typeof level !== "string") { 83 | // this is a plain log 84 | return true; 85 | } 86 | return this._globalLevels.has(level); 87 | } 88 | } 89 | 90 | const levelSingleton = new LevelHandlerImpl(); 91 | 92 | export function LevelHandlerSingleton(): LevelHandler { 93 | return levelSingleton; 94 | } 95 | -------------------------------------------------------------------------------- /ts/src/deno/deno-file-service.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { FileService, NamedWritableStream } from "../file-service.js"; 3 | import { TxtEnDecoder, Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 4 | 5 | const Deno = (globalThis as unknown as { Deno: unknown }).Deno as { 6 | cwd(): string; 7 | readFile(fname: string): Promise; 8 | mkdir(base: string, options: { recursive: boolean }): Promise; 9 | open(fname: string, options: { write: boolean; create: boolean; truncate: boolean }): Promise>; 10 | }; 11 | 12 | export class DenoFileService implements FileService { 13 | readonly baseDir: string; 14 | readonly txtEnde: TxtEnDecoder; 15 | constructor(baseDir: string = Deno.cwd(), txtEnde: TxtEnDecoder = Utf8EnDecoderSingleton()) { 16 | this.baseDir = this.abs(baseDir); 17 | this.txtEnde = txtEnde; 18 | } 19 | 20 | // nodeImport(fname: string): string { 21 | // // console.log('nodeImport:'+ fname); 22 | // if (path.isAbsolute(fname)) { 23 | // return fname; 24 | // } else { 25 | // return "./" + path.normalize(fname); 26 | // } 27 | // } 28 | 29 | async readFileString(fname: string): Promise { 30 | return this.txtEnde.decode(await Deno.readFile(fname)); 31 | } 32 | 33 | dirname(fname: string): string { 34 | return path.dirname(fname); 35 | } 36 | basename(fname: string): string { 37 | return path.basename(fname); 38 | } 39 | 40 | join(...paths: string[]): string { 41 | return path.join(...paths); 42 | } 43 | 44 | relative(from: string, to?: string): string { 45 | if (to === undefined) { 46 | to = from; 47 | from = Deno.cwd(); 48 | } 49 | const ret = path.relative(from, to); 50 | // console.log('relative:'+ from + " -> " + to + "= " + ret); 51 | return ret; 52 | } 53 | 54 | abs(fname: string): string { 55 | if (path.isAbsolute(fname)) { 56 | return fname; 57 | } else { 58 | const cwd = Deno.cwd(); 59 | return path.resolve(cwd, fname); 60 | } 61 | } 62 | 63 | isAbsolute(fname: string): boolean { 64 | return path.isAbsolute(fname); 65 | } 66 | 67 | async writeFileString(fname: string, content: string, ende: TxtEnDecoder = Utf8EnDecoderSingleton()): Promise { 68 | const o = await this.create(fname); 69 | const wr = o.stream.getWriter(); 70 | await wr.write(ende.encode(content)); 71 | await wr.close(); 72 | } 73 | 74 | async create(fname: string): Promise { 75 | let oName = fname; 76 | if (!path.isAbsolute(fname)) { 77 | oName = this.abs(fname); 78 | } 79 | 80 | const base = path.dirname(oName); 81 | await Deno.mkdir(base, { recursive: true }); 82 | const out = await Deno.open(oName, { 83 | write: true, 84 | create: true, 85 | truncate: true, 86 | }); 87 | return { 88 | name: oName, 89 | stream: out, 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adviser/cement", 3 | "version": "0.0.0", 4 | "description": "better try/catch/finally handling", 5 | "main": "index.js", 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts", 10 | "require": "./index.cjs", 11 | "import": "./index.js" 12 | }, 13 | "./web": { 14 | "types": "./web/index.d.ts", 15 | "require": "./web/index.cjs", 16 | "import": "./web/index.js" 17 | }, 18 | "./utils": { 19 | "types": "./utils/index.d.ts", 20 | "require": "./utils/index.cjs", 21 | "import": "./utils/index.js" 22 | }, 23 | "./cf": { 24 | "types": "./cf/index.d.ts", 25 | "require": "./cf/index.cjs", 26 | "import": "./cf/index.js" 27 | }, 28 | "./node": { 29 | "types": "./node/index.d.ts", 30 | "require": "./node/index.cjs", 31 | "import": "./node/index.js" 32 | }, 33 | "./deno": { 34 | "types": "./deno/index.d.ts", 35 | "require": "./deno/index.cjs", 36 | "import": "./deno/index.js" 37 | } 38 | }, 39 | "scripts": { 40 | "clean": "rm -rf dist node_modules", 41 | "build": "pnpm run build:tsc; pnpm run build:js; pnpm run build:deno", 42 | "build:tsc": "tsc", 43 | "xbuild:js": "tsup", 44 | "build:js": "tsup --out-dir dist/pkg src/index.ts src/*/index.ts --sourcemap --format cjs,esm --dts --clean --external node:fs --external node:path", 45 | "build:deno": "deno publish --dry-run --unstable-sloppy-imports --allow-dirty", 46 | "test": "pnpm run test:js; pnpm run test:deno", 47 | "test:deno": "deno run --quiet --allow-net --allow-write --allow-run --allow-sys --allow-ffi --allow-read --allow-env ./node_modules/vitest/vitest.mjs --run --project node", 48 | "test:js": "vitest --run", 49 | "pubdir": "bash -xe ./prepare-pubdir.sh", 50 | "presmoke": "pnpm run pubdir ; cd pubdir ; pnpm pack", 51 | "smoke": "bash ./smoke/it.sh", 52 | "lint": "eslint .", 53 | "prettier": "prettier .", 54 | "format": "prettier ." 55 | }, 56 | "keywords": [], 57 | "author": "Meno Abels ", 58 | "license": "AFL-2.0", 59 | "bugs": { 60 | "url": "https://github.com/mabels/cement/issues" 61 | }, 62 | "homepage": "https://github.com/mabels/cement#readme", 63 | "dependencies": { 64 | "ts-essentials": "^10.0.2", 65 | "yaml": "^2.5.1" 66 | }, 67 | "devDependencies": { 68 | "@cloudflare/vitest-pool-workers": "^0.6.0", 69 | "@types/node": "^22.10.2", 70 | "@typescript-eslint/eslint-plugin": "^8.7.0", 71 | "@typescript-eslint/parser": "^8.7.0", 72 | "@vitest/browser": "^2.1.8", 73 | "esbuild-plugin-replace": "^1.4.0", 74 | "esbuild-plugin-resolve": "^2.0.0", 75 | "eslint": "9.19.0", 76 | "prettier": "^3.3.3", 77 | "tsup": "^8.3.0", 78 | "tsx": "^4.19.1", 79 | "typescript": "^5.7.2", 80 | "typescript-eslint": "^8.18.2", 81 | "vite-tsconfig-paths": "^5.0.1", 82 | "vitest": "^2.1.8", 83 | "webdriverio": "^9.4.5" 84 | }, 85 | "engines": { 86 | "node": ">=20" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/ng-nodejs-ci.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build - @adviser/cement 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | permissions: 13 | contents: read 14 | id-token: write 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [20.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - uses: denoland/setup-deno@v1 31 | with: 32 | deno-version: v2.x 33 | 34 | - uses: pnpm/action-setup@v4 35 | name: Install pnpm 36 | with: 37 | run_install: false 38 | version: 9 39 | 40 | # - name: Patch Version ${{ env.GITHUB_REF }} 41 | # run: | 42 | # cd ts ; node ../.github/workflows/patch-package.json.js "origin/head/0.0.0-smoke" 43 | # git diff 44 | # 45 | # - name: Patch Version ${{ env.GITHUB_REF }} 46 | # if: startsWith(github.ref, 'refs/tags/v') 47 | # run: | 48 | # cd ts ; node ../.github/workflows/patch-package.json.js "$GITHUB_REF" 49 | # git diff 50 | 51 | - name: Build JS @adviser/cement 52 | run: | 53 | cd ts 54 | pnpm install 55 | pnpm run prettier --check 56 | pnpm run lint 57 | pnpm run build 58 | # deno publish --dry-run --unstable-sloppy-imports --allow-dirty 59 | pnpm run test 60 | # deno throws an error as side effect node-sys-abstraction 61 | # pnpm run test:deno || true 62 | pnpm run smoke 63 | 64 | - uses: mabels/neckless@main 65 | if: startsWith(github.ref, 'refs/tags/v') 66 | with: 67 | version: v0.1.16 68 | 69 | - name: Publish runified NPM 70 | if: startsWith(github.ref, 'refs/tags/v') 71 | env: 72 | NECKLESS_PRIVKEY: ${{ secrets.NECKLESS_PRIVKEY }} 73 | run: | 74 | eval $(neckless kv ls --shKeyValue --ghAddMask NPM_PASS NPM_USER "NPM_TOTP@Totp()") 75 | # env | sort | grep NPM 76 | token=$(curl -H "npm-otp: $NPM_TOTP" \ 77 | -X PUT \ 78 | -H 'content-type: application/json' \ 79 | -d "{\"name\":\"$NPM_USER\", \"password\": \"$NPM_PASS\"}" \ 80 | https://registry.npmjs.org/-/user/org.couchdb.user:$NPM_USER | jq -r .token) 81 | echo "//registry.npmjs.org/:_authToken=$token" > $HOME/.npmrc 82 | cd ts 83 | cd pubdir 84 | pnpm publish --otp=$(neckless kv ls --onlyValue "NPM_TOTP@Totp()") --access public --no-git-checks 85 | 86 | - name: Publish runified JSR 87 | if: startsWith(github.ref, 'refs/tags/v') 88 | run: | 89 | cd ts/pubdir/src 90 | deno publish --allow-dirty --unstable-sloppy-imports 91 | 92 | -------------------------------------------------------------------------------- /ts/src/utils/stripper.test.ts: -------------------------------------------------------------------------------- 1 | import { stripper } from "./stripper.js"; 2 | 3 | const toStrip = { 4 | main: "main", 5 | Main: "main", 6 | nested: { 7 | main: "main", 8 | Main: "main", 9 | nested: { 10 | main: "main", 11 | Main: "main", 12 | }, 13 | }, 14 | arrays: [ 15 | "main", 16 | "Main", 17 | { 18 | main: "main", 19 | Main: "main", 20 | nested: { 21 | main: "main", 22 | Main: "main", 23 | }, 24 | }, 25 | ], 26 | }; 27 | it("empty stripper", () => { 28 | expect(stripper("", toStrip)).toEqual(toStrip); 29 | }); 30 | 31 | it("array concrete stripper", () => { 32 | expect(stripper(["main", "Main", "blub"], toStrip)).toEqual({ 33 | nested: { 34 | nested: {}, 35 | }, 36 | arrays: [ 37 | "main", 38 | "Main", 39 | { 40 | nested: {}, 41 | }, 42 | ], 43 | }); 44 | }); 45 | 46 | it("array simple regex concrete stripper", () => { 47 | expect(stripper([/ain$/, "blub"], toStrip)).toEqual({ 48 | nested: { 49 | nested: {}, 50 | }, 51 | arrays: [ 52 | "main", 53 | "Main", 54 | { 55 | nested: {}, 56 | }, 57 | ], 58 | }); 59 | }); 60 | 61 | it("array regex concrete stripper", () => { 62 | expect(stripper([/Main/, "blub"], toStrip)).toEqual({ 63 | main: "main", 64 | nested: { 65 | main: "main", 66 | nested: { 67 | main: "main", 68 | }, 69 | }, 70 | arrays: [ 71 | "main", 72 | "Main", 73 | { 74 | main: "main", 75 | nested: { 76 | main: "main", 77 | }, 78 | }, 79 | ], 80 | }); 81 | }); 82 | 83 | it("array dotted concrete stripper", () => { 84 | expect( 85 | stripper(["nested.main", "blub", "nested.nested.Main", "arrays[1]", "arrays[2].main", /arrays.2..nested..ain/], toStrip), 86 | ).toEqual({ 87 | main: "main", 88 | Main: "main", 89 | nested: { 90 | Main: "main", 91 | nested: { 92 | main: "main", 93 | }, 94 | }, 95 | arrays: [ 96 | "main", 97 | { 98 | Main: "main", 99 | nested: { 100 | // "main": "main", 101 | }, 102 | }, 103 | ], 104 | }); 105 | }); 106 | it("return type unknown|unknown[]", () => { 107 | const plain = stripper(["main"], { main: "main" }); 108 | expectTypeOf(plain).toEqualTypeOf>(); 109 | const aplain = stripper(["main"], [{ main: "main" }]); 110 | expectTypeOf(aplain).toEqualTypeOf[]>(); 111 | }); 112 | it("array top level stripper", () => { 113 | expect( 114 | stripper( 115 | ["main"], 116 | [ 117 | { o: 1, main: "main" }, 118 | { o: 2, main: "main" }, 119 | [ 120 | { o: 3, main: "main" }, 121 | { o: 4, main: "main" }, 122 | ], 123 | ], 124 | ), 125 | ).toEqual([ 126 | { 127 | o: 1, 128 | }, 129 | { 130 | o: 2, 131 | }, 132 | [ 133 | { 134 | o: 3, 135 | }, 136 | { 137 | o: 4, 138 | }, 139 | ], 140 | ]); 141 | }); 142 | -------------------------------------------------------------------------------- /ts/src/result.test.ts: -------------------------------------------------------------------------------- 1 | import { exception2Result, Result, WithoutResult } from "@adviser/cement"; 2 | // import { it } from "vitest/globals"; 3 | 4 | it("ResultOk", () => { 5 | const result = Result.Ok(1); 6 | expect(result.isOk()).toBe(true); 7 | expect(result.is_ok()).toBe(true); 8 | expect(result.Ok()).toBe(1); 9 | expect(result.unwrap()).toBe(1); 10 | 11 | expect(result.isErr()).toBe(false); 12 | expect(result.is_err()).toBe(false); 13 | expect(() => result.Err()).toThrow(); 14 | expect(() => result.unwrap_err()).toThrow(); 15 | }); 16 | 17 | it("ResultErr", () => { 18 | const result = Result.Err("xxx"); 19 | expect(result.isOk()).toBe(false); 20 | expect(result.is_ok()).toBe(false); 21 | expect(result.Err().message).toEqual("xxx"); 22 | expect(result.unwrap_err().message).toBe("xxx"); 23 | 24 | expect(result.isErr()).toBe(true); 25 | expect(result.is_err()).toBe(true); 26 | expect(() => result.Ok()).toThrow(); 27 | expect(() => result.unwrap()).toThrow(); 28 | }); 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 31 | class xResult {} 32 | class fakeResult { 33 | is_ok(): boolean { 34 | return true; 35 | } 36 | is_err(): boolean { 37 | return false; 38 | } 39 | unwrap(): number { 40 | return 1; 41 | } 42 | unwrap_err(): Error { 43 | throw new Error("Result is Ok"); 44 | } 45 | } 46 | it("is Result", () => { 47 | expect(Result.Is(Result.Ok(1))).toBe(true); 48 | expect(Result.Is(Result.Err("xxx"))).toEqual(true); 49 | expect(Result.Is(new fakeResult())).toBe(true); 50 | expect(Result.Is(new xResult())).toBe(false); 51 | }); 52 | 53 | it("WithoutResult", () => { 54 | const result = Result.Ok({ a: 1 }); 55 | const a1: Partial> = {}; 56 | a1.a = 1; 57 | expect(a1.a).toEqual(1); 58 | expect(result.Ok().a).toEqual(1); 59 | }); 60 | 61 | it("sync exception2Result ok", () => { 62 | expect(exception2Result(() => 1)).toEqual(Result.Ok(1)); 63 | }); 64 | 65 | it("sync exception2Result throw", () => { 66 | expect( 67 | exception2Result(() => { 68 | throw new Error("x"); 69 | }), 70 | ).toEqual(Result.Err("x")); 71 | }); 72 | 73 | it("async exception2Result ok", async () => { 74 | expect(await exception2Result(() => Promise.resolve(1))).toEqual(Result.Ok(1)); 75 | }); 76 | 77 | it("async exception2Result throw", async () => { 78 | expect(await exception2Result(() => Promise.reject(new Error("x")))).toEqual(Result.Err("x")); 79 | }); 80 | 81 | it("result typ", () => { 82 | function ok(): Result { 83 | return Result.Ok(1); 84 | } 85 | function err(): Result { 86 | return Result.Err("x"); 87 | } 88 | expect(ok().Ok()).toBe(1); 89 | expect(err().Err().message).toBe("x"); 90 | }); 91 | 92 | it("Result.Err receive Result", () => { 93 | expect(Result.Err(Result.Ok(1)).Err().message).toBe("Result Error is Ok"); 94 | const err = Result.Err("xxx"); 95 | expect(Result.Err(err)).toBe(err); 96 | expect(Result.Err(err.Err())).toStrictEqual(err); 97 | }); 98 | 99 | // it("Result.OK with void", () => { 100 | // const result = Result.Ok(); 101 | // expect(result.isOk()).toBe(true); 102 | // expect(result.is_ok()).toBe(true); 103 | // expect(result.isErr()).toBe(false); 104 | // expect(result.is_err()).toBe(false); 105 | // } 106 | -------------------------------------------------------------------------------- /ts/src/web/web-sys-abstraction.ts: -------------------------------------------------------------------------------- 1 | import { BaseSysAbstraction, WrapperSysAbstraction, WrapperSysAbstractionParams } from "../base-sys-abstraction.js"; 2 | import { FileService, NamedWritableStream } from "../file-service.js"; 3 | import { SysAbstraction, SystemService, VoidFunc } from "../sys-abstraction.js"; 4 | import { Env, envFactory } from "../sys-env.js"; 5 | import { Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 6 | 7 | export class WebFileService implements FileService { 8 | get baseDir(): string { 9 | throw new Error("basedir-Method not implemented."); 10 | } 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | create(fname: string): Promise { 13 | throw new Error("create-Method not implemented."); 14 | } 15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 16 | readFileString(fname: string): Promise { 17 | throw new Error("readFileString-Method not implemented."); 18 | } 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | writeFileString(fname: string, content: string): Promise { 21 | throw new Error("writeFileString-Method not implemented."); 22 | } 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | abs(fname: string): string { 25 | throw new Error("abs-Method not implemented."); 26 | } 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | join(...paths: string[]): string { 29 | throw new Error("join-Method not implemented."); 30 | } 31 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 32 | relative(from: string, to?: string): string { 33 | throw new Error("relative-Method not implemented."); 34 | } 35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 36 | dirname(fname: string): string { 37 | throw new Error("dirname-Method not implemented."); 38 | } 39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 40 | basename(fname: string): string { 41 | throw new Error("basename-Method not implemented."); 42 | } 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | nodeImport(fname: string): string { 45 | throw new Error("nodeImport-Method not implemented."); 46 | } 47 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 48 | isAbsolute(fname: string): boolean { 49 | throw new Error("isAbsolute-Method not implemented."); 50 | } 51 | } 52 | 53 | class WebSystemService implements SystemService { 54 | Env(): Env { 55 | return envFactory(); 56 | } 57 | Args(): string[] { 58 | throw new Error("Args-Method not implemented."); 59 | } 60 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 61 | OnExit(hdl: VoidFunc): VoidFunc { 62 | throw new Error("OnExit-Method not implemented."); 63 | } 64 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 65 | Exit(code: number): void { 66 | throw new Error("Exit-Method not implemented."); 67 | } 68 | } 69 | 70 | let my: BaseSysAbstraction | undefined = undefined; 71 | export function WebSysAbstraction(param?: WrapperSysAbstractionParams): SysAbstraction { 72 | if (!my) { 73 | my = new BaseSysAbstraction({ 74 | TxtEnDecoder: param?.TxtEnDecoder || Utf8EnDecoderSingleton(), 75 | FileSystem: new WebFileService(), 76 | SystemService: new WebSystemService(), 77 | }); 78 | } 79 | return new WrapperSysAbstraction(my, param); 80 | } 81 | -------------------------------------------------------------------------------- /ts/src/test/log-write-stream.ts: -------------------------------------------------------------------------------- 1 | import { FanoutWriteStream } from "../utils/fanout-write-stream.js"; 2 | import { Future } from "../future.js"; 3 | import { TxtEnDecoder, Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 4 | 5 | export class LogWriteStream implements WritableStreamDefaultWriter { 6 | private readonly _bufferArr: Uint8Array[]; 7 | 8 | constructor(bufferArr: Uint8Array[]) { 9 | this._bufferArr = bufferArr; 10 | } 11 | 12 | readonly _resolveClosed: Future = new Future(); 13 | readonly closed: Promise = this._resolveClosed.asPromise(); 14 | readonly desiredSize: number | null = null; 15 | readonly ready: Promise = Promise.resolve(undefined); 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any 18 | abort(reason?: any): Promise { 19 | throw new Error("Method not implemented."); 20 | } 21 | async close(): Promise { 22 | await this.closed; 23 | return Promise.resolve(undefined); 24 | } 25 | releaseLock(): void { 26 | // do nothing 27 | } 28 | async write(chunk?: Uint8Array): Promise { 29 | if (chunk) { 30 | this._bufferArr.push(chunk); 31 | } 32 | return Promise.resolve(undefined); 33 | } 34 | } 35 | 36 | export class LogCollector implements WritableStream { 37 | readonly locked: boolean = false; 38 | private _writer?: FanoutWriteStream; 39 | private readonly _pass?: WritableStreamDefaultWriter; 40 | private readonly _bufferArr: Uint8Array[] = []; 41 | private readonly _txtEnDe: TxtEnDecoder; 42 | 43 | constructor(pass?: WritableStreamDefaultWriter, txtEnDe?: TxtEnDecoder) { 44 | this._pass = pass; 45 | this._txtEnDe = txtEnDe || Utf8EnDecoderSingleton(); 46 | } 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 49 | abort(reason?: Uint8Array): Promise { 50 | throw new Error("Method not implemented."); 51 | } 52 | 53 | async close(): Promise { 54 | if (this._writer) { 55 | const ret = await this._writer.close(); 56 | this._writer = undefined; 57 | return ret; 58 | } 59 | return Promise.resolve(undefined); 60 | } 61 | 62 | getWriter(): WritableStreamDefaultWriter { 63 | if (!this._writer) { 64 | const dests: WritableStreamDefaultWriter[] = [new LogWriteStream(this._bufferArr)]; 65 | if (this._pass) { 66 | dests.push(this._pass); 67 | } 68 | this._writer = new FanoutWriteStream(dests); 69 | } 70 | return this._writer; 71 | } 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 74 | Logs(notJsonLine = false): any[] { 75 | if (!this._writer) { 76 | return []; 77 | } 78 | const jsonNlStr = this._txtEnDe.decode( 79 | new Uint8Array( 80 | (function* (res: Uint8Array[]): Generator { 81 | for (const x of res) { 82 | yield* x; 83 | } 84 | })(this._bufferArr), 85 | ), 86 | ); 87 | if (!notJsonLine) { 88 | const splitStr = jsonNlStr.split("\n"); 89 | const filterStr = splitStr.filter((a) => a.length); 90 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 91 | const mapStr = filterStr.map((a) => JSON.parse(a)); 92 | return mapStr; 93 | } 94 | return jsonNlStr.split("\n").filter((a) => a.length); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ts/src/node/node-sys-abstraction.test.ts: -------------------------------------------------------------------------------- 1 | import type { ExecException, exec } from "node:child_process"; 2 | import { runtimeFn } from "@adviser/cement"; 3 | 4 | function exitHandler(errCode: number, larg: string, done: () => void) { 5 | return (err: ExecException | null, stdout: string | Buffer, stderr: string | Buffer): void => { 6 | if (err) { 7 | expect(err.code).toBe(errCode); 8 | } 9 | if (stdout) { 10 | const res = stdout 11 | .toString() 12 | .split("\n") 13 | .filter((line) => line.trim()) 14 | .map((line) => { 15 | const out = JSON.parse(line) as { pid?: number }; 16 | return out; 17 | }) 18 | .map((obj) => { 19 | delete obj.pid; 20 | return obj; 21 | }); 22 | expect(res).toEqual([ 23 | { 24 | larg: larg, 25 | }, 26 | { 27 | larg: larg, 28 | msg: "Called OnExit 1", 29 | }, 30 | { 31 | larg: larg, 32 | msg: "Called OnExit 2", 33 | }, 34 | ]); 35 | done(); 36 | } 37 | if (stderr) { 38 | expect(stderr).toEqual({}); 39 | } 40 | }; 41 | } 42 | 43 | describe("node_sys", () => { 44 | if (runtimeFn().isNodeIsh || runtimeFn().isDeno) { 45 | let fnExec: typeof exec; 46 | let execHandler = "tsx src/test/test-exit-handler.ts"; 47 | beforeAll(async () => { 48 | const { exec } = await import("node:child_process"); 49 | fnExec = exec; 50 | if (runtimeFn().isDeno) { 51 | execHandler = "deno run --allow-net --allow-read --allow-run --unstable-sloppy-imports src/test/test-exit-handler.ts"; 52 | // // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | // const gs = globalThis as any; 54 | // fnExec = (async (cmd: string, cb: (err: ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void) => { 55 | // const c = new gs.Deno.Command(cmd.split(" ")[0], { 56 | // args: cmd.split(" ").slice(1), 57 | // stdout: "piped", 58 | // stderr: "piped", 59 | // }); 60 | // const result = await c.output(); 61 | // const td = new TextDecoder(); 62 | // cb(result, td.decode(result.stdout), td.decode(result.stderr)); 63 | // }) as unknown as typeof exec; 64 | } 65 | }); 66 | it("just-exit", () => { 67 | return new Promise((done) => { 68 | fnExec(`${execHandler} exit24`, exitHandler(24, "exit24", done)); 69 | }); 70 | }); 71 | 72 | it("throw", () => { 73 | return new Promise((done) => { 74 | fnExec(`${execHandler} throw`, exitHandler(19, "throw", done)); 75 | }); 76 | }); 77 | 78 | it("via sigint", () => { 79 | return new Promise((done) => { 80 | fnExec(`${execHandler} sigint`, exitHandler(2, "sigint", done)); 81 | }); 82 | }); 83 | 84 | it("via sigterm", () => { 85 | return new Promise((done) => { 86 | fnExec(`${execHandler} sigterm`, exitHandler(9, "sigterm", done)); 87 | }); 88 | }); 89 | 90 | it("via sigquit", () => { 91 | return new Promise((done) => { 92 | fnExec(`${execHandler} sigquit`, exitHandler(3, "sigquit", done)); 93 | }); 94 | }); 95 | } else { 96 | it.skip("nothing in browser", () => { 97 | expect(true).toBe(true); 98 | }); 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /ts/src/utils/stream-map.test.ts: -------------------------------------------------------------------------------- 1 | import { utils } from "@adviser/cement"; 2 | import { receiveFromStream, sendToStream, streamingTestState } from "./stream-test-helper.js"; 3 | 4 | it("array2stream", async () => { 5 | const as = utils.array2stream([1, 2, 3]); 6 | let i = 0; 7 | const reader = as.getReader(); 8 | while (true) { 9 | const { done, value } = await reader.read(); 10 | if (done) { 11 | break; 12 | } 13 | expect(1 + i++).toBe(value); 14 | } 15 | }); 16 | 17 | it("stream2array", async () => { 18 | const as = await utils.stream2array( 19 | new ReadableStream({ 20 | start(controller): void { 21 | controller.enqueue(1); 22 | controller.enqueue(2); 23 | controller.enqueue(3); 24 | controller.close(); 25 | }, 26 | }), 27 | ); 28 | expect(as).toEqual([1, 2, 3]); 29 | }); 30 | 31 | it("devnull", async () => { 32 | const cnt = await utils.devnull( 33 | utils.streamMap(utils.array2stream([1, 2, 3]), { 34 | Map: (i, idx) => (idx + 1) * 10 + i + 1, 35 | }), 36 | ); 37 | expect(cnt).toBe(3); 38 | }); 39 | 40 | it("stream_map", async () => { 41 | const closeFn = vitest.fn(); 42 | const s = await utils.stream2array( 43 | utils.streamMap(utils.array2stream([1, 2, 3]), { 44 | Map: (i, idx) => (idx + 1) * 10 + i + 1, 45 | Close: closeFn, 46 | }), 47 | ); 48 | expect(closeFn).toBeCalledTimes(1); 49 | expect(s).toEqual([12, 23, 34]); 50 | }); 51 | 52 | it("stream_map async", async () => { 53 | const closeFn = vitest.fn(); 54 | const s = await utils.stream2array( 55 | utils.streamMap(utils.array2stream([1, 2, 3]), { 56 | Map: (i, idx) => Promise.resolve((idx + 1) * 10 + i + 1), 57 | Close: closeFn, 58 | }), 59 | ); 60 | expect(closeFn).toBeCalledTimes(1); 61 | expect(s).toEqual([12, 23, 34]); 62 | }); 63 | 64 | it("map types", async () => { 65 | const oo = await utils.stream2array( 66 | utils.streamMap(utils.array2stream([1, 2, 3]), { 67 | Map: (chunk, idx) => { 68 | return "[" + chunk + "/" + idx + "]"; 69 | }, 70 | }), 71 | ); 72 | expect(oo).toEqual(["[1/0]", "[2/1]", "[3/2]"]); 73 | }); 74 | 75 | describe("test streaming through streamMap", () => { 76 | const state: streamingTestState = { 77 | sendChunks: 10000, 78 | sendChunkSize: 3, 79 | fillCalls: 0, 80 | CollectorFn: vitest.fn(), 81 | }; 82 | it("does streamMap respect backpressure", async () => { 83 | const ts = new TransformStream(undefined, undefined, { highWaterMark: 2 }); 84 | const reb = utils.streamMap(ts.readable, { 85 | Map: (chunk) => { 86 | for (let i = 0; i < chunk.length; i++) { 87 | chunk[i] = (chunk[i] + 1) % 256; 88 | } 89 | return chunk; 90 | }, 91 | }); 92 | await Promise.all([receiveFromStream(reb, state), sendToStream(ts.writable, state)]); 93 | 94 | expect(state.CollectorFn).toBeCalledTimes(state.sendChunks + 1 /*done*/); 95 | expect(state.CollectorFn.mock.calls.slice(-1)[0][0].done).toBeTruthy(); 96 | let lastfillCalls = 0; 97 | for (let i = 0; i < state.CollectorFn.mock.calls.length - 1 /*done*/; i++) { 98 | const { fillCalls, reBufferCalls, value } = state.CollectorFn.mock.calls[i][0]; 99 | expect(value?.[0]).toBe((i + 1) % 256); 100 | expect(fillCalls * state.sendChunkSize).toBeGreaterThanOrEqual( 101 | (fillCalls - lastfillCalls) * state.sendChunkSize + reBufferCalls * state.sendChunkSize, 102 | ); 103 | lastfillCalls = fillCalls; 104 | } 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /ts/src/base-sys-abstraction.test.ts: -------------------------------------------------------------------------------- 1 | import { WebSysAbstraction } from "@adviser/cement/web"; 2 | import { CFSysAbstraction } from "@adviser/cement/cf"; 3 | import { DenoSysAbstraction } from "@adviser/cement/deno"; 4 | import { NodeSysAbstraction } from "@adviser/cement/node"; 5 | import { runtimeFn } from "./runtime.js"; 6 | import { IDMode, TimeMode, RandomMode } from "./sys-abstraction.js"; 7 | 8 | const abstractions = []; 9 | 10 | if (runtimeFn().isCFWorker) { 11 | abstractions.push({ name: "CFSysAbstraction", fn: CFSysAbstraction }); 12 | } 13 | 14 | if (runtimeFn().isNodeIsh) { 15 | abstractions.push({ name: "NodeSysAbstraction", fn: NodeSysAbstraction }); 16 | } 17 | 18 | if (runtimeFn().isDeno) { 19 | abstractions.push({ name: "DenoSysAbstraction", fn: DenoSysAbstraction }); 20 | } 21 | 22 | if (runtimeFn().isBrowser) { 23 | abstractions.push({ name: "WebSysAbstraction", fn: WebSysAbstraction }); 24 | } 25 | 26 | for (const abstraction of abstractions) { 27 | describe(abstraction.name, () => { 28 | it("IdService UUID", () => { 29 | const sys = abstraction.fn(); 30 | const id1 = sys.NextId(); 31 | const id2 = sys.NextId(); 32 | expect(id1).not.toEqual(id2); 33 | }); 34 | 35 | it("IdService explict UUID", () => { 36 | const sys = abstraction.fn({ IdMode: IDMode.UUID }); 37 | const id1 = sys.NextId(); 38 | const id2 = sys.NextId(); 39 | expect(id1).not.toEqual(id2); 40 | }); 41 | 42 | it("IdService const", () => { 43 | const sys = abstraction.fn({ IdMode: IDMode.CONST }); 44 | const id1 = sys.NextId(); 45 | const id2 = sys.NextId(); 46 | expect(id1).toEqual(id2); 47 | }); 48 | 49 | it("IdService set", () => { 50 | for (let i = 0; i < 10; i++) { 51 | const sys = abstraction.fn({ IdMode: IDMode.STEP }); 52 | const id1 = sys.NextId(); 53 | const id2 = sys.NextId(); 54 | expect(id1).toEqual("STEPId-0"); 55 | expect(id2).toEqual("STEPId-1"); 56 | } 57 | }); 58 | 59 | it("time sleep", async () => { 60 | const sys = abstraction.fn(); 61 | const start = sys.Time().Now(); 62 | await sys.Time().Sleep(100); 63 | expect(sys.Time().TimeSince(start)).toBeGreaterThan(90); 64 | }); 65 | 66 | it("time sleep const", async () => { 67 | const sys = abstraction.fn({ TimeMode: TimeMode.REAL }); 68 | const start = new Date(); 69 | await sys.Time().Sleep(100); 70 | const end = new Date(); 71 | expect(end.getTime() - start.getTime()).toBeGreaterThan(90); 72 | }); 73 | 74 | it("time sleep step", async () => { 75 | const sys = abstraction.fn({ TimeMode: TimeMode.STEP }); 76 | const start = sys.Time().Now(); 77 | await sys.Time().Sleep(86400500); 78 | expect(sys.Time().Now().getTime() - start.getTime()).toEqual(86401500); 79 | }); 80 | 81 | it("const random", () => { 82 | const sys = abstraction.fn({ RandomMode: RandomMode.CONST }); 83 | expect(sys.Random0ToValue(10)).toEqual(5); 84 | expect(sys.Random0ToValue(10)).toEqual(5); 85 | }); 86 | 87 | it("step random", () => { 88 | const sys = abstraction.fn({ RandomMode: RandomMode.STEP }); 89 | expect(sys.Random0ToValue(10000)).toEqual(1); 90 | expect(sys.Random0ToValue(10000)).toEqual(2); 91 | }); 92 | 93 | it("random", () => { 94 | const sys = abstraction.fn({}); 95 | for (let i = 0; i < 100; i++) { 96 | const val = sys.Random0ToValue(10); 97 | expect(val).toBeGreaterThanOrEqual(0); 98 | expect(val).toBeLessThanOrEqual(10); 99 | } 100 | }); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /ts/src/utils/rebuffer.test.ts: -------------------------------------------------------------------------------- 1 | import { utils } from "@adviser/cement"; 2 | import { receiveFromStream, sendToStream, streamingTestState } from "./stream-test-helper.js"; 3 | 4 | it("rechunk empty", async () => { 5 | const chunks = await utils.rebufferArray([], 10); 6 | expect(chunks.length).toEqual(0); 7 | }); 8 | 9 | it("rechunk 0 size", async () => { 10 | const chunks = await utils.rebufferArray([new Uint8Array(0)], 10); 11 | expect(chunks.length).toEqual(0); 12 | }); 13 | 14 | it("rechunk smaller 10", async () => { 15 | const chunks = await utils.rebufferArray([new Uint8Array(3)], 10); 16 | expect(chunks.length).toEqual(1); 17 | expect(chunks[0].length).toEqual(3); 18 | }); 19 | 20 | it("rechunk smaller 10 pack smaller chunks", async () => { 21 | const chunks = await utils.rebufferArray( 22 | Array(7) 23 | .fill(0) 24 | .map((_, i) => { 25 | const o = new Uint8Array(3); 26 | for (let j = 0; j < o.length; j++) { 27 | o[j] = i * o.length + j; 28 | } 29 | return o; 30 | }), 31 | 10, 32 | ); 33 | expect(chunks.length).toEqual(3); 34 | expect(chunks[0].length).toEqual(10); 35 | expect(Array.from(chunks[0]).map((i) => i)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 36 | expect(chunks[1].length).toEqual(10); 37 | expect(Array.from(chunks[1]).map((i) => i)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); 38 | expect(chunks[2].length).toEqual(1); 39 | expect(Array.from(chunks[2]).map((i) => i)).toEqual([20]); 40 | }); 41 | 42 | it("rechunk smaller 10 pack bigger chunks", async () => { 43 | const chunks = await utils.rebufferArray( 44 | Array(3) 45 | .fill(0) 46 | .map((_, i) => { 47 | const o = new Uint8Array(11); 48 | for (let j = 0; j < o.length; j++) { 49 | o[j] = i * o.length + j; 50 | } 51 | return o; 52 | }), 53 | 10, 54 | ); 55 | expect(chunks.length).toEqual(4); 56 | expect(chunks[0].length).toEqual(10); 57 | expect(Array.from(chunks[0]).map((i) => i)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 58 | expect(chunks[1].length).toEqual(10); 59 | expect(Array.from(chunks[1]).map((i) => i)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); 60 | expect(chunks[2].length).toEqual(10); 61 | expect(Array.from(chunks[2]).map((i) => i)).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); 62 | expect(chunks[3].length).toEqual(3); 63 | expect(Array.from(chunks[3]).map((i) => i)).toEqual([30, 31, 32]); 64 | }); 65 | 66 | describe("test streaming through rebuffer", () => { 67 | const state: streamingTestState = { 68 | sendChunks: 10000, 69 | sendChunkSize: 3, 70 | fillCalls: 0, 71 | CollectorFn: vitest.fn(), 72 | }; 73 | const reBufferSize = 11; 74 | 75 | it("does rebuffer respect backpressure", async () => { 76 | const ts = new TransformStream(undefined, undefined, { highWaterMark: 2 }); 77 | const reb = utils.rebuffer(ts.readable, reBufferSize); 78 | await Promise.all([receiveFromStream(reb, state), sendToStream(ts.writable, state)]); 79 | 80 | expect(state.CollectorFn).toBeCalledTimes(~~((state.sendChunkSize * state.sendChunks) / reBufferSize) + 1 + 1 /*done*/); 81 | expect(state.CollectorFn.mock.calls.slice(-1)[0][0].done).toBeTruthy(); 82 | let lastfillCalls = 0; 83 | for (let i = 0; i < state.CollectorFn.mock.calls.length - 1 /*done*/; i++) { 84 | const { fillCalls, reBufferCalls, value } = state.CollectorFn.mock.calls[i][0]; 85 | expect(value?.[0]).toBe(~~((reBufferSize * i) / state.sendChunkSize) % 256); 86 | expect(fillCalls * state.sendChunkSize).toBeGreaterThanOrEqual( 87 | (fillCalls - lastfillCalls) * state.sendChunkSize + reBufferCalls * reBufferSize, 88 | ); 89 | lastfillCalls = fillCalls; 90 | } 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /ts/src/node/node-sys-abstraction.ts: -------------------------------------------------------------------------------- 1 | import { SysAbstraction, SystemService, VoidFunc } from "../sys-abstraction.js"; 2 | import { 3 | BaseSysAbstraction, 4 | ExitHandler, 5 | ExitService, 6 | WrapperSysAbstraction, 7 | WrapperSysAbstractionParams, 8 | } from "../base-sys-abstraction.js"; 9 | import { NodeFileService } from "./node-file-service.js"; 10 | import { Env, envFactory } from "../sys-env.js"; 11 | import { Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 12 | import process from "node:process"; 13 | 14 | export class NodeExitServiceImpl implements ExitService { 15 | constructor() { 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | process.on("unhandledRejection", (reason: string, p: Promise) => { 18 | this.exit(19); 19 | }); 20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 21 | process.on("uncaughtException", (error: Error) => { 22 | this.exit(18); 23 | }); 24 | process.on("close", () => { 25 | this.exit(0); 26 | }); 27 | process.on("exit", () => { 28 | this.exit(0); 29 | }); 30 | process.on("SIGQUIT", () => { 31 | this.exit(3); 32 | }); 33 | process.on("SIGINT", () => { 34 | this.exit(2); 35 | }); 36 | process.on("SIGTERM", () => { 37 | this.exit(9); 38 | }); 39 | } 40 | _exitHandlers: ExitHandler[] = []; 41 | injectExitHandlers(hdls: ExitHandler[]): void { 42 | // console.log("ExitService: injecting exit handlers", hdls) 43 | this._exitHandlers = hdls; 44 | } 45 | invoked = false; 46 | readonly _handleExit = async (): Promise => { 47 | if (this.invoked) { 48 | // console.error("ExitService: already invoked"); 49 | return; 50 | } 51 | this.invoked = true; 52 | for (const h of this._exitHandlers) { 53 | try { 54 | // console.log(`ExitService: calling handler ${h.id}`) 55 | const ret = h.hdl(); 56 | // console.log(`ExitService: called handler ${h.id}`, ret) 57 | if (typeof (ret as Promise).then === "function") { 58 | await ret; 59 | } 60 | } finally { 61 | // ignore 62 | } 63 | } 64 | }; 65 | 66 | exit(code: number): void { 67 | // console.log("ExitService: exit called", code) 68 | this._handleExit() 69 | .then(() => { 70 | process.exit(code); 71 | }) 72 | .catch((err) => { 73 | // eslint-disable-next-line no-console 74 | console.error("ExitService: failed to handle exit", err); 75 | process.exit(code); 76 | }); 77 | } 78 | } 79 | 80 | export class NodeSystemService implements SystemService { 81 | static readonly _exitHandlers: ExitHandler[] = []; 82 | readonly _exitService: ExitService = new NodeExitServiceImpl(); 83 | constructor() { 84 | this._exitService.injectExitHandlers(NodeSystemService._exitHandlers); 85 | } 86 | 87 | Env(): Env { 88 | return envFactory(); 89 | } 90 | 91 | Args(): string[] { 92 | return process.argv; 93 | } 94 | 95 | OnExit(hdl: VoidFunc): VoidFunc { 96 | const id = crypto.randomUUID(); 97 | NodeSystemService._exitHandlers.push({ hdl, id }); 98 | return () => { 99 | const idx = NodeSystemService._exitHandlers.findIndex((h) => h.id === id); 100 | if (idx >= 0) { 101 | NodeSystemService._exitHandlers.splice(idx, 1); 102 | } 103 | }; 104 | } 105 | 106 | Exit(code: number): void { 107 | this._exitService.exit(code); 108 | } 109 | } 110 | 111 | let my: BaseSysAbstraction | undefined = undefined; 112 | export function NodeSysAbstraction(param?: WrapperSysAbstractionParams): SysAbstraction { 113 | if (!my) { 114 | my = new BaseSysAbstraction({ 115 | TxtEnDecoder: param?.TxtEnDecoder || Utf8EnDecoderSingleton(), 116 | FileSystem: new NodeFileService(), 117 | SystemService: new NodeSystemService(), 118 | }); 119 | } 120 | return new WrapperSysAbstraction(my, param); 121 | } 122 | -------------------------------------------------------------------------------- /ts/src/http_header.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpHeader } from "./http_header.js"; 2 | 3 | describe("HttpHeader", () => { 4 | it("Add should join different case headings to case insensitive ", () => { 5 | const h = HttpHeader.from({ 6 | "Content-Type": "application/json", 7 | }); 8 | 9 | h.Add("content-Type", "application/xml"); 10 | expect(h.Get("Content-Type")).toEqual("application/json"); 11 | expect(h.Values("Content-Type")).toEqual(["application/json", "application/xml"]); 12 | }); 13 | it("items should return all items", () => { 14 | const h = new HttpHeader(); 15 | expect(h.Items()).toEqual([]); 16 | h.Add("key", []); 17 | expect(h.Items()).toEqual([]); 18 | h.Add("key", "value"); 19 | expect(h.Items()).toEqual([["key", ["value"]]]); 20 | }); 21 | it("Set and Get should be case insensitive", () => { 22 | const h = HttpHeader.from({ 23 | "Content-Type": "application/json", 24 | }); 25 | 26 | h.Set("content-Type", "application/xml"); 27 | expect(h.Values("Content-Type")).toEqual(["application/xml"]); 28 | expect(h.Values("content-Type")).toEqual(["application/xml"]); 29 | }); 30 | it("Get with empty values should return undefined and empty Items", () => { 31 | const h = new HttpHeader(); 32 | h.Add("key", []); 33 | 34 | expect(h.Get("key")).toBe(undefined); 35 | expect(h.Values("key")).toEqual([]); 36 | 37 | expect(h.Items()).toEqual([]); 38 | }); 39 | 40 | it("from Array", () => { 41 | const h = HttpHeader.from([ 42 | ["Content-Type", "application/json"], 43 | ["Content-Type", "application/xml"], 44 | ["bla", "application/xml"], 45 | ["blub", ["bla", "blub"]], 46 | ] as HeadersInit); 47 | expect(h.SortItems()).toEqual([ 48 | ["bla", ["application/xml"]], 49 | ["blub", ["bla", "blub"]], 50 | ["content-type", ["application/json", "application/xml"]], 51 | ]); 52 | }); 53 | 54 | it("from Object", () => { 55 | const h = HttpHeader.from({ 56 | "Content-Type": "application/json", 57 | "content-Type": "application/xml", 58 | bla: "application/xml", 59 | blub: ["bla", "blub"] as unknown as string, 60 | }); 61 | expect(h.SortItems()).toEqual([ 62 | ["bla", ["application/xml"]], 63 | ["blub", ["bla", "blub"]], 64 | ["content-type", ["application/json", "application/xml"]], 65 | ]); 66 | }); 67 | 68 | it("from Headers", () => { 69 | const header = new Headers(); 70 | header.append("Content-Type", "application/json"); 71 | header.append("content-Type", "application/xml"); 72 | header.append("bla", "application/xml"); 73 | header.append("blub", "bla"); 74 | header.append("bluB", "blub"); 75 | const h = HttpHeader.from(header); 76 | expect(h.Items()).toEqual([ 77 | ["bla", ["application/xml"]], 78 | ["blub", ["bla", "blub"]], 79 | ["content-type", ["application/json", "application/xml"]], 80 | ]); 81 | }); 82 | 83 | it("AbstractHeaders", () => { 84 | const ah = new HttpHeader().AsHeaders(); 85 | ah.append("a", "b"); 86 | expect(Array.from(ah.keys())).toEqual(["a"]); 87 | expect(Array.from(ah.entries())).toEqual([["a", "b"]]); 88 | ah.append("a", "c"); 89 | expect(Array.from(ah.keys())).toEqual(["a"]); 90 | expect(Array.from(ah.entries())).toEqual([["a", "b, c"]]); 91 | ah.append("a", "d, e"); 92 | expect(Array.from(ah.keys())).toEqual(["a"]); 93 | expect(Array.from(ah.entries())).toEqual([["a", "b, c, d, e"]]); 94 | ah.append("v", "w"); 95 | expect(Array.from(ah.keys())).toEqual(["a", "v"]); 96 | expect(Array.from(ah.values())).toEqual(["b, c, d, e", "w"]); 97 | }); 98 | 99 | it("From receives HttpHeader", () => { 100 | const sh = HttpHeader.from(HttpHeader.from({ "Content-Type": "application/json" })); 101 | const h = HttpHeader.from(sh); 102 | expect(h).not.toBe(sh); 103 | expect(h.Items()).toEqual([["content-type", ["application/json"]]]); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /ts/src/deno/deno-sys-abstraction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExitService, 3 | ExitHandler, 4 | BaseSysAbstraction, 5 | WrapperSysAbstractionParams, 6 | WrapperSysAbstraction, 7 | } from "../base-sys-abstraction.js"; 8 | import { SysAbstraction, SystemService, VoidFunc } from "../sys-abstraction.js"; 9 | import { Env, envFactory } from "../sys-env.js"; 10 | import { Utf8EnDecoderSingleton } from "../txt-en-decoder.js"; 11 | // import * as process from "node:process"; 12 | import { DenoFileService } from "./deno-file-service.js"; 13 | 14 | const Deno = (globalThis as unknown as { Deno: unknown }).Deno as { 15 | addSignalListener(sig: string, hdl: () => void): void; 16 | exit(code?: number): void; 17 | args: string[]; 18 | }; 19 | 20 | export class DenoExitServiceImpl implements ExitService { 21 | constructor() { 22 | globalThis.addEventListener("unhandledrejection", (e) => { 23 | e.preventDefault(); 24 | this.exit(19); 25 | }); 26 | globalThis.addEventListener("error", () => { 27 | this.exit(19); 28 | }); 29 | globalThis.addEventListener("uncaughtException", () => { 30 | this.exit(19); 31 | }); 32 | 33 | // process.on("close", () => { 34 | // this.exit(0); 35 | // }); 36 | globalThis.addEventListener("unload", () => { 37 | this.exit(0); 38 | // console.log('goodbye!'); 39 | }); 40 | 41 | // process.on("exit", () => { 42 | // }); 43 | Deno.addSignalListener("SIGQUIT", () => { 44 | this.exit(3); 45 | }); 46 | Deno.addSignalListener("SIGINT", () => { 47 | this.exit(2); 48 | }); 49 | Deno.addSignalListener("SIGTERM", () => { 50 | this.exit(9); 51 | }); 52 | } 53 | _exitHandlers: ExitHandler[] = []; 54 | injectExitHandlers(hdls: ExitHandler[]): void { 55 | // console.log("ExitService: injecting exit handlers", hdls) 56 | this._exitHandlers = hdls; 57 | } 58 | invoked = false; 59 | readonly _handleExit = async (): Promise => { 60 | if (this.invoked) { 61 | // console.error("ExitService: already invoked"); 62 | return; 63 | } 64 | this.invoked = true; 65 | for (const h of this._exitHandlers) { 66 | try { 67 | // console.log(`ExitService: calling handler ${h.id}`) 68 | const ret = h.hdl(); 69 | // console.log(`ExitService: called handler ${h.id}`, ret) 70 | if (typeof (ret as Promise).then === "function") { 71 | await ret; 72 | } 73 | } finally { 74 | // ignore 75 | } 76 | } 77 | }; 78 | 79 | exit(code: number): void { 80 | // console.log("ExitService: exit called", code) 81 | this._handleExit() 82 | .then(() => { 83 | Deno.exit(code); 84 | }) 85 | .catch((err) => { 86 | // eslint-disable-next-line no-console 87 | console.error("ExitService: failed to handle exit", err); 88 | Deno.exit(code); 89 | }); 90 | } 91 | } 92 | 93 | export class DenoSystemService implements SystemService { 94 | static readonly _exitHandlers: ExitHandler[] = []; 95 | readonly _exitService: ExitService = new DenoExitServiceImpl(); 96 | constructor() { 97 | this._exitService.injectExitHandlers(DenoSystemService._exitHandlers); 98 | } 99 | 100 | Env(): Env { 101 | return envFactory(); 102 | } 103 | 104 | Args(): string[] { 105 | return Deno.args; 106 | } 107 | 108 | OnExit(hdl: VoidFunc): VoidFunc { 109 | const id = crypto.randomUUID(); 110 | DenoSystemService._exitHandlers.push({ hdl, id }); 111 | return () => { 112 | const idx = DenoSystemService._exitHandlers.findIndex((h) => h.id === id); 113 | if (idx >= 0) { 114 | DenoSystemService._exitHandlers.splice(idx, 1); 115 | } 116 | }; 117 | } 118 | 119 | Exit(code: number): void { 120 | this._exitService.exit(code); 121 | } 122 | } 123 | 124 | let my: BaseSysAbstraction | undefined = undefined; 125 | export function DenoSysAbstraction(param?: WrapperSysAbstractionParams): SysAbstraction { 126 | if (!my) { 127 | my = new BaseSysAbstraction({ 128 | TxtEnDecoder: param?.TxtEnDecoder || Utf8EnDecoderSingleton(), 129 | FileSystem: new DenoFileService(), 130 | SystemService: new DenoSystemService(), 131 | }); 132 | } 133 | return new WrapperSysAbstraction(my, param); 134 | } 135 | -------------------------------------------------------------------------------- /ts/src/crypto.ts: -------------------------------------------------------------------------------- 1 | export interface CTJsonWebKey { 2 | alg?: string; 3 | crv?: string; 4 | d?: string; 5 | dp?: string; 6 | dq?: string; 7 | e?: string; 8 | ext?: boolean; 9 | k?: string; 10 | key_ops?: string[]; 11 | kty?: string; 12 | n?: string; 13 | oth?: RsaOtherPrimesInfo[]; 14 | p?: string; 15 | q?: string; 16 | qi?: string; 17 | use?: string; 18 | x?: string; 19 | y?: string; 20 | } 21 | 22 | export type CTKeyFormat = "jwk" | "pkcs8" | "raw" | "spki"; 23 | export type CTKeyUsage = "decrypt" | "deriveBits" | "deriveKey" | "encrypt" | "sign" | "unwrapKey" | "verify" | "wrapKey"; 24 | 25 | export interface CTAlgorithm { 26 | name: string; 27 | } 28 | export type CTAlgorithmIdentifier = CTAlgorithm | string; 29 | 30 | export interface CTRsaHashedImportParams extends CTAlgorithm { 31 | hash: CTAlgorithmIdentifier; 32 | } 33 | 34 | export type CTNamedCurve = string; 35 | export interface CTEcKeyImportParams extends CTAlgorithm { 36 | namedCurve: CTNamedCurve; 37 | } 38 | 39 | export interface CTHmacImportParams extends CTAlgorithm { 40 | hash: CTAlgorithmIdentifier; 41 | length?: number; 42 | } 43 | 44 | export interface CTAesKeyAlgorithm extends CTAlgorithm { 45 | length: number; 46 | } 47 | 48 | export type CTKeyType = "private" | "public" | "secret"; 49 | 50 | export interface CTCryptoKey { 51 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) */ 52 | readonly algorithm: CTAlgorithm; 53 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) */ 54 | readonly extractable: boolean; 55 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) */ 56 | readonly type: CTKeyType; 57 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) */ 58 | readonly usages: CTKeyUsage[]; 59 | } 60 | 61 | interface CTArrayBufferTypes { 62 | ArrayBuffer: ArrayBuffer; 63 | } 64 | type CTArrayBufferLike = CTArrayBufferTypes[keyof CTArrayBufferTypes]; 65 | 66 | export interface CTArrayBufferView { 67 | /** 68 | * The ArrayBuffer instance referenced by the array. 69 | */ 70 | buffer: CTArrayBufferLike; 71 | 72 | /** 73 | * The length in bytes of the array. 74 | */ 75 | byteLength: number; 76 | 77 | /** 78 | * The offset in bytes of the array. 79 | */ 80 | byteOffset: number; 81 | } 82 | 83 | export type CTBufferSource = CTArrayBufferView | ArrayBuffer | Uint8Array; 84 | 85 | export interface CryptoRuntime { 86 | importKey( 87 | format: CTKeyFormat, 88 | keyData: CTJsonWebKey | CTBufferSource, 89 | algorithm: CTAlgorithmIdentifier | CTRsaHashedImportParams | CTEcKeyImportParams | CTHmacImportParams | CTAesKeyAlgorithm, 90 | extractable: boolean, 91 | keyUsages: CTKeyUsage[], 92 | ): Promise; 93 | exportKey(format: CTKeyFormat, key: CTCryptoKey): Promise; 94 | 95 | //(format: "raw", key: ArrayBuffer, algo: string, extractable: boolean, usages: string[]) => Promise; 96 | decrypt(algo: { name: string; iv: Uint8Array; tagLength: number }, key: CTCryptoKey, data: Uint8Array): Promise; 97 | encrypt(algo: { name: string; iv: Uint8Array; tagLength: number }, key: CTCryptoKey, data: Uint8Array): Promise; 98 | digestSHA256(data: Uint8Array): Promise; 99 | randomBytes(size: number): Uint8Array; 100 | } 101 | 102 | function randomBytes(size: number): Uint8Array { 103 | const bytes = new Uint8Array(size); 104 | if (size > 0) { 105 | crypto.getRandomValues(bytes); 106 | } 107 | return bytes; 108 | } 109 | 110 | function digestSHA256(data: Uint8Array): Promise { 111 | return Promise.resolve(crypto.subtle.digest("SHA-256", data)); 112 | } 113 | 114 | export function toCryptoRuntime(cryptoOpts: Partial = {}): CryptoRuntime { 115 | const runtime = { 116 | importKey: cryptoOpts.importKey || crypto.subtle.importKey.bind(crypto.subtle), 117 | exportKey: cryptoOpts.exportKey || crypto.subtle.exportKey.bind(crypto.subtle), 118 | encrypt: cryptoOpts.encrypt || crypto.subtle.encrypt.bind(crypto.subtle), 119 | decrypt: cryptoOpts.decrypt || crypto.subtle.decrypt.bind(crypto.subtle), 120 | randomBytes: cryptoOpts.randomBytes || randomBytes, 121 | digestSHA256: cryptoOpts.digestSHA256 || digestSHA256, 122 | }; 123 | // console.log("cryptoOpts", cryptoOpts, opts) 124 | return runtime; 125 | } 126 | -------------------------------------------------------------------------------- /ts/src/result.ts: -------------------------------------------------------------------------------- 1 | export abstract class Result { 2 | static Ok(t: T): Result { 3 | return new ResultOK(t); 4 | } 5 | static Err(t: E | string | Result): Result { 6 | if (typeof t === "string") { 7 | return new ResultError(new Error(t) as E); 8 | } 9 | if (Result.Is(t)) { 10 | if (t.is_ok()) { 11 | return new ResultError(new Error("Result Error is Ok") as E); 12 | } 13 | return t as Result; 14 | } 15 | return new ResultError(t); 16 | } 17 | static Is(t: unknown): t is Result { 18 | if (!t) { 19 | return false; 20 | } 21 | if (t instanceof Result) { 22 | return true; 23 | } 24 | const rt = t as Result; 25 | if ([typeof rt.is_ok, typeof rt.is_err, typeof rt.unwrap, typeof rt.unwrap_err].every((x) => x === "function")) { 26 | return true; 27 | } 28 | return false; 29 | } 30 | 31 | isOk(): boolean { 32 | return this.is_ok(); 33 | } 34 | isErr(): boolean { 35 | return this.is_err(); 36 | } 37 | 38 | Ok(): T { 39 | return this.unwrap(); 40 | } 41 | Err(): E { 42 | return this.unwrap_err(); 43 | } 44 | 45 | abstract is_ok(): boolean; 46 | abstract is_err(): boolean; 47 | abstract unwrap(): T; 48 | abstract unwrap_err(): E; 49 | } 50 | 51 | export class ResultOK extends Result { 52 | private _t: T; 53 | constructor(t: T) { 54 | super(); 55 | this._t = t; 56 | } 57 | is_ok(): boolean { 58 | return true; 59 | } 60 | is_err(): boolean { 61 | return false; 62 | } 63 | unwrap_err(): Error { 64 | throw new Error("Result is Ok"); 65 | } 66 | unwrap(): T { 67 | return this._t; 68 | } 69 | } 70 | 71 | export class ResultError extends Result { 72 | private _error: T; 73 | constructor(t: T) { 74 | super(); 75 | this._error = t; 76 | } 77 | is_ok(): boolean { 78 | return false; 79 | } 80 | is_err(): boolean { 81 | return true; 82 | } 83 | unwrap(): never { 84 | throw new Error(`Result is Err: ${this._error}`); 85 | } 86 | unwrap_err(): T { 87 | return this._error; 88 | } 89 | } 90 | 91 | export type WithoutResult = T extends Result ? U : T; 92 | 93 | // type WithoutPromise = T extends Promise ? U : T; 94 | type WithResult = T extends Promise ? Promise> : Result; 95 | 96 | export function exception2Result Promise | T, T>(fn: FN): WithResult> { 97 | try { 98 | const res = fn(); 99 | if (res instanceof Promise) { 100 | return res.then((value) => Result.Ok(value)).catch((e) => Result.Err(e)) as WithResult>; 101 | } 102 | return Result.Ok(res) as WithResult>; 103 | } catch (e) { 104 | return Result.Err(e as Error) as WithResult>; 105 | } 106 | } 107 | 108 | /* 109 | 110 | type FinalizedResult = { 111 | result: T; 112 | scopeResult?: Result; 113 | finally: () => Promise; 114 | } 115 | 116 | type exection2ResultParam = { 117 | init: () => Promise; 118 | inScope?: (t: T) => Promise; 119 | cleanup: (t: T) => Promise; 120 | 121 | } 122 | 123 | async function expection2Result({fn, inScope, cleanup}: exection2ResultParam): Promise>> { 124 | try { 125 | const res = await fn(); 126 | if (inScope) { 127 | try { 128 | await inScope?.(res) 129 | } catch (err) { 130 | return Result.Err(err as Error) 131 | } 132 | await cleanup(res) 133 | return Result.Ok({ 134 | result: res, 135 | finally: async () => { } 136 | }) 137 | } 138 | return Result.Ok({ 139 | result: res , 140 | finally: async () => { 141 | return cleanup(res) 142 | } 143 | }) 144 | } catch (err) { 145 | return Result.Err(err as Error) 146 | } 147 | } 148 | */ 149 | 150 | // await expection2Result({ 151 | // init: openDB, 152 | // inScope: (res) => { 153 | // res.query() 154 | // }, 155 | // cleanup: async (y) => { 156 | // await y.close() 157 | // } 158 | // }) 159 | // async function openDB() { 160 | // try { 161 | // const opendb = await openDB() 162 | // return Result.Ok({ 163 | // openDB, 164 | // finally: async () => { 165 | // await opendb.close() 166 | // }}) 167 | // } catch (err) { 168 | // return Result.Err(err) 169 | // } 170 | // } 171 | // } 172 | -------------------------------------------------------------------------------- /ts/src/utils/relative-path.ts: -------------------------------------------------------------------------------- 1 | export enum PartType { 2 | Slash = 0x1, 3 | Root = 0x3, 4 | Up = 0x4 /* /../ */, 5 | Noop = 0x8 /* ./ */, 6 | // RootUp = 0x8 /* ../ */, 7 | } 8 | 9 | export type PathItem = string | PartType; 10 | 11 | export class Path { 12 | readonly parts: PathItem[]; 13 | constructor(parts: PathItem[] = []) { 14 | this.parts = parts; 15 | } 16 | 17 | toString(): string { 18 | return this.parts 19 | .map((part) => { 20 | if (typeof part === "string") { 21 | return part; 22 | } else { 23 | switch (part) { 24 | case PartType.Slash: 25 | case PartType.Root: 26 | return "/"; 27 | case PartType.Up: 28 | return ".."; 29 | default: 30 | return part; 31 | } 32 | } 33 | }) 34 | .join(""); 35 | } 36 | 37 | add(part: PathItem): void { 38 | if (this.parts.includes(PartType.Root) && part === PartType.Root) { 39 | throw new Error("Cannot add absolute part to absolute path"); 40 | } 41 | const last = this.parts[this.parts.length - 1] as PartType; 42 | if (last & PartType.Slash && part === PartType.Slash) { 43 | return; 44 | } 45 | switch (part) { 46 | case ".": 47 | this.parts.push(PartType.Noop); 48 | return; 49 | case "..": 50 | part = PartType.Up; 51 | } 52 | 53 | // if (part === PartType.Up && last === PartType.Slash) { 54 | // this.parts[this.parts.length - 1] = PartType.Up 55 | // return 56 | // } 57 | if (last === PartType.Noop && part === PartType.Slash) { 58 | if (last === PartType.Noop) { 59 | this.parts.pop(); 60 | } 61 | return; 62 | } 63 | this.parts.push(part); 64 | } 65 | } 66 | 67 | export function splitPath(path: string): Path { 68 | const p = new Path(); 69 | if (path === "") { 70 | return p; 71 | } 72 | for (let count = 0; path.length; count++) { 73 | // const ipath = path 74 | if (path.match(/^\/+/)) { 75 | if (count === 0) { 76 | p.add(PartType.Root); 77 | } else { 78 | p.add(PartType.Slash); 79 | } 80 | path = path.replace(/^\/+/, ""); 81 | } else { 82 | const part = path.replace(/\/.*$/, ""); 83 | p.add(part); 84 | path = path.replace(/^[^/]+/, ""); 85 | } 86 | } 87 | return p; 88 | } 89 | 90 | export function pathJoin(...paths: string[]): string { 91 | let prev = ""; 92 | const res: string[] = []; 93 | // eslint-disable-next-line @typescript-eslint/prefer-for-of 94 | for (let i = 0; i < paths.length; i++) { 95 | const path = paths[i]; 96 | if (path === "") { 97 | continue; 98 | } 99 | // i + 1 !== paths.length && 100 | if (!(prev.endsWith("/") || path.startsWith("/"))) { 101 | if (prev !== "") { 102 | res.push("/"); 103 | } 104 | res.push(path); 105 | } else { 106 | res.push(path); 107 | } 108 | prev = path; 109 | } 110 | return res.join(""); 111 | } 112 | 113 | export function relativePath(path: string, relative: string): string { 114 | const relativeParts = splitPath(relative); 115 | let result: string; 116 | if (relativeParts.parts[0] === PartType.Root) { 117 | result = relative; 118 | } else { 119 | result = pathJoin(path, relative); 120 | } 121 | const unoptPath = splitPath(result); 122 | // console.log("What", result, unoptPath.parts) 123 | const out: PathItem[] = []; 124 | let topUp = false; 125 | for (const part of unoptPath.parts) { 126 | switch (part) { 127 | case PartType.Root: 128 | out.push(PartType.Root); 129 | break; 130 | case PartType.Up: 131 | if (out.length && !topUp) { 132 | const last = out.length - 1; 133 | if (typeof out[last] === "string" && (out[last - 1] as PartType) == PartType.Root) { 134 | out.pop(); 135 | } else { 136 | out.pop(); 137 | out.pop(); 138 | } 139 | if (out.length === 0) { 140 | topUp = !topUp ? true : topUp; 141 | out.push(PartType.Up); 142 | } 143 | } else { 144 | out.push(PartType.Up); 145 | } 146 | break; 147 | case PartType.Slash: 148 | if (!((out[out.length - 1] as PartType) & PartType.Slash)) { 149 | out.push(PartType.Slash); 150 | } 151 | break; 152 | default: 153 | out.push(part); 154 | break; 155 | } 156 | } 157 | return new Path(out).toString(); 158 | // return pathParts 159 | // .filter((part, index) => part !== relativeParts[index]) 160 | // .join("") 161 | } 162 | -------------------------------------------------------------------------------- /ts/src/http_header.ts: -------------------------------------------------------------------------------- 1 | export class HeadersImpl extends Headers { 2 | readonly _headers: Map; 3 | 4 | constructor(init: Map) { 5 | super(); 6 | this._headers = init; 7 | } 8 | 9 | override [Symbol.iterator](): IterableIterator<[string, string]> { 10 | return this.entries(); 11 | } 12 | 13 | override entries(): IterableIterator<[string, string]> { 14 | return this._headers.entries(); 15 | } 16 | override keys(): IterableIterator { 17 | return this._headers.keys(); 18 | } 19 | override values(): IterableIterator { 20 | return this._headers.values(); 21 | } 22 | 23 | override append(key: string, value: string | string[] | undefined): HeadersImpl { 24 | const values = this._headers.get(key); 25 | if (typeof value === "undefined") { 26 | value = ""; 27 | } 28 | if (Array.isArray(value)) { 29 | this._headers.set(key, [values, ...value].filter((i) => i).join(", ")); 30 | } else { 31 | this._headers.set(key, [values, value].filter((i) => i).join(", ")); 32 | } 33 | return this; 34 | } 35 | } 36 | 37 | export class HttpHeader { 38 | readonly _headers: Map = new Map(); 39 | 40 | static from(headers?: HeadersInit | NodeJS.Dict | Headers | HttpHeader): HttpHeader { 41 | if (headers instanceof HttpHeader) { 42 | return headers.Clone(); 43 | } 44 | const h = new HttpHeader(); 45 | if (headers) { 46 | if (Array.isArray(headers)) { 47 | for (const [k, v] of headers as [string, string][]) { 48 | if (v) { 49 | h.Add(k, v); 50 | } 51 | } 52 | } else if (headers instanceof Headers) { 53 | for (const [k, v] of headers.entries()) { 54 | if (v) { 55 | h.Add( 56 | k, 57 | v.split(",").map((v) => v.trim()), 58 | ); 59 | } 60 | } 61 | } else { 62 | for (const k in headers) { 63 | const v = (headers as Record)[k]; 64 | (Array.isArray(v) ? v : [v]).forEach((v) => { 65 | h.Add(k, v); 66 | }); 67 | } 68 | } 69 | } 70 | return h; 71 | } 72 | 73 | _asStringString(): Map { 74 | const ret = new Map(); 75 | for (const [key, values] of this._headers) { 76 | ret.set(key, values.join(", ")); 77 | } 78 | return ret; 79 | } 80 | 81 | _key(key: string): string { 82 | return key.toLowerCase(); 83 | } 84 | Values(key: string): string[] { 85 | const values = this._headers.get(this._key(key)); 86 | return values || []; 87 | } 88 | Get(key: string): string | undefined { 89 | const values = this._headers.get(this._key(key)); 90 | if (values === undefined || values.length === 0) { 91 | return undefined; 92 | } 93 | return values[0]; 94 | } 95 | Set(key: string, valueOr: string | string[]): HttpHeader { 96 | const value = Array.isArray(valueOr) ? valueOr : [valueOr]; 97 | this._headers.set(this._key(key), value); 98 | return this; 99 | } 100 | Add(key: string, value: string | string[] | undefined): HttpHeader { 101 | if (typeof value === "undefined") { 102 | return this; 103 | } 104 | const vs = Array.isArray(value) ? value : [value]; 105 | const values = this._headers.get(this._key(key)); 106 | if (values === undefined) { 107 | this._headers.set(this._key(key), vs); 108 | } else { 109 | values.push(...vs); 110 | } 111 | return this; 112 | } 113 | Del(ey: string): HttpHeader { 114 | this._headers.delete(this._key(ey)); 115 | return this; 116 | } 117 | Items(): [string, string[]][] { 118 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 119 | return Array.from(this._headers).filter(([_, vs]) => vs.length > 0); 120 | } 121 | SortItems(): [string, string[]][] { 122 | return this.Items().sort(([[a]], [[b]]) => a.localeCompare(b)); 123 | } 124 | Clone(): HttpHeader { 125 | const clone = new HttpHeader(); 126 | for (const [key, values] of this._headers.entries()) { 127 | clone._headers.set(key, values.slice()); 128 | } 129 | return clone; 130 | } 131 | AsRecordStringStringArray(): Record { 132 | const obj: Record = {}; 133 | for (const [key, values] of this._headers.entries()) { 134 | obj[key] = [...values]; 135 | } 136 | return obj; 137 | } 138 | AsRecordStringString(): Record { 139 | const obj: Record = {}; 140 | for (const [key, values] of this._headers.entries()) { 141 | obj[key] = values.join(", "); 142 | } 143 | return obj; 144 | } 145 | AsHeaderInit(): HeadersInit { 146 | const obj: HeadersInit = {}; 147 | for (const [key, values] of this._headers.entries()) { 148 | obj[key] = values[0]; 149 | } 150 | return obj; 151 | } 152 | AsHeaders(): Headers { 153 | return new HeadersImpl(this._asStringString()); 154 | } 155 | Merge(other?: HttpHeader): HttpHeader { 156 | const ret = this.Clone(); 157 | if (other) { 158 | for (const [key, values] of other.Items()) { 159 | ret.Add(key, values); 160 | } 161 | } 162 | return ret; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /ts/src/resolve-once.ts: -------------------------------------------------------------------------------- 1 | import { Future } from "./future.js"; 2 | 3 | interface ResolveSeqItem { 4 | readonly future: Future; 5 | readonly fn: (c: C) => Promise; 6 | readonly id?: number; 7 | } 8 | 9 | export class ResolveSeq { 10 | readonly ctx: C; 11 | constructor(ctx?: C) { 12 | this.ctx = ctx as C; 13 | } 14 | reset(): void { 15 | /* noop */ 16 | } 17 | _step(item?: ResolveSeqItem): Promise { 18 | if (!item) { 19 | // done 20 | return Promise.resolve(); 21 | } 22 | item 23 | .fn(this.ctx) 24 | .then((value) => item.future.resolve(value)) 25 | .catch((e) => item.future.reject(e as Error)) 26 | .finally(() => { 27 | this._seqFutures.shift(); 28 | void this._step(this._seqFutures[0]); 29 | }); 30 | return Promise.resolve(); 31 | } 32 | readonly _seqFutures: ResolveSeqItem[] = []; 33 | async add(fn: (c: C) => Promise, id?: number): Promise { 34 | const future = new Future(); 35 | this._seqFutures.push({ future, fn, id }); 36 | if (this._seqFutures.length === 1) { 37 | void this._step(this._seqFutures[0]); 38 | } 39 | return future.asPromise(); 40 | } 41 | } 42 | 43 | export class ResolveOnce { 44 | _onceDone = false; 45 | readonly _onceFutures: Future[] = []; 46 | _onceOk = false; 47 | _onceValue?: T; 48 | _onceError?: Error; 49 | _isPromise = false; 50 | 51 | readonly ctx: CTX; 52 | 53 | constructor(ctx?: CTX) { 54 | this.ctx = ctx as CTX; 55 | } 56 | 57 | get ready(): boolean { 58 | return this._onceDone; 59 | } 60 | 61 | reset(): void { 62 | this._onceDone = false; 63 | this._onceOk = false; 64 | this._onceValue = undefined; 65 | this._onceError = undefined; 66 | this._onceFutures.length = 0; 67 | } 68 | 69 | // T extends Option ? U : T 70 | once(fn: (c: CTX) => R): R { 71 | if (this._onceDone) { 72 | if (this._onceError) { 73 | if (this._isPromise) { 74 | return Promise.reject(this._onceError) as unknown as R; 75 | } else { 76 | throw this._onceError; 77 | } 78 | } 79 | if (this._onceOk) { 80 | if (this._isPromise) { 81 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 82 | return Promise.resolve(this._onceValue!) as unknown as R; 83 | } else { 84 | return this._onceValue as unknown as R; 85 | } 86 | } 87 | throw new Error("ResolveOnce.once impossible"); 88 | } 89 | const future = new Future(); 90 | this._onceFutures.push(future); 91 | if (this._onceFutures.length === 1) { 92 | const okFn = (value: T): void => { 93 | this._onceValue = value; 94 | this._onceOk = true; 95 | this._onceDone = true; 96 | if (this._isPromise) { 97 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 98 | this._onceFutures.forEach((f) => f.resolve(this._onceValue!)); 99 | } 100 | this._onceFutures.length = 0; 101 | }; 102 | const catchFn = (e: Error): void => { 103 | this._onceError = e; 104 | this._onceOk = false; 105 | this._onceValue = undefined; 106 | this._onceDone = true; 107 | if (this._isPromise) { 108 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 109 | this._onceFutures.forEach((f) => f.reject(this._onceError!)); 110 | } 111 | this._onceFutures.length = 0; 112 | }; 113 | try { 114 | const ret = fn(this.ctx); 115 | if (typeof (ret as Promise).then === "function") { 116 | this._isPromise = true; 117 | (ret as Promise).then(okFn).catch(catchFn); 118 | } else { 119 | okFn(ret as unknown as T); 120 | } 121 | } catch (e) { 122 | catchFn(e as Error); 123 | } 124 | } 125 | if (this._isPromise) { 126 | return future.asPromise() as unknown as R; 127 | } else { 128 | // abit funky but i don't want to impl the return just once 129 | return this.once(fn); 130 | } 131 | } 132 | } 133 | 134 | export class Keyed void }, K = string> { 135 | private readonly _map = new Map(); 136 | 137 | readonly factory: (key: K) => T; 138 | constructor(factory: (key: K) => T) { 139 | this.factory = factory; 140 | } 141 | 142 | async asyncGet(key: () => Promise): Promise { 143 | return this.get(await key()); 144 | } 145 | 146 | get(key: K | (() => K)): T { 147 | if (typeof key === "function") { 148 | key = (key as () => K)(); 149 | } 150 | let keyed = this._map.get(key); 151 | if (!keyed) { 152 | keyed = this.factory(key); 153 | this._map.set(key, keyed); 154 | } 155 | return keyed; 156 | } 157 | 158 | unget(key: K): void { 159 | const keyed = this._map.get(key); 160 | keyed?.reset(); 161 | this._map.delete(key); 162 | } 163 | 164 | reset(): void { 165 | this._map.forEach((keyed) => keyed.reset()); 166 | this._map.clear(); 167 | } 168 | } 169 | 170 | export class KeyedResolvOnce extends Keyed, K> { 171 | constructor() { 172 | super((key) => new ResolveOnce(key)); 173 | } 174 | } 175 | 176 | export class KeyedResolvSeq extends Keyed, K> { 177 | constructor() { 178 | super((key) => new ResolveSeq(key)); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /ts/src/sys-env.test.ts: -------------------------------------------------------------------------------- 1 | import { Env, EnvActions, EnvFactoryOpts, EnvImpl, envFactory, registerEnvAction, runtimeFn } from "@adviser/cement"; 2 | import { CFEnvActions } from "./cf/cf-env-actions.js"; 3 | import { BrowserEnvActions } from "./web/web-env-actions.js"; 4 | import { param } from "./utils/get-params-result.js"; 5 | 6 | describe("sys_env", () => { 7 | let key: string; 8 | const envImpl = envFactory(); 9 | beforeEach(() => { 10 | key = `key-${Math.random()}`; 11 | }); 12 | it("actions", () => { 13 | expect(envImpl.get(key)).toBeUndefined(); 14 | envImpl.set(key, "value"); 15 | expect(envImpl.get(key)).toBe("value"); 16 | envImpl.set(key); 17 | expect(envImpl.get(key)).toBe("value"); 18 | envImpl.delete(key); 19 | expect(envImpl.get(key)).toBeUndefined(); 20 | }); 21 | it("preset", () => { 22 | const env = new EnvImpl(BrowserEnvActions.new({}), { 23 | presetEnv: new Map([[key, "value"]]), 24 | }); 25 | expect(env.get(key)).toBe("value"); 26 | env.delete(key); 27 | expect(env.get(key)).toBeUndefined(); 28 | }); 29 | it("onSet wild card", () => { 30 | const fn = vi.fn(); 31 | envImpl.onSet(fn); 32 | expect(fn).toBeCalledTimes(envImpl.keys().length); 33 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 34 | expect(fn.mock.calls.map((i) => i[0]).sort()).toEqual(envImpl.keys().sort()); 35 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 36 | expect(fn.mock.calls.map((i) => i[1]).sort()).toEqual( 37 | envImpl 38 | .keys() 39 | .map((i) => envImpl.get(i)) 40 | .sort(), 41 | ); 42 | }); 43 | it("onSet filter", () => { 44 | const env = new EnvImpl(BrowserEnvActions.new({}), { 45 | presetEnv: new Map([[key, "value"]]), 46 | }); 47 | const fn = vi.fn(); 48 | env.onSet(fn, key); 49 | expect(fn).toBeCalledTimes(1); 50 | expect(fn.mock.calls[0]).toEqual([key, "value"]); 51 | env.set(key, "value2"); 52 | expect(fn).toBeCalledTimes(2); 53 | expect(fn.mock.calls[1]).toEqual([key, "value2"]); 54 | env.delete(key); 55 | expect(fn).toBeCalledTimes(3); 56 | expect(fn.mock.calls[2]).toEqual([key, undefined]); 57 | }); 58 | it("test register", () => { 59 | class TestEnvActions implements EnvActions { 60 | readonly #map: Map; 61 | 62 | constructor(opts: Partial) { 63 | this.#map = opts.presetEnv || new Map(); 64 | } 65 | 66 | register(env: Env): Env { 67 | return env; 68 | } 69 | 70 | active(): boolean { 71 | return true; 72 | } 73 | keys(): string[] { 74 | return Array.from(this.#map.keys()); 75 | } 76 | get(key: string): string | undefined { 77 | return this.#map.get(key); 78 | } 79 | set(key: string, value?: string): void { 80 | if (value) { 81 | this.#map.set(key, value); 82 | } 83 | } 84 | delete(key: string): void { 85 | this.#map.delete(key); 86 | } 87 | } 88 | let tea: TestEnvActions = {} as TestEnvActions; 89 | const unreg = registerEnvAction((opts) => { 90 | tea = new TestEnvActions(opts); 91 | return tea; 92 | }); 93 | const env = envFactory({ 94 | presetEnv: new Map([[key, "value"]]), 95 | }); 96 | expect(env.get(key)).toBe("value"); 97 | expect(tea.get(key)).toBe("value"); 98 | unreg(); 99 | }); 100 | if (runtimeFn().isCFWorker) { 101 | it("CFWorker env", () => { 102 | const env = envFactory(); 103 | const onSet = vi.fn(); 104 | env.onSet(onSet); 105 | 106 | CFEnvActions.inject({ "cf-key": "cf-value" }); 107 | 108 | env.set("cf-key-1", "cf-value-2"); 109 | 110 | expect(onSet).toBeCalledWith("cf-key", "cf-value"); 111 | expect(env.get("cf-key")).toBe("cf-value"); 112 | }); 113 | } 114 | it("gets ok", () => { 115 | const res0 = envImpl.gets({ key: param.REQUIRED }); 116 | expect(res0.isErr()).toBeTruthy(); 117 | envImpl.set("key", "value"); 118 | const res = envImpl.gets({ key: param.REQUIRED }); 119 | expect(res.isOk()).toBeTruthy(); 120 | expect(res.unwrap()).toEqual({ key: "value" }); 121 | }); 122 | 123 | it("gets error", () => { 124 | envImpl.set("key", "value"); 125 | const res = envImpl.gets({ 126 | unk1: param.REQUIRED, 127 | unk2: param.REQUIRED, 128 | key: param.REQUIRED, 129 | }); 130 | expect(res.isErr()).toBeTruthy(); 131 | expect(res.Err().message).toEqual("missing parameters: unk1,unk2"); 132 | }); 133 | 134 | it("sets array flat tuple", () => { 135 | envImpl.sets(["key1", "value1"], ["key2", "value2"]); 136 | expect(envImpl.get("key1")).toBe("value1"); 137 | expect(envImpl.get("key2")).toBe("value2"); 138 | }); 139 | it("sets array array tuple", () => { 140 | envImpl.sets([ 141 | ["key1", "value1"], 142 | ["key2", "value2"], 143 | ]); 144 | expect(envImpl.get("key1")).toBe("value1"); 145 | expect(envImpl.get("key2")).toBe("value2"); 146 | }); 147 | 148 | it("sets object", () => { 149 | envImpl.sets({ 150 | key1: "value1", 151 | key2: "value2", 152 | }); 153 | expect(envImpl.get("key1")).toBe("value1"); 154 | expect(envImpl.get("key2")).toBe("value2"); 155 | }); 156 | 157 | it("sets iterator", () => { 158 | envImpl.sets( 159 | new Map( 160 | Object.entries({ 161 | key1: "value1", 162 | key2: "value2", 163 | }), 164 | ).entries(), 165 | ); 166 | expect(envImpl.get("key1")).toBe("value1"); 167 | expect(envImpl.get("key2")).toBe("value2"); 168 | }); 169 | 170 | it("sets combination", () => { 171 | envImpl.sets( 172 | new Map( 173 | Object.entries({ 174 | key1: "value1", 175 | key2: "value2", 176 | }), 177 | ).entries(), 178 | { 179 | key3: "value3", 180 | key4: "value4", 181 | }, 182 | ["key5", "value5"], 183 | ["key6", "value6"], 184 | [ 185 | ["key7", "value7"], 186 | ["key8", "value8"], 187 | ], 188 | ); 189 | expect(envImpl.get("key1")).toBe("value1"); 190 | expect(envImpl.get("key2")).toBe("value2"); 191 | expect(envImpl.get("key3")).toBe("value3"); 192 | expect(envImpl.get("key4")).toBe("value4"); 193 | expect(envImpl.get("key5")).toBe("value5"); 194 | expect(envImpl.get("key6")).toBe("value6"); 195 | expect(envImpl.get("key7")).toBe("value7"); 196 | expect(envImpl.get("key8")).toBe("value8"); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /ts/src/tracer.ts: -------------------------------------------------------------------------------- 1 | import type { MarkWritable } from "ts-essentials"; 2 | import { Time } from "./time.js"; 3 | import { Logger } from "./logger.js"; 4 | 5 | export type TraceCtx = { 6 | readonly spanId: string; 7 | readonly time: Time; 8 | readonly parent: TraceNode; 9 | readonly metrics: Map>; 10 | readonly logger?: Logger; 11 | } & Record; 12 | 13 | export type CleanCtx = { 14 | readonly spanId: string; 15 | } & Record; 16 | 17 | export type TraceCtxParam = { 18 | readonly spanId: string; 19 | } & Partial<{ 20 | readonly time: Time; 21 | readonly parent: TraceNode; 22 | readonly logger: Logger; 23 | }> & 24 | Record; 25 | 26 | export class Metric { 27 | value?: T; 28 | readonly path: string; 29 | 30 | constructor(path: string) { 31 | this.path = path; 32 | } 33 | 34 | set(value: T): void { 35 | this.value = value; 36 | } 37 | 38 | add>(value: R): void { 39 | if (typeof value === "number") { 40 | if (this.value === undefined) { 41 | this.value = 0 as T; 42 | } 43 | this.value = ((this.value as number) + value) as T; 44 | } else if (Array.isArray(value)) { 45 | if (!Array.isArray(this.value)) { 46 | this.value = [] as T; 47 | } 48 | (this.value as T[]).push(...value); 49 | } else { 50 | throw new Error("add only support number or array"); 51 | } 52 | } 53 | } 54 | 55 | export type MetricMap = Map>; 56 | 57 | export class Metrics { 58 | readonly tracenode: TraceNode; 59 | private readonly map: MetricMap; 60 | 61 | readonly spanRefs: MetricMap = new Map>(); 62 | constructor(tracenode: TraceNode) { 63 | this.tracenode = tracenode; 64 | this.map = tracenode.ctx.metrics; 65 | } 66 | 67 | toJSON(): Record { 68 | const obj: Record = {}; 69 | for (const [key, value] of this.map) { 70 | obj[key] = value.value; 71 | } 72 | return obj; 73 | } 74 | 75 | get(ipath: string): Metric { 76 | const path = ipath.replace(/[/]+/g, "/").trim(); 77 | if (path.startsWith("/")) { 78 | if (path.slice(1).length === 0) { 79 | throw new Error(`Metrics path must contain value /:${path}`); 80 | } 81 | let metric = this.map.get(path); 82 | if (!metric) { 83 | metric = new Metric(path); 84 | this.map.set(path, metric); 85 | } 86 | this.spanRefs.set(path, metric); 87 | return metric as Metric; 88 | } else if (path.includes("/")) { 89 | throw new Error(`Metrics path must start with /:${path}`); 90 | } 91 | const rootPath = this.tracenode.getRootPath(); 92 | return this.get(`${rootPath}/${path}`); 93 | } 94 | } 95 | 96 | export interface Invokaction { 97 | readonly result: "success" | "error"; 98 | readonly start: number; 99 | readonly end: number; 100 | readonly metrics?: Metrics; 101 | } 102 | 103 | export type TraceNodeMap = Map; 104 | 105 | export class TraceNode { 106 | readonly childs: TraceNodeMap = new Map(); 107 | 108 | readonly invokations: Invokaction[] = []; 109 | 110 | readonly spanId: string; 111 | readonly ctx: TraceCtx; 112 | readonly metrics: Metrics; 113 | 114 | static root(time: Time, logger?: Logger): TraceNode { 115 | return new TraceNode({ 116 | spanId: "root", 117 | time, 118 | logger, 119 | metrics: new Map(), 120 | parent: undefined as unknown as TraceNode, 121 | }); 122 | } 123 | 124 | constructor(ctx: TraceCtx) { 125 | this.spanId = ctx.spanId; 126 | this.ctx = ctx; 127 | this.metrics = new Metrics(this); 128 | } 129 | 130 | getRootPath(rpath: string[] = []): string { 131 | if (!this.ctx.parent) { 132 | return "/" + rpath.reverse().join("/"); 133 | } 134 | return this.ctx.parent.getRootPath(rpath.concat(this.ctx.spanId)); 135 | } 136 | 137 | invokes(): { ctx: CleanCtx; invokations: Invokaction[] } { 138 | const cleanCtx = { ...this.ctx } as CleanCtx; 139 | delete cleanCtx.parent; 140 | delete cleanCtx.time; 141 | delete cleanCtx.logger; 142 | delete cleanCtx.metrics; 143 | const spanRefs = this.metrics.toJSON.call({ map: this.metrics.spanRefs }); 144 | const metricsRefs = Object.keys(spanRefs).length > 0 ? { metricRefs: spanRefs } : {}; 145 | return { 146 | ctx: cleanCtx, 147 | invokations: this.invokations, 148 | ...metricsRefs, 149 | }; 150 | } 151 | 152 | ctxWith(spanId: string, logger?: Logger): TraceCtxParam { 153 | const ctx = { 154 | ...this.ctx, 155 | spanId, 156 | }; 157 | if (logger) { 158 | ctx.logger = logger; 159 | } 160 | return ctx; 161 | } 162 | 163 | // Promise | T, T>(id: string, fn: V): ReturnType 164 | span Promise | T, T>(inSpanId: string | TraceCtxParam, fn: V): ReturnType { 165 | let ctx: TraceCtx; 166 | if (typeof inSpanId === "string") { 167 | ctx = { 168 | ...this.ctx, 169 | spanId: inSpanId, 170 | parent: this, 171 | }; 172 | } else { 173 | ctx = { 174 | ...this.ctx, 175 | ...inSpanId, 176 | parent: this, 177 | }; 178 | } 179 | if (ctx.logger) { 180 | ctx = { 181 | ...ctx, 182 | ...ctx.logger.Attributes(), 183 | }; 184 | } 185 | const spanId = ctx.spanId; 186 | let spanTrace = this.childs.get(spanId); 187 | if (!spanTrace) { 188 | spanTrace = new TraceNode(ctx); 189 | this.childs.set(spanId.toString(), spanTrace); 190 | } 191 | const invokation: MarkWritable, "result"> = { 192 | start: this.ctx.time.Now().getTime(), 193 | end: 0, 194 | result: "success", 195 | }; 196 | spanTrace.invokations.push(invokation); 197 | try { 198 | const possiblePromise = fn(spanTrace); 199 | if (possiblePromise instanceof Promise) { 200 | return possiblePromise 201 | .then((v) => { 202 | return v; 203 | }) 204 | .catch((e) => { 205 | invokation.result = "error"; 206 | throw e; 207 | }) 208 | .finally(() => { 209 | invokation.end = this.ctx.time.Now().getTime(); 210 | }) as ReturnType; 211 | } 212 | invokation.end = this.ctx.time.Now().getTime(); 213 | return possiblePromise as ReturnType; 214 | } catch (e) { 215 | invokation.result = "error"; 216 | invokation.end = this.ctx.time.Now().getTime(); 217 | throw e; 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /ts/src/sys-env.ts: -------------------------------------------------------------------------------- 1 | import { DenoEnvActions } from "./deno/deno-env-actions.js"; 2 | import { NodeEnvActions } from "./node/node-env-actions.js"; 3 | import { BrowserEnvActions } from "./web/web-env-actions.js"; 4 | import { CFEnvActions } from "./cf/cf-env-actions.js"; 5 | import { KeyedResolvOnce } from "./resolve-once.js"; 6 | import { Result } from "./result.js"; 7 | import { getParamsResult, KeysParam } from "./utils/get-params-result.js"; 8 | 9 | export type EnvTuple = ([string, string] | [string, string][] | Record | Iterator<[string, string]>)[]; 10 | 11 | export interface EnvMap { 12 | get(key: string): string | undefined; 13 | set(key: string, value?: string): void; 14 | delete(key: string): void; 15 | keys(): string[]; 16 | } 17 | export interface EnvActions extends EnvMap { 18 | active(): boolean; 19 | register(env: Env): Env; 20 | } 21 | 22 | export interface EnvFactoryOpts { 23 | readonly symbol: string; // default "CP_ENV" used by BrowserEnvActions 24 | readonly presetEnv: Map; 25 | } 26 | 27 | type OnSetFn = (key: string, value?: string) => void; 28 | export interface OnSetItem { 29 | readonly filter: Set; 30 | readonly fn: OnSetFn; 31 | } 32 | 33 | export interface Env extends EnvMap { 34 | onSet(fn: OnSetFn, ...filter: string[]): void; 35 | 36 | gets(...kparams: KeysParam): Result>; 37 | sets(...keys: EnvTuple): void; 38 | } 39 | 40 | export type EnvFactoryFn = (opts: Partial) => EnvActions; 41 | 42 | const envActions: { id: string; fn: EnvFactoryFn }[] = [ 43 | { id: "cf", fn: (opts: Partial): EnvActions => CFEnvActions.new(opts) }, 44 | { id: "node", fn: (opts: Partial): EnvActions => NodeEnvActions.new(opts) }, 45 | { id: "deno", fn: (opts: Partial): EnvActions => DenoEnvActions.new(opts) }, 46 | { id: "browser", fn: (opts: Partial): EnvActions => BrowserEnvActions.new(opts) }, 47 | ]; 48 | 49 | export function registerEnvAction(fn: EnvFactoryFn): () => void { 50 | const id = `id-${Math.random()}`; 51 | envActions.unshift({ id, fn }); 52 | // rerun envFactory 53 | _envFactories.unget(id); 54 | return () => { 55 | const index = envActions.findIndex((i) => i.id === id); 56 | if (index >= 0) { 57 | envActions.splice(index, 1); 58 | } 59 | }; 60 | } 61 | 62 | const _envFactories = new KeyedResolvOnce(); 63 | export function envFactory(opts: Partial = {}): Env { 64 | const found = envActions.find((fi) => fi.fn(opts).active()); 65 | if (!found) { 66 | throw new Error("SysContainer:envFactory: no env available"); 67 | } 68 | return _envFactories.get(found.id).once(() => { 69 | const action = found.fn(opts); 70 | const ret = new EnvImpl(action, opts); 71 | action.register(ret); 72 | return ret; 73 | }); 74 | } 75 | 76 | function isIterable(obj: unknown): obj is Iterable<[string, string]> { 77 | // checks for null and undefined 78 | if (obj == null) { 79 | return false; 80 | } 81 | return typeof (obj as Record)[Symbol.iterator] === "function"; 82 | } 83 | 84 | export class EnvImpl implements Env { 85 | readonly _map: EnvMap; 86 | constructor(map: EnvMap, opts: Partial = {}) { 87 | this._map = map; 88 | this._updatePresets(opts.presetEnv); 89 | } 90 | gets(...kparams: KeysParam): Result> { 91 | return getParamsResult(kparams, { 92 | getParam: (k) => this.get(k), 93 | }); 94 | } 95 | sets(...keys: EnvTuple): void { 96 | keys.forEach((key) => { 97 | if (Array.isArray(key)) { 98 | if (key.length === 2) { 99 | const [k, v] = key; 100 | if (typeof k === "string" && typeof v === "string") { 101 | this.set(k, v); 102 | return; 103 | } 104 | } 105 | for (const item of key) { 106 | if (Array.isArray(item)) { 107 | // [string, string] 108 | if (item.length === 2) { 109 | const [k, v] = item; 110 | if (typeof k === "string" && typeof v === "string") { 111 | this.set(k, v); 112 | } 113 | } 114 | } 115 | } 116 | } else { 117 | if (isIterable(key)) { 118 | for (const [k, v] of key) { 119 | if (typeof k === "string" && typeof v === "string") { 120 | this.set(k, v); 121 | } 122 | } 123 | } else { 124 | const rKey = key as Record; 125 | for (const k in rKey) { 126 | const v = rKey[k]; 127 | if (typeof k === "string" && typeof v === "string") { 128 | this.set(k, v); 129 | } 130 | } 131 | } 132 | } 133 | }); 134 | } 135 | _updatePresets(presetEnv?: Map): void { 136 | if (!presetEnv) { 137 | return; 138 | } 139 | for (const [key, value] of presetEnv) { 140 | this._map.set(key, value); 141 | } 142 | } 143 | _applyOnSet(onSet: OnSetItem[], key?: string, value?: string): void { 144 | onSet.forEach((item) => { 145 | let keys: string[] = []; 146 | if (key) { 147 | keys = [key]; 148 | } else { 149 | keys = this._map.keys(); 150 | } 151 | keys 152 | .filter((k) => { 153 | if (item.filter.size === 0) { 154 | return true; 155 | } 156 | if (item.filter.has(k)) { 157 | return true; 158 | } 159 | return false; 160 | }) 161 | .forEach((k) => { 162 | let v; 163 | if (!key && !value) { 164 | // init 165 | v = this._map.get(k); 166 | } else if (key && !value) { 167 | // del 168 | v = undefined; 169 | } else { 170 | // set 171 | v = value; 172 | } 173 | item.fn(k, v); 174 | }); 175 | }); 176 | } 177 | readonly _onSet: OnSetItem[] = []; 178 | keys(): string[] { 179 | return this._map.keys(); 180 | } 181 | // filter is not set all sets passed 182 | onSet(fn: OnSetFn, ...filter: string[]): void { 183 | const item: OnSetItem = { filter: new Set(filter), fn }; 184 | this._onSet.push(item); 185 | this._applyOnSet([item]); 186 | } 187 | get(key: string): string | undefined { 188 | return this._map.get(key); 189 | } 190 | set(key: string, value?: string): void { 191 | if (!value) { 192 | return; 193 | } 194 | this._map.set(key, value); 195 | this._applyOnSet(this._onSet, key, value); 196 | } 197 | delete(key: string): void { 198 | this._map.delete(key); 199 | this._applyOnSet(this._onSet, key); 200 | } 201 | } 202 | 203 | // export const envImpl = new EnvImpl(); 204 | -------------------------------------------------------------------------------- /ts/src/utils/relative-path.test.ts: -------------------------------------------------------------------------------- 1 | import { PartType, pathJoin, relativePath, splitPath } from "./relative-path.js"; 2 | 3 | describe("relativePath", () => { 4 | describe("splitPath", () => { 5 | it("empty - split path into parts", () => { 6 | expect(splitPath("").parts).toEqual([]); 7 | }); 8 | it("/ - split path into parts", () => { 9 | expect(splitPath("/").parts).toEqual([PartType.Root]); 10 | }); 11 | it("///./// - split path into parts", () => { 12 | expect(splitPath("///.///").parts).toEqual([PartType.Root]); 13 | }); 14 | 15 | it("/a/b/c/./d/../u - split path into parts", () => { 16 | expect(splitPath("/abb/b/cdd/./ddkkd/../ukdd///..//.///..///bl/.///.//.//mmm").parts).toEqual([ 17 | PartType.Root, 18 | "abb", 19 | PartType.Slash, 20 | "b", 21 | PartType.Slash, 22 | "cdd", 23 | PartType.Slash, 24 | "ddkkd", 25 | PartType.Slash, 26 | PartType.Up, 27 | PartType.Slash, 28 | "ukdd", 29 | PartType.Slash, 30 | PartType.Up, 31 | PartType.Slash, 32 | PartType.Up, 33 | PartType.Slash, 34 | "bl", 35 | PartType.Slash, 36 | "mmm", 37 | ]); 38 | }); 39 | it("/a/b/c/./d/../u/ - split path into parts", () => { 40 | expect(splitPath("/abb/b/cdd/./ddkkd/../ukdd///..//.///..///bl/.///.//.//mmm/").parts).toEqual([ 41 | PartType.Root, 42 | "abb", 43 | PartType.Slash, 44 | "b", 45 | PartType.Slash, 46 | "cdd", 47 | PartType.Slash, 48 | "ddkkd", 49 | PartType.Slash, 50 | PartType.Up, 51 | PartType.Slash, 52 | "ukdd", 53 | PartType.Slash, 54 | PartType.Up, 55 | PartType.Slash, 56 | PartType.Up, 57 | PartType.Slash, 58 | "bl", 59 | PartType.Slash, 60 | "mmm", 61 | PartType.Slash, 62 | ]); 63 | }); 64 | 65 | it("/a/b/c/./d/../u/ - split path into parts", () => { 66 | expect(splitPath("/abb/b/cdd/./ddkkd/../ukdd///..//.///..///bl/.///.//.//mmm//.////").parts).toEqual([ 67 | PartType.Root, 68 | "abb", 69 | PartType.Slash, 70 | "b", 71 | PartType.Slash, 72 | "cdd", 73 | PartType.Slash, 74 | "ddkkd", 75 | PartType.Slash, 76 | PartType.Up, 77 | PartType.Slash, 78 | "ukdd", 79 | PartType.Slash, 80 | PartType.Up, 81 | PartType.Slash, 82 | PartType.Up, 83 | PartType.Slash, 84 | "bl", 85 | PartType.Slash, 86 | "mmm", 87 | PartType.Slash, 88 | ]); 89 | }); 90 | 91 | it("aaa - split path into parts", () => { 92 | expect(splitPath("aaa").parts).toEqual(["aaa"]); 93 | }); 94 | it("aa/..", () => { 95 | expect(splitPath("aa/..").parts).toEqual(["aa", PartType.Slash, PartType.Up]); 96 | }); 97 | 98 | it("../aa/..", () => { 99 | expect(splitPath("aa/..").parts).toEqual(["aa", PartType.Slash, PartType.Up]); 100 | }); 101 | 102 | it("./aaa - split path into parts", () => { 103 | expect(splitPath("./aaa").parts).toEqual(["aaa"]); 104 | }); 105 | it("aaa/ - split path into parts", () => { 106 | expect(splitPath("aaa/").parts).toEqual(["aaa", PartType.Slash]); 107 | }); 108 | it("relative ./aaa - split path into parts", () => { 109 | expect(splitPath(".////././aaa/").parts).toEqual(["aaa", PartType.Slash]); 110 | }); 111 | 112 | it("../aaa - split path into parts", () => { 113 | expect(splitPath(".////././../././aaa/").parts).toEqual([PartType.Up, PartType.Slash, "aaa", PartType.Slash]); 114 | }); 115 | 116 | it("../../aaa - split path into parts", () => { 117 | expect(splitPath(".././..////aaa/").parts).toEqual([ 118 | PartType.Up, 119 | PartType.Slash, 120 | PartType.Up, 121 | PartType.Slash, 122 | "aaa", 123 | PartType.Slash, 124 | ]); 125 | }); 126 | }); 127 | describe("relativePath", () => { 128 | it("append relative to abs", () => { 129 | expect(relativePath("b/c/d", "/y/z/w")).toEqual("/y/z/w"); 130 | }); 131 | 132 | it("append relative to relative", () => { 133 | expect(relativePath("b/c/d", "y/z/w")).toEqual("b/c/d/y/z/w"); 134 | }); 135 | 136 | it("append up to relative", () => { 137 | expect(relativePath("../b/c/d", "y/z/w")).toEqual("../b/c/d/y/z/w"); 138 | }); 139 | 140 | it("append up to relative", () => { 141 | expect(relativePath("./b/c/d", "y/z/w")).toEqual("b/c/d/y/z/w"); 142 | }); 143 | 144 | it("override root", () => { 145 | expect(relativePath("/b/c/d", "/y/z/w")).toEqual("/y/z/w"); 146 | }); 147 | 148 | it("simple attach ", () => { 149 | expect(relativePath("/b/c/./d/./", "y/./z/w")).toEqual("/b/c/d/y/z/w"); 150 | }); 151 | 152 | it("simple with slash attach ", () => { 153 | expect(relativePath("/b/c/./d/././////", "./////y/./z/w")).toEqual("/b/c/d/y/z/w"); 154 | }); 155 | 156 | it("simple with .. attach ", () => { 157 | expect(relativePath("/b/c/./../d/././////", "../////y/./../z/w")).toEqual("/b/z/w"); 158 | }); 159 | 160 | it("simple simple .. attach ", () => { 161 | expect(relativePath("/b/top/", "../oo")).toEqual("/b/oo"); 162 | }); 163 | 164 | it("simple simple .. attach ", () => { 165 | expect(relativePath("/b/bb/../../../../hh", "")).toEqual("../../hh"); 166 | }); 167 | 168 | it("top over ../ attach ", () => { 169 | expect(relativePath("/b/bb/../../../hh/", "")).toEqual("../hh/"); 170 | }); 171 | 172 | it("simple simple .. attach ", () => { 173 | expect(relativePath("/b/top/", "../../oo")).toEqual("/oo"); 174 | }); 175 | }); 176 | 177 | describe("pathJoin", () => { 178 | it("empty pathjoin", () => { 179 | expect(pathJoin("", "")).toEqual(""); 180 | }); 181 | 182 | it("both relative pathjoin", () => { 183 | expect(pathJoin("a/b", "c/d")).toEqual("a/b/c/d"); 184 | }); 185 | 186 | it("abs empty pathjoin", () => { 187 | expect(pathJoin("/a/b", "")).toEqual("/a/b"); 188 | }); 189 | 190 | it("empty abs pathjoin", () => { 191 | expect(pathJoin("", "/a/b")).toEqual("/a/b"); 192 | }); 193 | 194 | it("empty rel pathjoin", () => { 195 | expect(pathJoin("", "a/b")).toEqual("a/b"); 196 | }); 197 | 198 | it("abs abs pathjoin", () => { 199 | expect(pathJoin("/a/b", "//mm/dd")).toEqual("/a/b//mm/dd"); 200 | }); 201 | 202 | it("abs rel pathjoin", () => { 203 | expect(pathJoin("/a/b//", "mm/dd")).toEqual("/a/b//mm/dd"); 204 | }); 205 | 206 | it("abs// abs// pathjoin", () => { 207 | expect(pathJoin("/a/b//", "//mm/dd")).toEqual("/a/b////mm/dd"); 208 | }); 209 | 210 | it('kaputt "/b/c/./d", "y/./z/w"', () => { 211 | expect(pathJoin("/b/c/./d", "y/./z/w")).toEqual("/b/c/./d/y/./z/w"); 212 | }); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /ts/src/base-sys-abstraction.ts: -------------------------------------------------------------------------------- 1 | import { FileService } from "./file-service.js"; 2 | import { TimeMode, RandomMode, IDMode, SystemService, VoidFunc, SysAbstraction } from "./sys-abstraction.js"; 3 | import { Time } from "./time.js"; 4 | import { TxtEnDecoder } from "./txt-en-decoder.js"; 5 | 6 | export class SysTime extends Time { 7 | Now(): Date { 8 | return new Date(); 9 | } 10 | Sleep(duration: number): Promise { 11 | return new Promise((resolve) => { 12 | setTimeout(() => { 13 | resolve(); 14 | }, duration); 15 | }); 16 | } 17 | } 18 | 19 | export class ConstTime extends Time { 20 | Now(): Date { 21 | return new Date(2021, 1, 1, 0, 0, 0, 0); 22 | } 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | Sleep(duration: number): Promise { 25 | return Promise.resolve(); 26 | } 27 | } 28 | 29 | export class StepTime extends Time { 30 | _step: Date; 31 | readonly _start: Date; 32 | constructor() { 33 | super(); 34 | this._step = new ConstTime().Now(); 35 | this._start = this._step; 36 | } 37 | Now(steps = 1): Date { 38 | // if (this._step.getTime() === 0) { 39 | // this._step = new ConstTime().Now(); 40 | // return this._step; 41 | // } 42 | for (let i = 0; steps > 0 && i < steps; i++) { 43 | this._step = new Date(this._step.getTime() + 1000); 44 | } 45 | if (steps < 1) { 46 | this._step = new Date(this._start.getTime() + steps * -1000); 47 | } 48 | // this._step = new Date(this._step.getTime() + 1000); 49 | return this._step; 50 | } 51 | Sleep(duration: number): Promise { 52 | this._step = new Date(this._step.getTime() + duration); 53 | return Promise.resolve(); 54 | } 55 | } 56 | 57 | export function TimeFactory(timeMode: TimeMode): Time { 58 | switch (timeMode) { 59 | case TimeMode.REAL: 60 | return new SysTime(); 61 | case TimeMode.CONST: 62 | return new ConstTime(); 63 | case TimeMode.STEP: 64 | return new StepTime(); 65 | } 66 | return new SysTime(); 67 | } 68 | 69 | export class RandomService { 70 | readonly _mode: RandomMode; 71 | _step = 0; 72 | constructor(mode: RandomMode) { 73 | this._mode = mode; 74 | } 75 | Random0ToValue(value: number): number { 76 | switch (this._mode) { 77 | case RandomMode.CONST: 78 | return 0.5 * value; 79 | case RandomMode.STEP: 80 | this._step += 0.0001; 81 | return this._step * value; 82 | case RandomMode.RANDOM: 83 | return Math.random() * value; 84 | default: 85 | throw new Error("Unknown RandomMode"); 86 | } 87 | } 88 | } 89 | 90 | export class IdService { 91 | readonly _mode: IDMode; 92 | _step = 0; 93 | constructor(mode?: IDMode) { 94 | if (!mode) { 95 | mode = IDMode.UUID; 96 | } 97 | this._mode = mode; 98 | } 99 | NextId(): string { 100 | switch (this._mode) { 101 | case IDMode.UUID: 102 | return crypto.randomUUID(); 103 | case IDMode.CONST: 104 | return "VeryUniqueID"; 105 | case IDMode.STEP: 106 | return `STEPId-${this._step++}`; 107 | default: 108 | throw new Error("Unknown IDMode"); 109 | } 110 | } 111 | } 112 | 113 | export interface BaseSysAbstractionParams { 114 | readonly TxtEnDecoder: TxtEnDecoder; 115 | readonly FileSystem: FileService; 116 | readonly SystemService: SystemService; 117 | } 118 | 119 | export interface ExitHandler { 120 | readonly hdl: VoidFunc; 121 | readonly id: string; 122 | } 123 | 124 | export interface ExitService { 125 | injectExitHandlers(hdls: ExitHandler[]): void; 126 | exit(code: number): void; 127 | } 128 | 129 | // some black magic to make it work with CF workers 130 | function consumeReadableStream( 131 | reader: ReadableStreamDefaultReader, 132 | writeFn: (chunk: Uint8Array) => Promise, 133 | ): void { 134 | reader 135 | .read() 136 | .then(({ done, value }) => { 137 | if (done) { 138 | return; 139 | } 140 | writeFn(value) 141 | .then(() => { 142 | consumeReadableStream(reader, writeFn); 143 | }) 144 | .catch((e) => { 145 | // eslint-disable-next-line no-console 146 | console.error("consumeReadableStream:writeFn", e); 147 | }); 148 | }) 149 | .catch((e) => { 150 | // eslint-disable-next-line no-console 151 | console.error("consumeReadableStream:read", e); 152 | }); 153 | } 154 | 155 | function CFWriteableStream(writeFn: (chunk: Uint8Array) => Promise): WritableStream { 156 | const ts = new TransformStream(); 157 | consumeReadableStream(ts.readable.getReader(), writeFn); 158 | return ts.writable; 159 | } 160 | 161 | export class BaseSysAbstraction { 162 | readonly _time: SysTime = new SysTime(); 163 | readonly _stdout: WritableStream; 164 | readonly _stderr: WritableStream; 165 | 166 | readonly _idService: IdService = new IdService(); 167 | readonly _randomService: RandomService = new RandomService(RandomMode.RANDOM); 168 | readonly _fileSystem: FileService; 169 | readonly _systemService: SystemService; 170 | readonly _txtEnDe: TxtEnDecoder; 171 | 172 | constructor(params: BaseSysAbstractionParams) { 173 | this._fileSystem = params.FileSystem; 174 | this._systemService = params.SystemService; 175 | this._txtEnDe = params.TxtEnDecoder; 176 | const decoder = this._txtEnDe; 177 | this._stdout = CFWriteableStream((chunk) => { 178 | const decoded = decoder.decode(chunk); 179 | // eslint-disable-next-line no-console 180 | console.log(decoded.trimEnd()); 181 | return Promise.resolve(); 182 | }); 183 | this._stderr = CFWriteableStream((chunk) => { 184 | const decoded = decoder.decode(chunk); 185 | // eslint-disable-next-line no-console 186 | console.error(decoded.trimEnd()); 187 | return Promise.resolve(); 188 | }); 189 | /* this is not CF worker compatible 190 | this._stdout = new WritableStream({ 191 | write(chunk): Promise { 192 | return new Promise((resolve) => { 193 | const decoded = decoder.decode(chunk); 194 | // eslint-disable-next-line no-console 195 | console.log(decoded.trimEnd()); 196 | resolve(); 197 | }); 198 | }, 199 | }); 200 | this._stderr = new WritableStream({ 201 | write(chunk): Promise { 202 | return new Promise((resolve) => { 203 | const decoded = decoder.decode(chunk); 204 | // eslint-disable-next-line no-console 205 | console.error(decoded.trimEnd()); 206 | resolve(); 207 | }); 208 | }, 209 | }); 210 | */ 211 | } 212 | } 213 | 214 | export interface WrapperSysAbstractionParams { 215 | readonly TimeMode?: TimeMode; 216 | readonly IdMode?: IDMode; 217 | readonly Stdout?: WritableStream; 218 | readonly Stderr?: WritableStream; 219 | readonly RandomMode?: RandomMode; 220 | readonly FileSystem?: FileService; 221 | readonly SystemService?: SystemService; 222 | readonly TxtEnDecoder?: TxtEnDecoder; 223 | } 224 | 225 | export class WrapperSysAbstraction implements SysAbstraction { 226 | readonly _time: Time; 227 | readonly _stdout: WritableStream; 228 | readonly _stderr: WritableStream; 229 | readonly _idService: IdService; 230 | readonly _randomService: RandomService; 231 | readonly _fileSystem: FileService; 232 | readonly _systemService: SystemService; 233 | constructor(base: BaseSysAbstraction, params?: WrapperSysAbstractionParams) { 234 | this._time = base._time; 235 | this._stdout = base._stdout; 236 | this._stderr = base._stderr; 237 | this._idService = base._idService; 238 | this._randomService = base._randomService; 239 | this._fileSystem = base._fileSystem; 240 | this._systemService = base._systemService; 241 | if (params) { 242 | if (params.TimeMode) { 243 | this._time = TimeFactory(params.TimeMode); 244 | } 245 | if (params.Stdout) { 246 | this._stdout = params.Stdout; 247 | } 248 | if (params.Stderr) { 249 | this._stderr = params.Stderr; 250 | } 251 | if (params.IdMode) { 252 | this._idService = new IdService(params.IdMode); 253 | } 254 | if (params.RandomMode) { 255 | this._randomService = new RandomService(params.RandomMode); 256 | } 257 | if (params.FileSystem) { 258 | this._fileSystem = params.FileSystem; 259 | } 260 | if (params.SystemService) { 261 | this._systemService = params.SystemService; 262 | } 263 | } 264 | } 265 | Time(): Time { 266 | return this._time; 267 | } 268 | NextId(): string { 269 | return this._idService.NextId(); 270 | } 271 | Random0ToValue(value: number): number { 272 | return this._randomService.Random0ToValue(value); 273 | } 274 | Stdout(): WritableStream { 275 | return this._stdout; 276 | } 277 | Stderr(): WritableStream { 278 | return this._stderr; 279 | } 280 | 281 | System(): SystemService { 282 | return this._systemService; 283 | } 284 | FileSystem(): FileService { 285 | return this._fileSystem; 286 | } 287 | } 288 | // export const BaseSysAbstraction = new BaseSysAbstractionImpl() 289 | -------------------------------------------------------------------------------- /ts/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { bin2string } from "./bin2text.js"; 2 | import { Option } from "./option.js"; 3 | import { Result } from "./result.js"; 4 | import { TxtEnDecoder } from "./txt-en-decoder.js"; 5 | import { CoerceURI } from "./uri.js"; 6 | 7 | export enum Level { 8 | WARN = "warn", 9 | DEBUG = "debug", 10 | INFO = "info", 11 | ERROR = "error", 12 | } 13 | 14 | export type Serialized = string | number | boolean; 15 | export type FnSerialized = () => Serialized | Serialized[]; 16 | 17 | export class LogValue { 18 | constructor(readonly fn: FnSerialized) {} 19 | value(): Serialized | Serialized[] { 20 | try { 21 | // console.log("LogValue.value", this.fn.toString()); 22 | return this.fn(); 23 | } catch (e) { 24 | return `LogValue:${(e as Error).message}`; 25 | } 26 | } 27 | toJSON(): Serialized | Serialized[] { 28 | return this.value(); 29 | } 30 | } 31 | 32 | export type LogSerializable = Record>; 33 | 34 | // export function sanitizeSerialize(lineEnd?: string): (key: unknown, val: unknown) => unknown { 35 | // const cache = new Set(); 36 | // return function (this: unknown, key: unknown, value: unknown) { 37 | // if (typeof value === "object" && value !== null) { 38 | // // Duplicate reference found, discard key 39 | // if (cache.has(value)) return "..."; 40 | // cache.add(value); 41 | // } 42 | // return lineEnd ? value + lineEnd : value; 43 | // }; 44 | // } 45 | 46 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 47 | export function asyncLogValue(val: () => Promise): Promise { 48 | // return Promise.resolve(logValue(val)); 49 | throw new Error("Not implemented"); 50 | } 51 | 52 | export type LogValueArg = LogValue | Serialized | Serialized[] | FnSerialized | undefined | null; 53 | 54 | export interface LogValueState { 55 | readonly state?: Set; 56 | readonly ignoreAttr: Option; 57 | } 58 | 59 | export function logValue(val: LogValueArg, ctx: LogValueState): LogValue { 60 | return logValueInternal(val, { 61 | ...ctx, 62 | state: ctx.state || new Set([Math.random()]), 63 | }); 64 | } 65 | 66 | type LogValueStateInternal = LogValueState & { readonly state: Set }; 67 | 68 | function logValueInternal(val: LogValueArg, ctx: LogValueStateInternal): LogValue { 69 | ctx = { 70 | ...ctx, 71 | state: ctx.state || new Set([Math.random()]), 72 | } satisfies LogValueStateInternal; 73 | switch (typeof val) { 74 | case "function": 75 | return new LogValue(val); 76 | case "string": { 77 | try { 78 | const ret = JSON.parse(val) as LogValueArg; 79 | if (typeof ret === "object" && ret !== null) { 80 | return logValueInternal(ret, ctx); 81 | } 82 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 83 | } catch (e) { 84 | try { 85 | const url = new URL(val); 86 | return new LogValue(() => url.toString()); 87 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 88 | } catch (e) { 89 | // ignore 90 | } 91 | } 92 | if (val.match(/[\n\r]/)) { 93 | const lines = val.split(/[\n\r]+/).map((v) => v.trim()); 94 | return new LogValue(() => lines); 95 | } 96 | return new LogValue(() => val.toString()); 97 | } 98 | case "number": 99 | return new LogValue(() => val); 100 | case "boolean": 101 | return new LogValue(() => val); 102 | case "object": { 103 | if (val === null) { 104 | return new LogValue(() => "null"); 105 | } 106 | if (ArrayBuffer.isView(val)) { 107 | try { 108 | // should be injected 109 | const decoder = new TextDecoder(); 110 | const asStr = decoder.decode(val); 111 | const obj = JSON.parse(asStr) as LogValueArg; 112 | return logValueInternal(obj, ctx); 113 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 114 | } catch (e) { 115 | return logValueInternal(bin2string(val, 512), ctx); 116 | } 117 | } 118 | if (Array.isArray(val)) { 119 | return new LogValue(() => 120 | (val as Serialized[]).map((v) => logValue(v, { ...ctx, state: undefined }).value() as Serialized), 121 | ); 122 | } 123 | // if (val instanceof Response) { 124 | // // my = my.clone() as unknown as LogValue | Serialized[] | null 125 | // // const rval = my as unknown as Partial; 126 | // // delete rval.clone 127 | // // delete rval.blob 128 | // } 129 | if (val instanceof Headers) { 130 | return new LogValue(() => Object.fromEntries(val.entries()) as unknown as Serialized); 131 | } 132 | if (val instanceof ReadableStream) { 133 | return new LogValue(() => ">Stream<"); 134 | } 135 | if (val instanceof Promise) { 136 | return new LogValue(() => ">Promise<"); 137 | } 138 | 139 | // Duplicate reference found, discard key 140 | if (ctx.state?.has(val)) { 141 | return new LogValue(() => "..."); 142 | } 143 | ctx.state?.add(val); 144 | if (typeof val.toJSON === "function") { 145 | return new LogValue(() => val.toJSON()); 146 | } 147 | 148 | const res: Record = {}; 149 | const typedVal = val as unknown as Record; 150 | for (const key in typedVal) { 151 | if (ctx.ignoreAttr.IsSome() && ctx.ignoreAttr.unwrap().test(key)) { 152 | continue; 153 | } 154 | const element = typedVal[key]; 155 | if (element instanceof LogValue) { 156 | res[key] = element; 157 | } else { 158 | if (typeof element !== "function") { 159 | res[key] = logValueInternal(element, ctx); 160 | } 161 | } 162 | } 163 | // ugly as hell cast but how declare a self-referencing type? 164 | return new LogValue(() => res as unknown as Serialized); 165 | } 166 | default: 167 | if (!val) { 168 | return new LogValue(() => "--Falsy--"); 169 | } 170 | throw new Error(`Invalid type:${typeof val}`); 171 | } 172 | } 173 | 174 | export interface Sized { 175 | size: number; 176 | } 177 | export interface Lengthed { 178 | length: number; 179 | } 180 | export type SizeOrLength = Sized | Lengthed; 181 | 182 | export interface LogFormatter { 183 | format(attr: LogSerializable): Uint8Array; 184 | } 185 | 186 | export interface LevelHandler { 187 | enableLevel(level: Level, ...modules: string[]): void; 188 | disableLevel(level: Level, ...modules: string[]): void; 189 | setExposeStack(enable?: boolean): void; 190 | setIgnoreAttr(re?: RegExp): void; 191 | ignoreAttr: Option; 192 | isStackExposed: boolean; 193 | setDebug(...modules: (string | string[])[]): void; 194 | isEnabled(ilevel: unknown, module: unknown): boolean; 195 | } 196 | 197 | export type HttpType = Response | Result | Request | Result; 198 | 199 | export interface LoggerInterface { 200 | readonly levelHandler: LevelHandler; 201 | TxtEnDe(): TxtEnDecoder; 202 | Module(key: string): R; 203 | // if modules is empty, set for all Levels 204 | EnableLevel(level: Level, ...modules: string[]): R; 205 | DisableLevel(level: Level, ...modules: string[]): R; 206 | 207 | Attributes(): Record; 208 | 209 | SetDebug(...modules: (string | string[])[]): R; 210 | // default is /^_/ 211 | SetIgnoreAttribute(re?: RegExp): R; 212 | SetExposeStack(enable?: boolean): R; 213 | SetFormatter(fmt: LogFormatter): R; 214 | 215 | Ref(key: string, action: { toString: () => string } | FnSerialized): R; 216 | Result(key: string, res: Result): R; 217 | // default key url 218 | Url(url: CoerceURI, key?: string): R; 219 | // len 220 | Len(value: unknown, key?: string): R; 221 | 222 | Hash(value: unknown, key?: string): R; 223 | 224 | Str>(key: T, value?: T extends string ? string : undefined): R; 225 | Uint64>(key: T, value?: T extends string ? number : undefined): R; 226 | Int>(key: T, value?: T extends string ? number : undefined): R; 227 | Bool>(key: T, value?: T extends string ? unknown : undefined): R; 228 | Any>(key: T, value?: T extends string ? unknown : undefined): R; 229 | 230 | // first string is the key 231 | // first response is Response 232 | // first request is Request 233 | Http(...mix: (HttpType | string)[]): R; 234 | Pair(x: Record): R; 235 | 236 | Error(): R; 237 | Warn(): R; 238 | Debug(): R; 239 | Log(): R; 240 | WithLevel(level: Level): R; 241 | 242 | Err(err: T | Result | Error): R; // could be Error, or something which coerces to string 243 | Info(): R; 244 | Timestamp(): R; 245 | Dur(key: string, nsec: number): R; 246 | } 247 | 248 | export function IsLogger(obj: unknown): obj is Logger { 249 | return ( 250 | typeof obj === "object" && 251 | [ 252 | "Module", 253 | "EnableLevel", 254 | "DisableLevel", 255 | "SetDebug", 256 | "Str", 257 | "Error", 258 | "Warn", 259 | "Debug", 260 | "Log", 261 | "WithLevel", 262 | "Err", 263 | "Info", 264 | "Timestamp", 265 | "Any", 266 | "Dur", 267 | "Uint64", 268 | ] 269 | .map((fn) => typeof (obj as Record)[fn] === "function") 270 | .reduce((a, b) => a && b, true) 271 | ); 272 | } 273 | 274 | export interface WithLogger extends LoggerInterface { 275 | Logger(): Logger; 276 | } 277 | 278 | export interface AsError { 279 | AsError(): Error; 280 | ResultError(): Result; 281 | } 282 | 283 | export interface Logger extends LoggerInterface { 284 | With(): WithLogger; 285 | 286 | Msg(...args: string[]): AsError; 287 | Flush(): Promise; 288 | } 289 | -------------------------------------------------------------------------------- /ts/src/tracer.test.ts: -------------------------------------------------------------------------------- 1 | import { Time } from "./time.js"; 2 | import { WebSysAbstraction } from "./web/index.js"; 3 | import { TimeMode } from "./sys-abstraction.js"; 4 | import { TraceNode } from "./tracer.js"; 5 | import { MockLogger } from "./test/mock-logger.js"; 6 | 7 | describe("trace", () => { 8 | let time: Time; 9 | let refTime: Time; 10 | let trace: TraceNode; 11 | const logger = MockLogger().logger.With().Module("trace").Str("value", "important").Logger(); 12 | beforeEach(() => { 13 | time = WebSysAbstraction({ TimeMode: TimeMode.STEP }).Time(); 14 | trace = TraceNode.root(time, logger); 15 | refTime = WebSysAbstraction({ TimeMode: TimeMode.STEP }).Time(); 16 | }); 17 | it("a simple trace", () => { 18 | expect( 19 | trace.span("test", (trace) => { 20 | const r1 = trace.span("test.1", () => { 21 | return 1; 22 | }); 23 | const r2 = trace.span("test.2", () => { 24 | return 1; 25 | }); 26 | return r1 + r2; 27 | }), 28 | ).toBe(2); 29 | const childs = Array.from(trace.childs.values()); 30 | expect(childs.map((v) => v.invokes())).toEqual([ 31 | { 32 | ctx: { 33 | module: "trace", 34 | spanId: "test", 35 | value: "important", 36 | }, 37 | invokations: [ 38 | { 39 | start: refTime.Now().getTime(), 40 | result: "success", 41 | end: refTime.Now(5).getTime(), 42 | }, 43 | ], 44 | }, 45 | ]); 46 | const layered = Array.from(trace.childs.get("test")?.childs.values() || []); 47 | refTime = WebSysAbstraction({ TimeMode: TimeMode.STEP }).Time(); 48 | expect(layered.map((v) => v.invokes())).toEqual([ 49 | { 50 | ctx: { 51 | module: "trace", 52 | spanId: "test.1", 53 | value: "important", 54 | }, 55 | invokations: [ 56 | { 57 | start: refTime.Now(2).getTime(), 58 | result: "success", 59 | end: refTime.Now().getTime(), 60 | }, 61 | ], 62 | }, 63 | { 64 | ctx: { 65 | module: "trace", 66 | spanId: "test.2", 67 | value: "important", 68 | }, 69 | invokations: [ 70 | { 71 | start: refTime.Now().getTime(), 72 | result: "success", 73 | end: refTime.Now(1).getTime(), 74 | }, 75 | ], 76 | }, 77 | ]); 78 | }); 79 | 80 | it("a async simple trace", async () => { 81 | const log = trace.ctx.logger?.With().Str("value", "test").Logger(); 82 | const ret = await trace.span(trace.ctxWith("test", log), async (trace) => { 83 | const r1 = trace.span(trace.ctxWith("test.1"), () => 1); 84 | const log2 = trace.ctx.logger?.With().Module("xxx").Str("r2", "test.2").Logger(); 85 | const r2 = await trace.span(trace.ctxWith("test.2", log2), async () => { 86 | time.Now(); 87 | await new Promise((resolve) => 88 | setTimeout(() => { 89 | time.Now(); 90 | time.Now(); 91 | resolve(); 92 | }, 100), 93 | ); 94 | return 1; 95 | }); 96 | return r1 + r2; 97 | }); 98 | expect(ret).toBe(2); 99 | const childs = Array.from(trace.childs.values()); 100 | const exp = childs.map((v) => v.invokes()); 101 | expect(exp).toEqual([ 102 | { 103 | ctx: { 104 | module: "trace", 105 | spanId: "test", 106 | value: "test", 107 | }, 108 | invokations: [ 109 | { 110 | start: refTime.Now().getTime(), 111 | result: "success", 112 | end: refTime.Now(8).getTime(), 113 | }, 114 | ], 115 | }, 116 | ]); 117 | const layered = Array.from(trace.childs.get("test")?.childs.values() || []); 118 | expect(layered.map((v) => v.invokes())).toEqual([ 119 | { 120 | ctx: { 121 | module: "trace", 122 | spanId: "test.1", 123 | value: "test", 124 | }, 125 | invokations: [ 126 | { 127 | result: "success", 128 | start: refTime.Now(-2).getTime(), 129 | end: refTime.Now().getTime(), 130 | }, 131 | ], 132 | }, 133 | { 134 | ctx: { 135 | module: "xxx", 136 | r2: "test.2", 137 | spanId: "test.2", 138 | value: "test", 139 | }, 140 | invokations: [ 141 | { 142 | start: refTime.Now().getTime(), 143 | end: refTime.Now(4).getTime(), 144 | result: "success", 145 | }, 146 | ], 147 | }, 148 | ]); 149 | }); 150 | 151 | it("a async exception trace", async () => { 152 | const ret = await trace.span("test", async (trace) => { 153 | let r1 = 0; 154 | let r2 = 0; 155 | for (let i = 0; i < 3; i++) { 156 | try { 157 | r1 += trace.span("test.1", (trace) => { 158 | if (i % 2) { 159 | throw new Error("test.1"); 160 | } 161 | trace.metrics.get("i.1").add([i]); 162 | return 1; 163 | }); 164 | } catch (e) { 165 | if (i % 2) { 166 | expect((e as Error).message).toEqual("test.1"); 167 | } else { 168 | assert(false, "should not happen"); 169 | } 170 | } 171 | try { 172 | r2 += await trace.span("test.2", async (trace) => { 173 | time.Now(); 174 | await new Promise((resolve, reject) => 175 | setTimeout(() => { 176 | time.Now(); 177 | time.Now(); 178 | if (i % 2) { 179 | trace.metrics.get("i.2").add(i); 180 | resolve(); 181 | } else { 182 | reject(new Error("test.2")); 183 | } 184 | }, 10), 185 | ); 186 | return 1; 187 | }); 188 | } catch (e) { 189 | if (i % 2) { 190 | assert(false, "should not happen"); 191 | } else { 192 | expect((e as Error).message).toEqual("test.2"); 193 | } 194 | } 195 | } 196 | return r1 + r2; 197 | }); 198 | expect(ret).toBe(3); 199 | expect(trace.metrics.toJSON()).toEqual({ 200 | "/test/test.1/i.1": [0, 2], 201 | "/test/test.2/i.2": 1, 202 | }); 203 | const childs = Array.from(trace.childs.values()); 204 | const exp = childs.map((v) => v.invokes()); 205 | expect(exp).toEqual([ 206 | { 207 | ctx: { 208 | module: "trace", 209 | spanId: "test", 210 | value: "important", 211 | }, 212 | invokations: [ 213 | { 214 | start: refTime.Now(1).getTime(), 215 | end: refTime.Now(22).getTime(), 216 | result: "success", 217 | }, 218 | ], 219 | }, 220 | ]); 221 | const layered = Array.from(trace.childs.get("test")?.childs.values() || []); 222 | expect(layered.map((v) => v.invokes())).toEqual([ 223 | { 224 | ctx: { 225 | module: "trace", 226 | spanId: "test.1", 227 | value: "important", 228 | }, 229 | invokations: [ 230 | { 231 | start: refTime.Now(-2).getTime(), 232 | end: refTime.Now().getTime(), 233 | result: "success", 234 | }, 235 | { 236 | start: refTime.Now(-9).getTime(), 237 | end: refTime.Now().getTime(), 238 | result: "error", 239 | }, 240 | { 241 | start: refTime.Now(-16).getTime(), 242 | end: refTime.Now().getTime(), 243 | result: "success", 244 | }, 245 | ], 246 | metricRefs: { 247 | "/test/test.1/i.1": [0, 2], 248 | }, 249 | }, 250 | { 251 | ctx: { 252 | module: "trace", 253 | spanId: "test.2", 254 | value: "important", 255 | }, 256 | invokations: [ 257 | { 258 | start: refTime.Now(-4).getTime(), 259 | end: refTime.Now(4).getTime(), 260 | result: "error", 261 | }, 262 | { 263 | start: refTime.Now(-11).getTime(), 264 | end: refTime.Now(4).getTime(), 265 | result: "success", 266 | }, 267 | { 268 | start: refTime.Now(-18).getTime(), 269 | end: refTime.Now(4).getTime(), 270 | result: "error", 271 | }, 272 | ], 273 | metricRefs: { 274 | "/test/test.2/i.2": 1, 275 | }, 276 | }, 277 | ]); 278 | }); 279 | }); 280 | 281 | describe("metrics", () => { 282 | let time: Time; 283 | let trace: TraceNode; 284 | // const logger = MockLogger().logger.With().Module("trace").Str("value", "important").Logger() 285 | beforeEach(() => { 286 | time = WebSysAbstraction({ TimeMode: TimeMode.STEP }).Time(); 287 | trace = TraceNode.root(time); 288 | }); 289 | 290 | it("a simple metrics", () => { 291 | ["/test", "test", "/test/wurst", "bla"].forEach((path) => { 292 | const abs = path.startsWith("/") ? path : "/" + path; 293 | expect(trace.metrics.get(path).path).toBe(abs); 294 | expect(trace.metrics.get(path).value).toBeFalsy(); 295 | trace.metrics.get(path).add(4711); 296 | expect(trace.metrics.get(path).value).toBe(4711); 297 | trace.metrics.get(path).set(undefined); 298 | }); 299 | }); 300 | it("create metrics path", () => { 301 | trace.span("test", (trace) => { 302 | trace.span("test.1", (trace) => { 303 | trace.metrics.get("m1.1").add(1); 304 | trace.metrics.get("/test/test.1/m1.1").add(1); 305 | expect(trace.metrics.get("m1.1").path).toBe("/test/test.1/m1.1"); 306 | expect(trace.metrics.get("m1.1").value).toBe(2); 307 | }); 308 | }); 309 | }); 310 | it("typed span promise or literal", async () => { 311 | expect(trace.span("test", () => "1")).toBe("1"); 312 | expect(await trace.span("test", () => Promise.resolve(1))).toBe(1); 313 | }); 314 | }); 315 | -------------------------------------------------------------------------------- /ts/src/resolve-once.test.ts: -------------------------------------------------------------------------------- 1 | import { KeyedResolvOnce, ResolveOnce, ResolveSeq } from "@adviser/cement"; 2 | 3 | describe("resolve-once", () => { 4 | it("sequence", async () => { 5 | const once = new ResolveOnce(); 6 | 7 | const reallyOnce = vi.fn(async () => { 8 | return new Promise((resolve) => { 9 | setTimeout(() => { 10 | resolve(42); 11 | }, 100); 12 | }); 13 | }); 14 | const start = Date.now(); 15 | const fn = (): Promise => once.once(async () => reallyOnce()); 16 | expect(reallyOnce).toHaveBeenCalledTimes(0); 17 | expect(await fn()).toBe(42); 18 | expect(reallyOnce).toHaveBeenCalledTimes(1); 19 | expect(await fn()).toBe(42); 20 | expect(reallyOnce).toHaveBeenCalledTimes(1); 21 | expect(await fn()).toBe(42); 22 | expect(reallyOnce).toHaveBeenCalledTimes(1); 23 | const diff = Date.now() - start; 24 | expect(diff).toBeGreaterThanOrEqual(99); 25 | expect(diff).toBeLessThan(150); 26 | }); 27 | it("parallel", async () => { 28 | const once = new ResolveOnce(); 29 | const reallyOnce = vi.fn(async () => { 30 | return new Promise((resolve) => { 31 | setTimeout(() => { 32 | resolve(42); 33 | }, 100); 34 | }); 35 | }); 36 | const fn = (): Promise => once.once(async () => reallyOnce()); 37 | const start = Date.now(); 38 | expect( 39 | await Promise.all( 40 | Array(100) 41 | .fill(fn) 42 | .map((fn: () => Promise) => fn()), 43 | ), 44 | ).toEqual(Array(100).fill(42)); 45 | expect(reallyOnce).toHaveBeenCalledTimes(1); 46 | const diff = Date.now() - start; 47 | expect(diff).toBeGreaterThanOrEqual(99); 48 | expect(diff).toBeLessThan(150); 49 | }); 50 | 51 | it("works with void", async () => { 52 | const once = new ResolveOnce(); 53 | const reallyOnce = vi.fn(async () => { 54 | return new Promise((resolve) => { 55 | setTimeout(() => { 56 | resolve(); 57 | }, 100); 58 | }); 59 | }); 60 | const fn = (): Promise => once.once(async () => reallyOnce()); 61 | const start = Date.now(); 62 | expect( 63 | await Promise.all( 64 | Array(100) 65 | .fill(fn) 66 | .map((fn: () => Promise) => fn()), 67 | ), 68 | ).toEqual(Array(100).fill(undefined)); 69 | expect(reallyOnce).toHaveBeenCalledTimes(1); 70 | const diff = Date.now() - start; 71 | expect(diff).toBeGreaterThanOrEqual(99); 72 | expect(diff).toBeLessThan(150); 73 | }); 74 | 75 | it("throws", async () => { 76 | const once = new ResolveOnce(); 77 | const reallyOnce = vi.fn(async () => { 78 | return new Promise((rs, rj) => { 79 | setTimeout(() => { 80 | rj(new Error("nope")); 81 | }, 100); 82 | }); 83 | }); 84 | const fn = (): Promise => once.once(async () => reallyOnce()); 85 | const start = Date.now(); 86 | await new Promise((rs) => { 87 | for (let i = 0; i < 100; i++) { 88 | fn() 89 | .then(() => { 90 | assert.fail("should not happen"); 91 | }) 92 | .catch((e) => { 93 | expect(e).toEqual(new Error("nope")); 94 | expect(reallyOnce).toHaveBeenCalledTimes(1); 95 | if (i === 99) { 96 | rs(undefined); 97 | } 98 | }); 99 | } 100 | }); 101 | const diff = Date.now() - start; 102 | expect(diff).toBeGreaterThanOrEqual(99); 103 | expect(diff).toBeLessThan(150); 104 | }); 105 | 106 | it("preserves order", async () => { 107 | const once = new ResolveOnce(); 108 | const reallyOnce = vi.fn(async () => { 109 | return new Promise((resolve) => { 110 | setTimeout(() => { 111 | resolve(42); 112 | }, 100); 113 | }); 114 | }); 115 | let order = 0; 116 | const fn = async (): Promise => { 117 | const o = order++; 118 | const ret = await once.once(async () => reallyOnce()); 119 | return `${o}:${ret}`; 120 | }; 121 | const start = Date.now(); 122 | expect( 123 | await Promise.all( 124 | Array(100) 125 | .fill(fn) 126 | .map((fn: () => Promise) => fn()), 127 | ), 128 | ).toEqual( 129 | Array(100) 130 | .fill(undefined) 131 | .map((_, i) => `${i}:42`), 132 | ); 133 | expect(reallyOnce).toHaveBeenCalledTimes(1); 134 | const diff = Date.now() - start; 135 | expect(diff).toBeGreaterThanOrEqual(99); 136 | expect(diff).toBeLessThan(150); 137 | }); 138 | 139 | it("preserves call order to resolv order", async () => { 140 | const once = new ResolveOnce(); 141 | const reallyOnce = vi.fn(async () => { 142 | return new Promise((resolve) => { 143 | setTimeout(() => { 144 | resolve(42); 145 | }, 100); 146 | }); 147 | }); 148 | const start = Date.now(); 149 | const orderFn = vi.fn(); 150 | const fns = Array(100) 151 | .fill(0) 152 | .map((_, i) => { 153 | return once 154 | .once(() => reallyOnce()) 155 | .then((once) => { 156 | orderFn(i, once); 157 | // expect(i).toBe(order++); 158 | return `${i}:${once}`; 159 | }); 160 | }); 161 | expect(await Promise.all(fns)).toEqual( 162 | Array(100) 163 | .fill(undefined) 164 | .map((_, i) => `${i}:42`), 165 | ); 166 | expect(reallyOnce).toHaveBeenCalledTimes(1); 167 | const diff = Date.now() - start; 168 | expect(diff).toBeGreaterThanOrEqual(99); 169 | expect(diff).toBeLessThan(150); 170 | expect(orderFn).toHaveBeenCalledTimes(100); 171 | expect(orderFn.mock.calls.map(([i]) => i as number)).toEqual( 172 | Array(100) 173 | .fill(0) 174 | .map((_, i) => i), 175 | ); 176 | }); 177 | 178 | it("reset", async () => { 179 | const once = new ResolveOnce(); 180 | const orderFn = vi.fn(() => Promise.resolve(42)); 181 | await once.once(orderFn); 182 | await once.once(orderFn); 183 | await once.once(orderFn); 184 | once.reset(); 185 | await once.once(orderFn); 186 | await once.once(orderFn); 187 | once.reset(); 188 | await once.once(orderFn); 189 | await once.once(orderFn); 190 | once.reset(); 191 | expect(orderFn).toHaveBeenCalledTimes(3); 192 | }); 193 | 194 | it("keyed", async () => { 195 | const keyed = new KeyedResolvOnce(); 196 | const a_orderFn = vi.fn(() => Promise.resolve(42)); 197 | const b_orderFn = vi.fn(() => Promise.resolve(42)); 198 | for (let i = 0; i < 5; i++) { 199 | await keyed.get("a").once(a_orderFn); 200 | await keyed.get(() => "a").once(a_orderFn); 201 | await keyed.get("b").once(b_orderFn); 202 | await keyed.get(() => "b").once(b_orderFn); 203 | expect(a_orderFn).toHaveBeenCalledTimes(i + 1); 204 | expect(b_orderFn).toHaveBeenCalledTimes(i + 1); 205 | keyed.reset(); 206 | } 207 | }); 208 | 209 | it("keyed with pass ctx", async () => { 210 | const keyed = new KeyedResolvOnce(); 211 | const a_orderFn = vi.fn((key) => Promise.resolve(key)); 212 | const b_orderFn = vi.fn((key) => Promise.resolve(key)); 213 | await Promise.all([ 214 | keyed.get("a").once(a_orderFn), 215 | keyed.get(() => "a").once(a_orderFn), 216 | keyed.get("b").once(b_orderFn), 217 | keyed.get(() => "b").once(b_orderFn), 218 | ]); 219 | expect(a_orderFn).toHaveBeenCalledTimes(1); 220 | expect(a_orderFn).toHaveBeenCalledWith("a"); 221 | expect(b_orderFn).toHaveBeenCalledTimes(1); 222 | expect(b_orderFn).toHaveBeenCalledWith("b"); 223 | }); 224 | 225 | it("keyed asyncGet", async () => { 226 | const keyed = new KeyedResolvOnce(); 227 | const a_orderFn = vi.fn((key) => Promise.resolve(key)); 228 | const b_orderFn = vi.fn((key) => Promise.resolve(key)); 229 | await Promise.all([ 230 | keyed 231 | .asyncGet(async () => { 232 | await new Promise((resolve) => setTimeout(resolve, 100)); 233 | return "a"; 234 | }) 235 | .then((resolveOnce) => resolveOnce.once(a_orderFn)), 236 | keyed 237 | .asyncGet(async () => { 238 | await new Promise((resolve) => setTimeout(resolve, 50)); 239 | return "b"; 240 | }) 241 | .then((resolveOnce) => resolveOnce.once(b_orderFn)), 242 | ]); 243 | expect(a_orderFn).toHaveBeenCalledTimes(1); 244 | expect(a_orderFn).toHaveBeenCalledWith("a"); 245 | expect(b_orderFn).toHaveBeenCalledTimes(1); 246 | expect(b_orderFn).toHaveBeenCalledWith("b"); 247 | }); 248 | 249 | function shuffle(array: T[]): T[] { 250 | let currentIndex = array.length; 251 | 252 | // While there remain elements to shuffle... 253 | while (currentIndex != 0) { 254 | // Pick a remaining element... 255 | const randomIndex = Math.floor(Math.random() * currentIndex); 256 | currentIndex--; 257 | 258 | // And swap it with the current element. 259 | [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; 260 | } 261 | return array; 262 | } 263 | 264 | it("ResolveSeq", async () => { 265 | const seq = new ResolveSeq(); 266 | let enter = 0; 267 | let leave = 0; 268 | const actions = Array(10) 269 | .fill(0) 270 | .map((_, i) => { 271 | return seq.add(async () => { 272 | expect(enter++).toBe(i); 273 | await new Promise((resolve) => setTimeout(resolve, i * 3)); 274 | await new Promise((resolve) => setTimeout(resolve, i * 2)); 275 | expect(leave++).toBe(i); 276 | expect(leave).toBe(enter); 277 | return i; 278 | }, i); 279 | }); 280 | const ret = await Promise.all(shuffle(actions)); 281 | expect(ret.length).toBe(10); 282 | expect(enter).toBe(10); 283 | expect(leave).toBe(10); 284 | }); 285 | 286 | it("with promise", async () => { 287 | const once = new ResolveOnce(); 288 | let val = 42; 289 | const fn = async (): Promise => { 290 | return new Promise((resolve) => { 291 | setTimeout(() => { 292 | resolve(val++); 293 | }, 10); 294 | }); 295 | }; 296 | expect(await once.once(fn)).toBe(42); 297 | expect(await once.once(fn)).toBe(42); 298 | }); 299 | 300 | it("without promise", () => { 301 | const once = new ResolveOnce(); 302 | let val = 42; 303 | const fn = (): number => val++; 304 | expect(once.once(fn)).toBe(42); 305 | expect(once.once(fn)).toBe(42); 306 | }); 307 | 308 | it("without promise but exception", () => { 309 | const once = new ResolveOnce(); 310 | let val = 42; 311 | const fn = (): Promise => { 312 | throw new Error(`nope ${val++}`); 313 | }; 314 | expect(() => once.once(fn)).toThrowError("nope 42"); 315 | expect(() => once.once(fn)).toThrowError("nope 42"); 316 | }); 317 | }); 318 | --------------------------------------------------------------------------------