toiljs 0.0.32 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +68 -34
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +73 -12
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/cache.d.ts +8 -0
- package/build/devserver/cache.js +0 -0
- package/build/devserver/index.js +10 -1
- package/examples/basic/server/README.md +19 -0
- package/examples/basic/server/{HelloHandler.ts → core/AppHandler.ts} +9 -4
- package/examples/basic/server/core/store.ts +31 -0
- package/examples/basic/server/main.ts +11 -2
- package/examples/basic/server/models/NewPlayer.ts +5 -0
- package/examples/basic/server/models/Player.ts +8 -0
- package/examples/basic/server/models/ScoreDelta.ts +5 -0
- package/examples/basic/server/models/Standings.ts +7 -0
- package/examples/basic/server/routes/Leaderboard.ts +20 -0
- package/examples/basic/server/routes/Players.ts +53 -0
- package/examples/basic/server/scheduled/README.md +7 -0
- package/examples/basic/server/services/Stats.ts +11 -0
- package/examples/basic/server/services/remotes.ts +7 -0
- package/package.json +2 -2
- package/server/runtime/response.ts +56 -0
- package/src/cli/create.ts +54 -14
- package/src/cli/notify.ts +4 -4
- package/src/cli/ui.ts +62 -3
- package/src/devserver/cache.ts +0 -0
- package/src/devserver/index.ts +21 -1
- package/test/devserver.test.ts +6 -4
- package/test/fixtures/bignum-wire/spec.ts +27 -0
- package/test/rpc-bignum-wire.test.ts +164 -0
- package/test/ui.test.ts +40 -0
- package/examples/basic/server/api.ts +0 -137
package/test/ui.test.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { box, tagline } from '../src/cli/ui';
|
|
4
|
+
|
|
5
|
+
describe('tagline', () => {
|
|
6
|
+
it('always yields a non-empty line, whichever variant is drawn', () => {
|
|
7
|
+
for (let i = 0; i < 100; i++) {
|
|
8
|
+
const t = tagline();
|
|
9
|
+
expect(t.length).toBeGreaterThan(0);
|
|
10
|
+
expect(t).not.toContain('undefined');
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('box', () => {
|
|
16
|
+
it('frames lines in a rounded box sized to the widest line', () => {
|
|
17
|
+
expect(box(['hello', 'hi'])).toBe(
|
|
18
|
+
[
|
|
19
|
+
' ╭─────────╮',
|
|
20
|
+
' │ hello │',
|
|
21
|
+
' │ hi │',
|
|
22
|
+
' ╰─────────╯',
|
|
23
|
+
].join('\n'),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('pads on visible width, ignoring ANSI color codes', () => {
|
|
28
|
+
const colored = '\x1b[1mhello\x1b[22m';
|
|
29
|
+
const lines = box([colored, 'hi']).split('\n');
|
|
30
|
+
// Both content rows must end at the same column once colors are stripped.
|
|
31
|
+
const stripped = lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, ''));
|
|
32
|
+
expect(new Set(stripped.map((l) => l.length)).size).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('paints only the border', () => {
|
|
36
|
+
const out = box(['hi'], (s) => `<${s}>`);
|
|
37
|
+
expect(out).toContain('<│> hi <│>');
|
|
38
|
+
expect(out).toContain('<╭──────╮>');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// A small REST demo: a players list + a leaderboard.
|
|
2
|
-
//
|
|
3
|
-
// IMPORTANT - the server runs with a FRESH WebAssembly instance per request, so linear memory
|
|
4
|
-
// (and any module-level state, like the `store` below) is reset on every request. It does NOT
|
|
5
|
-
// persist across requests. We seed a few players at module init so the read routes always have
|
|
6
|
-
// data; the write routes (create / addScore) take effect only for the current request's response.
|
|
7
|
-
// For real persistence, call out to a database or KV store from your handler.
|
|
8
|
-
//
|
|
9
|
-
// `@data` types are the wire model (shared by HTTP and RPC). `@rest` controllers expose HTTP
|
|
10
|
-
// routes; `@service`/`@remote` expose typed RPC. Building the server (toilscript --rpcModule)
|
|
11
|
-
// regenerates the typed client into shared/server.ts:
|
|
12
|
-
// @rest -> Server.REST.<controller>.<route>(args) (a working fetch client)
|
|
13
|
-
// @service -> Server.<service>.<method>() (RPC, transport TODO)
|
|
14
|
-
|
|
15
|
-
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
16
|
-
|
|
17
|
-
/** A leaderboard player. */
|
|
18
|
-
@data
|
|
19
|
-
class Player {
|
|
20
|
-
id: u64 = 0;
|
|
21
|
-
name: string = '';
|
|
22
|
-
score: i64 = 0;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Request body for `POST /players` - the fields a client supplies to create a player. */
|
|
26
|
-
@data
|
|
27
|
-
class NewPlayer {
|
|
28
|
-
name: string = '';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Request body for `POST /players/:id/score` - points to add to a player's score. */
|
|
32
|
-
@data
|
|
33
|
-
class ScoreDelta {
|
|
34
|
-
points: i64 = 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A leaderboard page. A `@data` wrapper so the `Player[]` round-trips through the codec. */
|
|
38
|
-
@data
|
|
39
|
-
class Standings {
|
|
40
|
-
players: Player[] = [];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Re-seeded on EVERY request - module memory does not persist across requests (see the note at
|
|
44
|
-
// the top of the file). Swap this for a database/KV to keep data between requests.
|
|
45
|
-
const store = new Map<u64, Player>();
|
|
46
|
-
let nextId: u64 = 1;
|
|
47
|
-
|
|
48
|
-
function seed(name: string, score: i64): void {
|
|
49
|
-
const p = new Player();
|
|
50
|
-
p.id = nextId++;
|
|
51
|
-
p.name = name;
|
|
52
|
-
p.score = score;
|
|
53
|
-
store.set(p.id, p);
|
|
54
|
-
}
|
|
55
|
-
seed('Ada', 120);
|
|
56
|
-
seed('Linus', 95);
|
|
57
|
-
seed('Grace', 140);
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Players, mounted at `/players`. On the client:
|
|
61
|
-
* await Server.REST.players.get({ params: { id } })
|
|
62
|
-
* await Server.REST.players.create({ body: new NewPlayer('Bob') })
|
|
63
|
-
* await Server.REST.players.addScore({ params: { id }, body: new ScoreDelta(10n) })
|
|
64
|
-
*/
|
|
65
|
-
@rest('players')
|
|
66
|
-
class Players {
|
|
67
|
-
/** `GET /players/:id` - returns a `Response` for full control: a real 404 for a missing id,
|
|
68
|
-
* a custom header, and the `@data` body serialized with `toJSON()`. (The toilscript editor
|
|
69
|
-
* plugin types the compiler-injected `toJSON()`, so this is clean; return the `@data` type
|
|
70
|
-
* directly, like the other routes, when you do not need that control.) */
|
|
71
|
-
@get('/:id')
|
|
72
|
-
public get(ctx: RouteContext): Response {
|
|
73
|
-
const id = u64.parse(ctx.param('id'));
|
|
74
|
-
if (!store.has(id)) return Response.notFound();
|
|
75
|
-
const p = store.get(id);
|
|
76
|
-
|
|
77
|
-
return Response.json(p.toJSON().toString()).setHeader('cache-control', 'no-store');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** `POST /players` - build a player from the request body and return it with a fresh id.
|
|
81
|
-
* Note: it is NOT saved (memory resets next request); persist to a real store to keep it. */
|
|
82
|
-
@post('/')
|
|
83
|
-
public create(input: NewPlayer): Player {
|
|
84
|
-
const p = new Player();
|
|
85
|
-
p.id = nextId++;
|
|
86
|
-
p.name = input.name;
|
|
87
|
-
p.score = 0;
|
|
88
|
-
store.set(p.id, p);
|
|
89
|
-
|
|
90
|
-
return p;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** `POST /players/:id/score` - add `points` (from the body) to the seeded player named by
|
|
94
|
-
* `:id` and return it. The change applies to this response only (memory resets next request). */
|
|
95
|
-
@post('/:id/score')
|
|
96
|
-
public addScore(input: ScoreDelta, ctx: RouteContext): Player {
|
|
97
|
-
const id = u64.parse(ctx.param('id'));
|
|
98
|
-
if (!store.has(id)) return new Player();
|
|
99
|
-
const p = store.get(id);
|
|
100
|
-
p.score += input.points;
|
|
101
|
-
|
|
102
|
-
return p;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The leaderboard, mounted at `/leaderboard`. On the client:
|
|
108
|
-
* const board = await Server.REST.leaderboard.top(); // typed Standings { players: Player[] }
|
|
109
|
-
*/
|
|
110
|
-
@rest('leaderboard')
|
|
111
|
-
class Leaderboard {
|
|
112
|
-
/** `GET /leaderboard` - the seeded players, highest score first. */
|
|
113
|
-
@get('/')
|
|
114
|
-
public top(): Standings {
|
|
115
|
-
const board = new Standings();
|
|
116
|
-
const all = store.values();
|
|
117
|
-
for (let i = 0; i < all.length; i++) board.players.push(all[i]);
|
|
118
|
-
board.players.sort((a: Player, b: Player): i32 => (a.score < b.score ? 1 : a.score > b.score ? -1 : 0));
|
|
119
|
-
return board;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Typed RPC service (transport still a TODO): reached as `Server.stats.playerCount()` on the client. */
|
|
124
|
-
@service
|
|
125
|
-
class Stats {
|
|
126
|
-
/** Number of seeded players (the RPC transport is a TODO, so this throws on the client for now). */
|
|
127
|
-
@remote
|
|
128
|
-
public playerCount(): i32 {
|
|
129
|
-
return store.size;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** A free `@remote` function: `Server.ping(n)` on the client. */
|
|
134
|
-
@remote
|
|
135
|
-
function ping(n: i32): i32 {
|
|
136
|
-
return n + 1;
|
|
137
|
-
}
|