Skip to content
TakibluPublic

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Taki_Kit

Professional Bun + TypeScript Toolkit for FiveM Developers

Taki_Kit © Taki

A framework for building large-scale FiveM (CFX) resources with TypeScript, using Bun as the development environment, package manager, bundler and test runner.

  • Zero runtime dependencies. Nothing is installed into your resource.
  • Client and server are provably isolated. A server-only import in a client bundle is a build failure, not a review comment.
  • Type-safe events and callbacks. A wrong payload is a compile error.
  • Untrusted input by default. Every inbound request passes a fixed security checklist: source, payload size, rate limit, permission, schema.
  • No leaks by construction. Every listener, timer and pending request is tracked and released on resource stop.

Table of contents

  1. Why TypeScript
  2. Why Bun
  3. Install
  4. Build process
  5. FiveM setup
  6. Architecture
  7. Client development
  8. Server development
  9. Shared code
  10. Events
  11. Callbacks
  12. Commands
  13. Players
  14. Permissions
  15. Security
  16. NUI
  17. Taki_UI
  18. Database
  19. Framework adapters
  20. Resource lifecycle
  21. Testing
  22. Troubleshooting
  23. Examples
  24. Contributing
  25. Project reference

Why TypeScript

On a FiveM server, every client is an attacker. They can call any net event, with any payload, as often as they like. TypeScript is not about types being nice; it is about making the class of mistake that gets your server owned impossible to write:

// A typo, or a payload from a mismatched client version. Caught at build time.
Taki.Events.emitServer("taki:player:data", { source: "1" });
//                                ^^^^^^^^^^^^^^^^^^^^^^^ string, not number

The type system also does the tedious work that would otherwise be a hundred if (typeof x !== "number") throw lines, and — because it runs through a schema engine — it becomes an actual runtime check, not just a hint.

Taki_Kit is compiled at strict plus exactOptionalPropertyTypes, noUncheckedIndexedAccess, noUnusedLocals and noUnusedParameters.

Why Bun

Bun is used for everything that happens before the code reaches a player:

Stage Tool
Install bun install (one bun.lock, no npm/yarn/pnpm lockfile)
Bundling Bun.build
Testing bun test
Type checking tsc via bun run typecheck
Linting / formatting eslint, prettier via bunx

Bun is not the FiveM runtime. FiveM's client and server are V8 with a CFX shim; there is no Bun global. So Bun.file(), Bun.serve() and Bun.env are compile errors in this project (see Bun-API guard), and the tooling that legitimately needs them lives in tools/, which is never shipped.

bun test runs your tests in Bun, so a test cannot catch a Bun API that only breaks in FiveM. That is exactly why the build refuses to emit one.

Install

Requires Bun 1.2+.

bun install

That is the only install command. There is no npm install, and the repository contains no package-lock.json, yarn.lock or pnpm-lock.yaml.

Command Does
bun run build Type-safe production build (6 gates, see below)
bun run dev Rebuild on change, with the same gates
bun run typecheck tsc --noEmit over both TypeScript worlds
bun test Run the test suite
bun run lint ESLint (type-aware)
bun run format Prettier, writing changes
bun run check format → lint → typecheck → test → build
bun run clean Remove dist/

Adding a dependency:

bun add <package>          # runtime
bun add -d <package>       # dev

Taki_Kit itself has no runtime dependencies, so most projects never need bun add at all.

Build process

bun run build is a pipeline of gates. Each one fails loudly rather than emitting something subtly wrong.

1. clean          rm -rf dist/          a stale file can never be shipped
2. isolation      import-graph walk     client may not reach server code
3. Bun-API guard  source scan           no Bun.* in FiveM runtime code
4. bundle         Bun.build, IIFE       three separate outputs
5. scan output    bundle scan           no side-specific native in the wrong bundle
6. manifest       fxmanifest.lua        every referenced file exists

Output layout — client and server are never combined:

src/shared/index.ts   ->  dist/shared/taki-kit.js    (shared_script, both sides)
src/client/index.ts   ->  dist/client/taki-kit.js    (client_script)
src/server/index.ts   ->  dist/server/taki-kit.js    (server_script)

