{"id":40481856,"url":"https://github.com/e280/stz","last_synced_at":"2026-01-20T18:33:19.842Z","repository":{"id":287622822,"uuid":"965283741","full_name":"e280/stz","owner":"e280","description":"🏂 everyday typescript fns","archived":false,"fork":false,"pushed_at":"2025-11-23T18:29:56.000Z","size":414,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-23T20:23:56.031Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/e280.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-12T20:17:13.000Z","updated_at":"2025-11-23T18:29:57.000Z","dependencies_parsed_at":"2025-05-08T07:24:05.742Z","dependency_job_id":"498b9e9a-4f1a-4398-a9c9-5dbb30a7578c","html_url":"https://github.com/e280/stz","commit_stats":null,"previous_names":["e280/stz"],"tags_count":67,"template":false,"template_full_name":null,"purl":"pkg:github/e280/stz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fstz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fstz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fstz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fstz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/e280","download_url":"https://codeload.github.com/e280/stz/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fstz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28609120,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T16:10:39.856Z","status":"ssl_error","status_checked_at":"2026-01-20T16:10:39.493Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-20T18:33:19.072Z","updated_at":"2026-01-20T18:33:19.806Z","avatar_url":"https://github.com/e280.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# 🏂 `@e280/stz`\n\n**stz** is e280's standard library of environment-agnostic typescript tools. zero dependencies.\n\n\n\n\u003cbr/\u003e\n\n## 🥨 stz primitives\n\n### 🍏 `pub` and `sub`\n\u003e ergonomic event emitters\n\n```ts\nimport {pub, sub} from \"@e280/stz\"\n```\n\n#### `pub`\n- make a publisher fn\n  ```ts\n  // create a pub fn\n  const sendMessage = pub\u003c[string]\u003e()\n\n  // subscribe to it\n  sendMessage.subscribe(m =\u003e console.log(m))\n\n  // publish to it\n  sendMessage(\"hello\")\n  ```\n\n#### `sub`\n- make a subscriber fn — *it's just like pub, except it's flipsy-reversey!*\n  ```ts\n  // create a sub fn\n  const onMessage = sub\u003c[string]\u003e()\n\n  // subscribe to it\n  onMessage(m =\u003e console.log(m))\n\n  // publish to it\n  onMessage.publish(\"hello\")\n  ```\n\n#### pub vs sub\n- pub and sub both have the same facilities\n  - `.publish`\n  - `.subscribe`\n  - `.on`\n  - `.next`\n  - `.clear`\n- i seem to use `sub` more often\n\n#### the more you know, about pubsub\n- publish actually returns a promise, to wait for all async subscribers\n  ```ts\n  await onMessage.publish(\"hello\")\n  ```\n- subscribe returns a fn to unsubscribe\n  ```ts\n  const unsubscribe = onMessage(() =\u003e {})\n  unsubscribe()\n  ```\n- `.clear()` to wipe all subscribed listeners\n  ```ts\n  onMessage.clear()\n  ```\n- `.next(fn?)` is a better way to do .once..  \n  - you can use it like a .once:\n    ```ts\n    onMessage.next(message =\u003e {})\n    ```\n  - but it also gives you a promise like this:\n    ```ts\n    const [message] = await onMessage.next()\n    ```\n  - of course the promise can be used like this:\n    ```ts\n    onMessage.next().then(([message]) =\u003e {})\n    ```\n\n### 🍏 defer\n\u003e defer the resolve/reject of a promise to the outside\n\n```ts\nimport {defer} from \"@e280/stz\"\n\nconst deferred = defer()\n```\n\n- resolve the deferred promise\n    ```ts\n    deferred.resolve()\n    ```\n- reject the deferred promise\n    ```ts\n    deferred.reject(new Error(\"fail\"))\n    ```\n- await the promise\n    ```ts\n    await deferred.promise\n    ```\n\n### 🍏 nap\n\u003e sleep for some milliseconds\n\n```ts\nimport {nap} from \"@e280/stz\"\n\nawait nap(900)\n  // wait for 900 milliseconds\n```\n\n### 🍏 all\n\u003e it's just sugar for `Promise.all`\n\n```ts\nimport {all} from \"@e280/stz\"\n\nawait all(\n  nap(500),\n  Promise.resolve(\"hello\"),\n  fetch(\"whatever.json\"),\n)\n```\n\n### 🍏 concurrent\n\u003e sugar for `Promise.all`, but returns named things as an object\n\n```ts\nimport {concurrent} from \"@e280/stz\"\n\ncons {slept, hello, whatever} = await concurrent({\n  slept: nap(500),\n  hello: Promise.resolve(\"hello\"),\n  whatever: fetch(\"whatever.json\"),\n})\n```\n\n### 🍏 disposer\n\u003e easy trash management\n\n```ts\nimport {disposer} from \"@e280/stz\"\n```\n\n- create a disposer\n    ```ts\n    const dispose = disposer()\n    ```\n- schedule something for cleanup\n    ```ts\n    dispose.schedule(() =\u003e console.log(\"disposed!\"))\n    ```\n- schedule multiple things at once\n    ```ts\n    dispose.schedule(\n      () =\u003e console.log(\"disposed thing 1\"),\n      () =\u003e console.log(\"disposed thing 2\"),\n      () =\u003e ev(window, {keydown: () =\u003e console.log(\"keydown\")}),\n    )\n    ```\n- schedule is chainable if you prefer that vibe\n    ```ts\n    dispose\n      .schedule(() =\u003e console.log(\"disposed thing 1\"))\n      .schedule(() =\u003e console.log(\"disposed thing 2\"))\n      .schedule(() =\u003e ev(window, {keydown: () =\u003e console.log(\"keydown\")}))\n    ```\n- **dispose** of all that garbage\n    ```ts\n    dispose()\n    ```\n\n### 🍏 G Crew\n\u003e extended js data types\n\n#### GMap\n\u003e extended js Map\n- many are saying it's *\"The Deluxe Mapping Experience\"*\n  ```ts\n  import {GMap} from \"@e280/stz\"\n\n  const map = new GMap\u003cnumber, string\u003e([\n    [1, \"hello\"],\n    [2, \"world\"],\n  ])\n  ```\n- `map.require(key)` — returns the value for key.. if missing, throw an error\n  ```ts\n  const value = map.require(1)\n    // \"hello\"\n  ```\n- `map.guarantee(key, makeFn)` — returns the value for `key`.. if missing, run `makeFn` to set and return the value\n  ```ts\n  const value = map.guarantee(3, () =\u003e \"rofl\")\n    // \"rofl\"\n  ```\n\n#### GSet\n\u003e extended js Set\n- `new GSet\u003cT\u003e()`\n- `set.adds(item1, item2, item3)` — add multiple items without a for-loop\n- `set.deletes(item1, item2, item3)` — add multiple items without a for-loop\n\n#### GWeakMap\n\u003e extended js WeakMap\n- `new GWeakMap\u003cK, V\u003e()`\n- `weakMap.require(key)` — returns value for key.. if missing, throw an error\n- `weakMap.guarantee(key, makeFn)` — returns the value for key.. if missing, run `makeFn` to set and return the value\n\n\n\n\u003cbr/\u003e\n\n## 🥨 stz fn tools\n\n### 🍏 `queue(fn)`\n\u003e execute calls in sequence (not concurrent)\n\n```ts\nimport {queue, nap} from \"@e280/stz\"\n\nconst fn = queue(async() =\u003e nap(100))\n\nfn()\nfn()\nawait fn() // waits for the previous calls (sequentially)\n```\n\n### 🍏 `once(fn)`\n\u003e ensure a fn is only executed one time\n\n```ts\nimport {once} from \"@e280/stz\"\n\nlet count = 0\nconst fn = once(() =\u003e count++)\nconsole.log(count) // 0\n\nfn()\nconsole.log(count) // 1\n\nfn()\nconsole.log(count) // 1\n```\n\n### 🍏 `deadline(100, fn)`\n\u003e throws an error if the async function takes too long\n\n```ts\nimport {deadline} from \"@e280/stz\"\n\nconst fn = deadline(100, async() =\u003e {\n\n  // example deliberately takes too long\n  await nap(200)\n})\n\nawait fn()\n  // DeadlineError: deadline exceeded (0.1 seconds)\n```\n\n### 🍏 `debounce(100, fn)`\n\u003e wait some time before actually executing the fn (absorbing redundant calls)\n\nwe use `debounce` a lot in ui code, like on a user's keyboard input in a form field, but rendering the form input can actually be slow enough that it causes problems when they type fast — to eliminate the jank, we `debounce` with like 400 ms, so we wait for the user to finish typing for a moment before actually running the validation.\n\n```ts\nimport {debounce} from \"@e280/stz\"\n\nconst fn = debounce(100, async() =\u003e {\n  await coolAction()\n})\n\n// each fn() call resets the timer\nfn()\nfn()\nfn()\n\n// coolAction is only called once here, other calls are redundant\n```\n\n### 🍏 `microbounce(fn)`\n\u003e collapse multiple calls into a single call (uses queueMicrotask under the hood)\n\nit's like `debounce(0, fn)` but more efficient by using queueMicrotask instead of setTimeout\n\n```ts\nimport {microbounce} from \"@e280/stz\"\n\nconst fn = microbounce(async() =\u003e coolAction())\nfn()\nfn()\nfn() // previous calls are redundant\n```\n\n### 🍏 `cycle(fn)`\n\u003e execute a function over and over again, back to back\n\n```ts\nimport {cycle} from \"@e280/stz\"\n\nlet ticks = 0\n\nconst stop = cycle(async() =\u003e {\n\n  // use a nap to add a delay between each execution\n  await nap(200)\n\n  ticks++\n})\n\n// stop repeating whenever you want\nstop()\n```\n\n\n\n\u003cbr/\u003e\n\n## 🥨 stz data utilities\n\n### 🍏 txt\n\u003e convert to/from utf8 string format\n- `txt.fromBytes(bytes)` — bytes to string\n- `txt.toBytes(string)` — string to bytes\n\n### 🍏 bytes\n\u003e utilities for dealing with Uint8Array\n- `bytes.eq(bytesA, bytesB)` — check if two byte arrays are equal\n- `bytes.random(32)` — generate crypto-random bytes\n\n### 🍏 BaseX utilities\n\u003e convert binary data to/from various encodings\n\n```ts\nimport {hex, base58, base64} from \"@e280/stz\"\n```\n\n#### hex\n\u003e all BaseX utilities have these methods\n- `hex.fromBytes(u8array)` — encode bytes to string\n- `hex.toBytes(str)` — decode string to bytes\n- `hex.toInteger(string)` — decode string as js integer\n- `hex.fromInteger(n)` — encode js integer as a string\n- `hex.random(32)` — generate random encoded string (32 bytes)\n\n#### all BaseX utilities\n- `hex`\n- `base2`\n- `base36`\n- `base58`\n- `base62`\n- `base64`\n- `base64url`\n\n#### make a custom BaseX utility\n- you can provide a `lexicon` to produce your own BaseX codec\n    ```ts\n    const myHex = new BaseX({characters: \"0123456789abcdef\"})\n    ```\n\n#### tiny timestamps\n- fun fact: you can make insanely compact timestamp strings like this:\n  ```ts\n  base62.fromInteger(Date.now() / 1000)\n    // \"1uK3au\"\n  ```\n  - `1748388028` base10 epoch seconds (10 chars)\n  - `1uK3au` base62 epoch seconds (6 chars)\n  - *nice*\n\n### 🍏 bytename\n\u003e friendly string encoding for binary data\n\na bytename looks like `\"midsen.picmyn.widrep.baclut dotreg.filtyp.nosnus.siptev\"`. that's 16 bytes. each byte maps to a three-letter triplet\n\nthe bytename parser (`bytename.toBytes`) ignores all non-alphabetic characters. thus `midsen.picmyn`, `midsenpicmyn`, and `mid@sen$pic@myn` are all equal.\n\n```ts\nimport {bytename} from \"@e280/stz\"\n```\n- ```ts\n  bytename.fromBytes(new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]))\n    // \"ribmug.hilmun\"\n  ```\n- ```ts\n  bytename.toBytes(\"ribmug.hilmun\")\n    // Uint8Array, 4 bytes\n  ```\n- ```ts\n  const data = new Uint8Array([\n    0xDE, 0xAD, 0xBE, 0xEF,\n    0xDE, 0xAD, 0xBE, 0xEF,\n  ])\n\n  bytename.fromBytes(data, {\n    groupSize: 2, // default is 4\n    groupSeparator: \" \",\n    wordSeparator: \".\",\n  })\n    // \"ribmug.hilmun ribmug.hilmun\"\n  ```\n\n### 🍏 thumbprint\n\u003e hybrid of bytename and base58 to make binary data more human-friendly\n- looks like `nodlyn.fasrep.habbud.ralwel.Avo7gFmdWMRHkwsD149mcaBoZdS69iXuJ`\n- the idea is that the first parts are in bytename format, so it's easy for humans to recognize\n- and the remaining data is shown in base58\n- `thumbprint.fromBytes(u8array)` — encode bytes to thumbprint string\n- `thumbprint.toBytes(thumbstring)` — decode thumbprint string to bytes\n- `thumbprint.fromHex(hexstring)` — convert a hex string into a thumbprint\n- `thumbprint.toHex(thumbstring)` — convert a thumbprint into a hex string\n\n### 🍏 toq\n\u003e tar-like binary file format for efficiently packing multiple files together\n\n```ts\nimport {toq, txt} from \"@e280/stz\"\n```\n\n#### data layout\n- 4 magic bytes `\"TOQ\\x01\"`\n- for each file (little endian)\n  - `name length` 1 byte (u8)\n  - `name` x bytes (max 255 B)\n  - `data length` 4 bytes (u32)\n  - `data` x bytes (max 4 GB)\n\n#### toq pack/unpack\n- **toq.pack** — accepts any iterable of file entries\n    ```ts\n    const pack: Uint8Array = toq.pack([\n      [\"hello.txt\", txt.toBytes(\"hello world\")],\n      [\"deadbeef.data\", new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])],\n    ])\n    ```\n- **toq.is** — check if a file is a toq pack or not\n    ```ts\n    toq.is(pack) // true\n    ```\n- **toq.unpack** — generator fn yields file entries\n    ```ts\n    for (const [name, data] of toq.unpack(pack))\n      console.log(name, data.length)\n    ```\n\n#### toq works nice with maps\n- **pack a map of files**\n    ```ts\n    const files = new Map\u003cstring, Uint8Array\u003e()\n    files.set(\"hello.txt\", txt.toBytes(\"hello world\"))\n    files.set(\"deadbeef.data\", new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]))\n\n    const pack = toq.pack(files)\n    ```\n- **unpack into a new map**\n    ```ts\n    const files = new Map(toq.unpack(pack))\n    ```\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n## 💖 stz is by e280\nreward us with github stars  \nbuild with us at https://e280.org/ but only if you're cool  \n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fe280%2Fstz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fe280%2Fstz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fe280%2Fstz/lists"}