Why IIFE

Bun.build({ format: "iife" }) produces a self-contained script. FiveM executes each listed script in its own scope, and although modern CFX understands ESM, an IIFE is the only format that cannot be broken by script load order, a stale dist/, or a duplicated module instance.

Why one shared bundle

dist/shared/ runs on both sides and owns all the state: the event bus, the callback table, the player cache, the disposable store. The client and server bundles only attach their native bindings to it.

If each side inlined the core instead, Taki.Events would be two different objects in the same V8 context and nothing would ever be delivered.

Why the isolation check is a build gate

// src/client/foo.ts
import { getPlayers } from "../server/players";
//                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// This typechecks perfectly well. TypeScript has no idea which side it is
// compiling for.

Without the check, that mistake survives review and ships a player list to everyone. With it:

SIDE-ISOLATION VIOLATION
  src/client/foo.ts
    imports  ../server/players
    resolves src/server/players/index.ts
    reason   client bundle may not import "src/server/"
    chain    src/client/index.ts -> src/client/foo.ts -> src/server/players/index.ts

A second, independent gate then scans the emitted bundle for server-only natives (GetPlayers, DropPlayer, TriggerClientEvent, …), which catches anything the graph walk cannot see.

FiveM setup

fxmanifest.lua is included and validated by the build. It uses explicit paths, not globs, on purpose — a glob that silently matches nothing produces a resource that loads with no code and no error.

fx_version 'cerulean'
game 'gta5'

shared_scripts { 'dist/shared/taki-kit.js' }
client_scripts { 'dist/client/taki-kit.js' }
server_scripts { 'dist/server/taki-kit.js' }

shared_scripts must come first. FiveM executes a resource's shared scripts before its client and server scripts, which is what lets the side bundles extend the single Taki object. If you get this wrong the bundle says so:

Taki_Kit: the shared bundle did not load before the client bundle.

OneSync is a soft dependency: it is required for Taki.Server.requireNear, and distance checks report measurable: false rather than silently passing without it.

Architecture

src/
├── shared/          side-agnostic. Runs on BOTH sides.
│   ├── index.ts       entry: creates the core, publishes the `Taki` global
│   ├── config/        the one file most projects edit
│   ├── types/         event maps, framework contract, commands, database
│   ├── constants/     wire event names and safety limits
│   ├── validators/    the schema engine
│   └── utils/         pure helpers
│
├── core/            the runtime kernel. Pure: it never calls a native.
│   ├── install.ts      assembles every service into `Taki`
│   ├── natives/        the ONE interface the sides implement
│   ├── events/         typed event bus
│   ├── callbacks/      request/response transport
│   ├── security/       guard, rate limiter, permissions
│   ├── commands/       registry + argument parser
│   ├── players/        player service and cache
│   ├── database/       provider-agnostic facade + in-memory provider
│   ├── nui/            typed NUI bridge
│   ├── resources/      lifecycle + disposable store
│   ├── scheduler/      tracked timers
│   ├── logger/         structured, redacting logger
│   ├── errors/         the error hierarchy
│   └── config/         typed configuration
│
├── client/          client-only. Cannot import from server/.
├── server/          server-only. Cannot import from client/.
└── adapters/        framework adapters: standalone, esx, qbcore

Three decisions worth explaining

1. adapters/ lives under src/, not beside it. The brief's layout put adapters/ at the project root. They contain runtime FiveM code — they call exports.es_extended — so outside src/ they would not be typechecked, not be linted, not be covered by the isolation check, and not be built. That is a hole in the security boundary, so they moved.

2. Two tsconfigs, not one.

Project Covers Bun types?
tsconfig.json src/, types/ no (types: [])
tsconfig.tools.json tools/, tests/ yes

src/ is compiled without Bun's types, so Bun.file() in runtime code is a compile error before the Bun-API guard script even runs. A single config with Bun types would have made this a lint-only concern.

3. The native bridge is a single seam. src/core is pure. Everything needing the host is one interface (src/core/natives/bridge.ts) implemented by src/client/natives/ and src/server/natives/. This buys testability (the whole core runs under bun test with no FiveM), side isolation (server-only capabilities do not exist on the client bridge) and one auditable file listing every native the toolkit touches.

Client development

// src/game/client/main.ts  ->  bundled into dist/client/taki-kit.js

// Listen for a server push.
Taki.Events.onClient("taki:ui:toast", ({ type, title, message }) => {
    Toast.Show({ type, title, message });
});

// Send to the server.
Taki.Events.emitClient("taki:player:ready", { resource: GetCurrentResourceName() });

// Ask the server for something and await it.
const state = await Taki.Callback.call("taki:diagnostics:clientState");

Local player helpers:

if (!Taki.Client.isAlive()) return;

Taki.Client.whenReady(() => {
    // Runs once the ped exists; safe against the pre-spawn window.
});

Taki.NUI.send, Taki.NUI.callback and Taki.NUI.setFocus are also available on the client. See NUI.

Server development

// Guarded: this is what application code should use.
const Withdraw = Taki.Validator.tuple([Taki.Validator.number().int().positive().max(1_000_000)]);

Taki.Events.onServer(
    "bank:withdraw",
    {
        args: Withdraw,
        permission: "bank.withdraw",
        rateLimit: Taki.Security.defineRateLimit("bank", 3, 10_000),
    },
    async (source, amount) => {
        const player = Taki.Player.get(source);
        if (player === null) return;

        // Authoritative, never client-supplied.
        const balance = Taki.Player.getMoney(source, "bank") ?? 0;
        if (balance < amount) return;

        await Taki.Database.update("accounts", { balance: balance - amount }, { owner: source });
    },
);
// Push to a client, or broadcast.
Taki.Events.emitServer("taki:player:data", source, data);
Taki.Events.emitServerAll("taki:ui:toast", { type: "info", title: "Hi", message: "…" });

// Ask one client for something.
const result = await Taki.Callback.callClient(source, "taki:diagnostics:clientState");

Player lifecycle

A FiveM server has two join events, in an order the resource cannot control: the socket connecting (playerConnecting) and the framework loading the record (esx:playerLoaded). Taki_Kit tracks a player from the first and only publishes authoritative data after the second, so a handler never sees a half-loaded player.

Taki.bus.on("taki:framework:playerLoaded", (source: number, data: unknown) => { … });

Shared code

Anything in src/shared/ runs on both sides, which makes it the right home for configuration, validators, types and pure helpers.

The one rule: shared code must not need a native. If it does, it belongs in src/client or src/server, and the build will tell you.

Events

Both event maps are open global interfaces. Extend them from any module:

// src/shared/types/my-events.ts
import type {} from "../../shared/types/events";

declare global {
    interface TakiClientToServerEvents {
        "shop:open": { shopId: number };
        "shop:purchase": [item: string, quantity: number];
    }
}
Map Direction Listen Emit
TakiClientToServerEvents client → server onServer emitClient
TakiServerToClientEvents server → client onClient emitServer

onNet / emitNet are aliases of onClient / emitClient, for muscle memory.

A map entry may be a single payload or a positional tuple. Both are fully checked:

// "shop:open": { shopId: number }
Taki.Events.emitClient("shop:open", { shopId: 1 });   // ok
Taki.Events.emitClient("shop:open", { shopId: "1" }); // compile error

// "shop:purchase": [item: string, quantity: number]
Taki.Events.onServer("shop:purchase", (source, item, quantity) => { … });

Why ArgsOf looks unusual. A plain conditional such as M[K] extends readonly unknown[] ? M[K] : [M[K]] is rejected by TypeScript in a rest-parameter position ("A rest parameter must be of an array type"). ArgsOf uses infer R extends readonly unknown[], which keeps the type lazy and provably an array. Five formulations were tested; only this one both compiles and correctly rejects wrong payloads, missing arguments, extra arguments and unknown event names.

Every on/register returns a Disposer. Call it when you mean it.

Callbacks

A FiveM net event is one-way: no answer, no failure signal, no timeout. The naive request/response implementation is a global array of pending promises that grows forever when the server never answers.

Taki_Kit's transport fixes all three:

// Client asks the server.
const { echo } = await Taki.Callback.call("taki:diagnostics:ping", { nonce: "abc" });

// Server answers a client.
Taki.Callback.register("taki:diagnostics:ping", (source, { nonce }) => ({ echo: nonce }));

// Server asks a client.
const state = await Taki.Callback.callClient(source, "taki:diagnostics:clientState");
  • Unique ids — a monotonic counter plus a random suffix, so two players issuing the same logical request can never collide.
  • Bounded — config.callbacks.maxPending caps the table; beyond that, new calls reject instead of growing the heap.
  • Self-cleaning — every entry removes itself on resolve, reject, timeout, abort, and on resource stop. There is no path that leaks an entry, and the test suite asserts pendingCount === 0 after teardown.
  • Owner tracking — cancelForSource(source) on playerDropped, because FiveM never signals that a peer has gone away.

Commands

Taki.Commands.register(
    {
        name: "give",
        aliases: ["g"],
        description: "Give money to a player",
        side: "server",
        permission: "admin.give",
        args: [
            { name: "target", type: "player" },
            { name: "amount", type: "number", min: 1, max: 1_000_000 },
            { name: "reason", type: "string", rest: true, optional: true },
        ],
    },
    ({ source, args }) => {
        // args are typed: `target` is number, `amount` is number.
    },
);

Argument types: string, number, boolean, player. Parsing is strict on purpose:

  • number must be fully numeric — Number("1abc") is NaN and parseFloat("1abc") is 1; both would be wrong.
  • boolean accepts only true/false, not 1/yes.
  • player must be a connected slot.
  • maxLength and choices stop a client stuffing junk into a field.

A duplicate name or alias throws rather than overwriting: two resources disagreeing about who owns /teleport is a real bug, and the player should not silently get whichever won the race.

FiveM cannot unregister a command. Disposal therefore tombstones the entry: the command stops doing anything. A design that pretended otherwise would leave a live handler behind after every restart.

Built-ins: /taki version | debug | config | players | rate | permissions. /taki debug prints live listener, timer, callback and rate-limit counters — which is how you actually tell a memory leak from a slow query.

Players

Taki.Player.exists(source); // live presence, not the cache
Taki.Player.get(source); // framework data, or null
Taki.Player.require(source); // throws NotFoundError instead
Taki.Player.getName(source);
Taki.Player.getIdentifier(source); // prefers license:, survives identity change
Taki.Player.getMoney(source, "bank");
Taki.Player.getInventory(source);
Taki.Player.hasPermission(source, "admin.give");
Taki.Player.setData(source, "cart", items); // resource-local, never persisted

There is deliberately no setPlayerData(source, dataFromClient). A client that sends { identifier: "license:admin" } changes nothing, because no method accepts a client-supplied identifier.

getPlayer() returns null — not a placeholder — when a player is connected but the framework has not finished loading them.

Permissions

Taki.Permissions.defineRole({
    name: "moderator",
    permissions: ["admin.command.tp", "admin.command.kick"],
    inherits: ["staff"],
});

Taki.Permissions.grantRole(source, "moderator");
Taki.Permissions.has(source, "admin.command.tp");
Taki.Permissions.require(source, "admin"); // throws PermissionError

Hierarchical matching is on by default: holding admin satisfies admin.command.teleport. This removes the most common permission bug in large resources.

Permissions are always resolved server-side, from the framework's groups merged with resource grants. A client may ask to use a privileged feature; it never answers whether it is allowed to.

Security

Treat every client as hostile. Taki.Security.guard applies the whole checklist in one call, in this order:

# Check Why here
1 source is a real player rejects console and other resources
2 payload size bounded before any parsing work
3 rate limit keyed per player, so one abuser cannot exhaust others
4 permission before validation, so an unprivileged flooder never reaches the schema
5 schema validation declared shape and arity
6 business logic fully isolated from the FiveM event pump
Taki.Security.defineRateLimit("bank", 3, 10_000);
Taki.Security.rateLimit("bank:withdraw", 3, 10_000); // peek, does not spend
Taki.Security.validate(Withdraw, args);
Taki.Security.requirePermission(source, "admin");
Taki.Security.stats(); // rejections by reason

The limiter is a token bucket, not a fixed window: a fixed window lets a client send limit requests at 0.999s and limit more at 1.001s. The bucket table is bounded and evicts in insertion order, so a client that varies its own key cannot grow the map without limit.

Failures never escape into the FiveM event pump. A guarded handler that throws logs a structured error and is otherwise inert.

Never trust money, inventory, permissions, jobs, identifiers, ownership or admin status when supplied by the client. Taki_Kit provides no API that lets you.

NUI

// Client -> browser
Taki.NUI.send("shop:setItems", { items });
Taki.NUI.sendAll("shop:select", "bread", 3);
Taki.NUI.setFocus(true);
Taki.NUI.close();

// Browser -> client, with a result
Taki.NUI.callback("taki:diagnostics:ping", async (payload) => {
    return { echo: payload.nonce };
});

// Server -> one client's UI
Taki.NUI.sendTo(source, "bank:balance", { amount: 4200 });

A single argument is sent as-is; two or more are sent as an array. Use sendAll when the single payload is itself an array.

Taki.NUI is the raw transport. For notifications, menus, dialogs, progress and the rest, use Taki.UI instead - it is built on the same transport and adds no new concepts.

Taki_UI

A complete widget set - notifications, progress, context menus, dialogs, input forms, text overlays, status panels, keybind hints, loading and error states - with theming, localisation and accessibility already handled.

// Notifications. `duration: 0` keeps one on screen until you dismiss it.
Taki.UI.notify({ type: "success", title: "Success", message: "Vehicle spawned" });
Taki.UI.notify({ type: "error", title: "Failed", message: "You are not the driver." });

// A persistent loading state, replaced in place when you reuse the id.
Taki.UI.loading("save", "Saving…");
Taki.UI.notify({ id: "save", type: "success", title: "Saved" });

// Progress that animates and closes itself.
Taki.UI.progress({ label: "Repairing vehicle", duration: 2500 });

// A context menu resolves the chosen entry, or null if dismissed.
const choice = await Taki.UI.context.open({
    id: "vehicle_menu",
    title: "Vehicle",
    options: [
        { label: "Repair", action: "repair" },
        { label: "Lock", action: "lock" },
        { label: "Delete", action: "delete", tone: "danger" },
    ],
});
if (choice?.action === "repair") {
    // …
}

// A dialog resolves true/false (null on timeout).
if (await Taki.UI.dialog.open({ title: "Delete vehicle?", tone: "danger" })) {
    // …
}

// An input form resolves the values, or null if dismissed. Always strings.
const values = await Taki.UI.input.open({
    title: "Deposit",
    fields: [{ name: "amount", label: "Amount", type: "number", required: true, min: 1 }],
});

// Persistent HUD elements.
Taki.UI.text({ id: "checkpoint", text: "Los Santos", subtext: "Waypoint reached" });
Taki.UI.status({
    id: "vitals",
    position: "top-right",
    entries: [{ id: "fuel", label: "Fuel", value: 68, total: 100, tone: "warn" }],
});
Taki.UI.keybinds([{ id: "trunk", keys: ["E"], label: "Trunk" }]);

// Theme and locale, live.
Taki.UI.setTheme("crimson");
Taki.UI.setLocale("pt");

The framework-independence rule

Taki_Kit Core contains no UI framework. Not React, not Vue, not Svelte.

That is enforced by a build gate, not by convention. bun run build fails if any file under src/ imports one:

[1/8] Framework-independence gate
      ok   Taki_Kit Core is framework-independent (64 source files scanned, 0 UI framework imports).

React exists only in ui/src/react/, and only the NUI page imports it. The result is that the client bundle carries zero React - 54.5 KiB for the whole client - while the page itself is 470 KiB.

The split:

src/client/ui/ui-store.ts     all behaviour. no DOM, no React, no FiveM
src/shared/types/ui.ts        the wire contract: UiCommand / UiState / UiResult
ui/src/bridge.ts              the ONLY file that touches window.postMessage
ui/src/react/                 the reference renderer

Because the store is pure, every behaviour is unit-tested under bun test with no browser and no game. Swapping the renderer means replacing one folder.

Working on the UI

bun run dev:ui           # preview at http://127.0.0.1:5173, rebuilds on save
bun run build:ui         # minified production page into web/
bun run build:ui --dev   # unminified, with a source map, for devtools

dev:ui serves the real page in standalone mode: GetParentResourceName() does not exist in a browser, so the bridge logs that once and renders anyway. A dev control panel is injected to drive every widget. It is never bundled into a real resource - main.tsx does not import it.

web/ is generated and git-ignored. Run bun run build before starting a server; fxmanifest.lua references web/app.js and the manifest check will tell you if it is missing.

Full documentation: docs/UI.md.

Database

Taki.Database.use(oxmysqlProvider());

const rows = await Taki.Database.query<PlayerRow>("SELECT * FROM players WHERE id = ?", [id]);
await Taki.Database.insert("players", { identifier, name });
await Taki.Database.update("players", { name }, { id });       // WHERE required
await Taki.Database.delete("players", { id });                 // WHERE required
await Taki.Database.transaction(async (tx) => { … });

Taki_Kit does not depend on a SQL library. DatabaseProvider is the whole surface; oxmysql is an adapter, and a MemoryProvider ships for tests.

Helpers never interpolate values — only ? placeholders. Identifiers (table/column names), which cannot be parameterised, are validated against ^[A-Za-z_][A-Za-z0-9_]*$ and backtick-quoted. Stacked statements are refused, because that is how a read-only endpoint becomes a write primitive:

await Taki.Database.query("SELECT 1; DROP TABLE users");
// NetworkError: Refusing a statement containing multiple statements.

Taki.Database.useMemory() makes the whole subsystem testable with no server.

Framework adapters

Taki.useFramework(new EsxAdapter(Taki)); // or explicit via config
Taki.framework()?.name; // "esx"

Taki_Kit ships standalone, esx (and ESX-Legacy) and qbcore (and QBox). config.framework.adapter: "auto" probes for ESX, then QBCore, then falls back to standalone — deferred and retried, because resource start order is not under your control.

src/core never imports a concrete adapter, and an adapter never reaches into core internals. The contract is FrameworkAdapter in src/shared/types/framework.ts.

The standalone adapter is not a stub. It supplies identity (so commands, permissions and callbacks work on a frameworkless server) and reports money, inventory and job as null rather than 0. A fabricated zero balance invites a resource to write to an account that does not exist.

exports is read lazily in every adapter. A module-level capture would snapshot it before the framework resource finishes starting, and then every lookup would silently return undefined.

Resource lifecycle

const off = Taki.Events.onServer("shop:open", handler);
off(); // detach for good

Taki.Resource.onStop(() => saveEverything());
Taki.Resource.exists("es_extended");
Taki.Resource.getState("qb-core");
Taki.Resource.isStarted("oxmysql");
await Taki.Resource.waitForStart("es_extended", 30_000);

Every register, on and schedule is tracked in one DisposableStore, which is drained on onResourceStop. A FiveM resource restart is then genuinely clean — no duplicate events, no orphaned timers.

Testing

bun test

223 tests covering validators, utilities, permissions, configuration, event schemas, callback logic, error handling, commands, the database, and a full end-to-end pass over the real createCore().

The FiveM runtime is mocked in tests/mocks/fivem.ts, installed via bunfig.toml so it is present before any test module loads:

[test]
preload = ["./tests/setup.ts"]

The mock is deliberately honest: it has a real event bus (so a disposer that does not detach is caught), a real player table, and GetPlayerName(999) returns undefined for a slot that does not exist, exactly as the real native does. A permissive stub would let tests pass for the wrong reason.

Switch the simulated side:

TAKI_TEST_SIDE=client bun test

What is deliberately not mocked: setTimeout, Promise, JSON, Math, Date. Those are identical in Bun and in FiveM's V8, so mocking them would only make the tests less honest.

Troubleshooting

"the shared bundle did not load before the client bundle" shared_scripts is missing, or listed after client_script. Check fxmanifest.lua, then bun run build to confirm all three files exist.

"my code doesn't run" and the console is empty Almost always a manifest glob that matches nothing. This project uses explicit paths; bun run check validates every reference against dist/.

ReferenceError: Bun is not defined in production A Bun API reached runtime code. It compiles and passes bun test (which runs in Bun). bun run build blocks this; if you hit it, a tsconfig was widened.

"a handler fires twice after restarting my resource" Something registered outside Taki_Kit, so it is not in the disposable store. Route it through Taki.Events.on* or register the disposer with Taki.Resource.onStop.

Taki.Permissions.has is false for a framework admin The framework's permission list is only merged when it exposes one. Grant explicitly: Taki.Permissions.grantRole(source, "admin").

measurable: false from a distance check OneSync is off. Pass { requireMeasurable: false } only for actions that genuinely do not depend on position.

Too many pending callback requests A handler is registered but never responds. /taki rate shows the count and the oldest pending request.

A callback times out only on a busy server Raise config.callbacks.defaultTimeoutMs, or maxTimeoutMs if you raise it past the ceiling — the ceiling is clamped silently.

Examples

  • web/ — a complete NUI page using the typed bridge in both directions.
  • src/shared/config/config.ts — every knob, documented inline.
  • src/server/commands/index.ts — the /taki diagnostics command tree.
  • src/adapters/standalone/index.ts — the smallest useful FrameworkAdapter.
  • tests/integration.test.ts — the full guarded request path, end to end.

Contributing

bun install
bun run check        # format, lint, typecheck, test, build

House rules, in the project's stated priority order:

Security → Type safety → FiveM compatibility → Maintainability →
Developer experience → Performance → Extensibility
  • Never add a runtime dependency. If you need one, it is an adapter.
  • Never add an abstraction without a concrete consumer.
  • import type across a side boundary is fine (it is erased). A value import is not.
  • Every register/on/schedule returns a disposer.
  • No any in src/. Prefer a discriminated union, a generic, or a type guard.
  • Prefer a plain Error subclass over a clever factory.

Further reading:

  • docs/ARCHITECTURE.md - the three-bundle split, why import type crosses a side boundary, and the build gates.
  • docs/SECURITY.md - the trust boundaries, what is validated where, and why client-side validation is UX rather than security.
  • docs/UI.md - the widget layer, the wire protocol, and how to write your own renderer.

Tests are part of the contract, not an afterthought: tests/docs.test.ts fails if this README and the real API disagree, so a renamed method cannot ship with documentation still describing the old one.

Project reference

Path Purpose
src/shared/index.ts shared entry; publishes the Taki global
src/client/index.ts client entry; attaches client natives
src/server/index.ts server entry; attaches server natives
src/core/install.ts assembles the core
src/core/natives/bridge.ts the native seam
src/shared/validators/schema.ts the schema engine
src/core/security/guard.ts the security envelope
src/core/security/rate-limiter.ts token bucket
src/core/callbacks/callback-transport.ts request/response
tools/build.ts the 6-gate build
tools/check-isolation.ts side-isolation graph walk
tools/check-bun-apis.ts Bun-API guard
tools/check-bundle.ts emitted-output scanner
tools/check-manifest.ts fxmanifest validator
types/fivem.d.ts minimal FiveM runtime declarations
types/runtime.d.ts timers, console, V8 extras

Third-party libraries retain their original licenses and attribution. Taki_Kit itself has no runtime dependencies.


Taki_Kit © Taki. All rights reserved.

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages