toilscript 0.1.57 → 0.1.59
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/dist/cli.js +80 -12
- package/dist/cli.js.map +2 -2
- package/dist/importmap.json +2 -2
- package/dist/toilscript.js +1 -1
- package/dist/toilscript.js.map +2 -2
- package/dist/web.js +3 -3
- package/package.json +1 -1
- package/std/assembly/bindings/toildb.ts +14 -0
- package/std/assembly/toildb.ts +65 -11
- package/std/assembly/toilscript.d.ts +33 -6
package/dist/web.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var ASSEMBLYSCRIPT_VERSION = "0.1.
|
|
1
|
+
var ASSEMBLYSCRIPT_VERSION = "0.1.59";
|
|
2
2
|
var ASSEMBLYSCRIPT_IMPORTMAP = {
|
|
3
3
|
"imports": {
|
|
4
|
-
"toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.
|
|
5
|
-
"toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.
|
|
4
|
+
"toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.59/dist/toilscript.js",
|
|
5
|
+
"toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.59/dist/cli.js",
|
|
6
6
|
"binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0-nightly.20260609/index.js",
|
|
7
7
|
"long": "https://cdn.jsdelivr.net/npm/long@5.3.2/index.js"
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -71,6 +71,20 @@ export namespace toildbHost {
|
|
|
71
71
|
idemPtr: usize
|
|
72
72
|
): i32;
|
|
73
73
|
|
|
74
|
+
// record.upsert -> 0 created | 1 updated | 2 @unique-conflict (not written) |
|
|
75
|
+
// negative error. A single blind last-writer-wins put: creates the row if
|
|
76
|
+
// absent, overwrites it if present, in ONE op (no create-then-patch round trip).
|
|
77
|
+
// @ts-ignore: decorator
|
|
78
|
+
@external("env", "data.upsert")
|
|
79
|
+
export declare function upsert(
|
|
80
|
+
handle: u32,
|
|
81
|
+
keyPtr: usize,
|
|
82
|
+
keyLen: i32,
|
|
83
|
+
valPtr: usize,
|
|
84
|
+
valLen: i32,
|
|
85
|
+
idemPtr: usize
|
|
86
|
+
): i32;
|
|
87
|
+
|
|
74
88
|
// record.delete -> 0 ok | negative error.
|
|
75
89
|
// @ts-ignore: decorator
|
|
76
90
|
@external("env", "data.delete")
|
package/std/assembly/toildb.ts
CHANGED
|
@@ -75,6 +75,18 @@ function __toildbTakeGrow(): Uint8Array {
|
|
|
75
75
|
/// A mutable keyed-entity collection (spec 7.1). `V` is the `@data` value type,
|
|
76
76
|
/// `K` the `@data` key type.
|
|
77
77
|
@global
|
|
78
|
+
/// The outcome of `Documents.upsert` (spec 8.3). The wire bytes are pinned in the
|
|
79
|
+
/// host ABI (`data.upsert`): a non-negative tag the guest returns as-is.
|
|
80
|
+
export enum UpsertResult {
|
|
81
|
+
/// The row did not exist and was created.
|
|
82
|
+
Created = 0,
|
|
83
|
+
/// The row already existed and was overwritten.
|
|
84
|
+
Updated = 1,
|
|
85
|
+
/// A `@unique` field value is already owned by a DIFFERENT record; nothing was
|
|
86
|
+
/// written (the only outcome an upsert cannot auto-resolve).
|
|
87
|
+
Conflict = 2,
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
export class Documents<K, V> {
|
|
79
91
|
private __handle: u32;
|
|
80
92
|
|
|
@@ -158,6 +170,30 @@ export class Documents<K, V> {
|
|
|
158
170
|
) == 0;
|
|
159
171
|
}
|
|
160
172
|
|
|
173
|
+
/// Create-or-overwrite in ONE op (spec 8.3): a blind last-writer-wins put that
|
|
174
|
+
/// creates the row if absent or overwrites it if present, replacing the two-op
|
|
175
|
+
/// `if (!create(k,v)) patch(k,v)` idiom. Returns `UpsertResult.Created` or
|
|
176
|
+
/// `.Updated`; on a `@unique` collection, `.Conflict` when the value's unique
|
|
177
|
+
/// field is already held by ANOTHER record (nothing is written).
|
|
178
|
+
///
|
|
179
|
+
/// The write is blind: it stores `value` as-is, so concurrent upserts do not
|
|
180
|
+
/// conflict or retry - the later one wins and overwrites the earlier. Correct
|
|
181
|
+
/// for writing a whole value (a profile, a settings blob); wrong for a
|
|
182
|
+
/// read-modify-write of accumulating state (a balance, a like count), where a
|
|
183
|
+
/// concurrent write would be lost - use `counter.add` or version-checked
|
|
184
|
+
/// `patch` there. A lost write is a data-integrity concern, not a boundary one:
|
|
185
|
+
/// the key is tenant-scoped, so a race is only ever one tenant's own concurrent
|
|
186
|
+
/// writes, and `@unique` stays race-safe (claim-once at the value's home).
|
|
187
|
+
upsert(key: K, value: V): UpsertResult {
|
|
188
|
+
const kb = key.encode();
|
|
189
|
+
const vb = value.encode();
|
|
190
|
+
const status = toildbHost.upsert(
|
|
191
|
+
this.__handle, kb.dataStart, kb.byteLength, vb.dataStart, vb.byteLength, 0
|
|
192
|
+
);
|
|
193
|
+
if (status < 0) unreachable();
|
|
194
|
+
return <UpsertResult>status;
|
|
195
|
+
}
|
|
196
|
+
|
|
161
197
|
/// Apply a write through the key's home cell; returns the stored record.
|
|
162
198
|
patch(key: K, value: V): V {
|
|
163
199
|
const kb = key.encode();
|
|
@@ -495,6 +531,14 @@ export enum MetricId {
|
|
|
495
531
|
/// section.
|
|
496
532
|
export const METRIC_COUNTERS: i32 = 42;
|
|
497
533
|
|
|
534
|
+
/// The sustained-rate window (seconds): requests are metered over a 3-minute ROLLING window. A plan's
|
|
535
|
+
/// sustained request rate `X` (rps) permits `X * BURST_WINDOW_SECS` requests across it. Instantaneous
|
|
536
|
+
/// spikes ABOVE the sustained rate are NOT tier-capped (only the box firewall's global pps ceiling, the
|
|
537
|
+
/// DDoS backstop, applies); throttling (HTTP 429) begins only once the window's allowance is spent.
|
|
538
|
+
/// The window slides continuously rather than resetting on a boundary, so a spend does not become free
|
|
539
|
+
/// all at once: it ages out gradually over the following 3 minutes.
|
|
540
|
+
export const BURST_WINDOW_SECS: i32 = 180;
|
|
541
|
+
|
|
498
542
|
/// A dashboard time range for `Analytics.series`. Short ranges (1h/6h) read the per-MINUTE ring; the long
|
|
499
543
|
/// ranges (60d/90d) read the per-DAY ring (90-day retention); the rest read the per-HOUR ring (30-day).
|
|
500
544
|
/// Append-only: never renumber (D60/D90 were added after D30, keeping the wire ids stable).
|
|
@@ -520,11 +564,14 @@ export class TenantStats {
|
|
|
520
564
|
/// Live gauges (current level, not a total).
|
|
521
565
|
connectedStreams: u64 = 0;
|
|
522
566
|
committedMemory: u64 = 0;
|
|
523
|
-
/// Request
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
567
|
+
/// Request meters (fleet-global, across every edge location) against the plan (cap 0 = unmetered).
|
|
568
|
+
/// Burst: requests in the trailing 3-minute ROLLING window vs its allowance
|
|
569
|
+
/// (`sustained_rps * BURST_WINDOW_SECS`). 30d: requests in the current 30-day bucket vs the quota
|
|
570
|
+
/// (the quota still tumbles: it is a billing period, not a rate).
|
|
571
|
+
reqBurstUsed: u64 = 0;
|
|
572
|
+
reqBurstCap: u64 = 0;
|
|
573
|
+
req30dUsed: u64 = 0;
|
|
574
|
+
req30dCap: u64 = 0;
|
|
528
575
|
/// LIVE per-second gauges (rates), computed edge-side from the last completed minute bucket so the
|
|
529
576
|
/// guest is STATELESS (no client diffing of two snapshots). Idle -> 0.0. `f64` (a rate is fractional
|
|
530
577
|
/// and bounded; f64 cannot overflow for any real value).
|
|
@@ -600,6 +647,13 @@ export class TenantStats {
|
|
|
600
647
|
const total = hits + this.cacheMisses;
|
|
601
648
|
return total > 0 ? <f64>hits / <f64>total : 0.0;
|
|
602
649
|
}
|
|
650
|
+
|
|
651
|
+
/// The plan's SUSTAINED request rate (requests/second), derived from the 3-minute burst cap
|
|
652
|
+
/// (`reqBurstCap / BURST_WINDOW_SECS`). 0.0 means unmetered. Spikes above this rate are allowed;
|
|
653
|
+
/// throttling only starts once `reqBurstUsed` passes `reqBurstCap` in the current bucket.
|
|
654
|
+
get sustainedRpsCap(): f64 {
|
|
655
|
+
return this.reqBurstCap == 0 ? 0.0 : <f64>this.reqBurstCap / <f64>BURST_WINDOW_SECS;
|
|
656
|
+
}
|
|
603
657
|
}
|
|
604
658
|
|
|
605
659
|
/// One metric's time series for a range: `points` oldest→newest (per-bucket totals, `u64` — non-negative),
|
|
@@ -642,7 +696,7 @@ export class Analytics {
|
|
|
642
696
|
const r = new DataReader(buf);
|
|
643
697
|
const stats = new TenantStats();
|
|
644
698
|
const version = r.readU16();
|
|
645
|
-
if (version <
|
|
699
|
+
if (version < 3) return stats; // pre-v3 edge: refuse rather than mis-decode (clean break, no v2)
|
|
646
700
|
stats.nowMs = r.readU64();
|
|
647
701
|
const count = <i32>r.readU32();
|
|
648
702
|
for (let i: i32 = 0; i < count && r.ok; i++) {
|
|
@@ -652,10 +706,10 @@ export class Analytics {
|
|
|
652
706
|
}
|
|
653
707
|
stats.connectedStreams = r.readU64();
|
|
654
708
|
stats.committedMemory = r.readU64();
|
|
655
|
-
stats.
|
|
656
|
-
stats.
|
|
657
|
-
stats.
|
|
658
|
-
stats.
|
|
709
|
+
stats.reqBurstUsed = r.readU64();
|
|
710
|
+
stats.reqBurstCap = r.readU64();
|
|
711
|
+
stats.req30dUsed = r.readU64();
|
|
712
|
+
stats.req30dCap = r.readU64();
|
|
659
713
|
// LIVE per-second rates (f64), appended after the windows. VERSION-GATE by LENGTH: a pre-rates
|
|
660
714
|
// (shorter) frame from an older edge exhausts the buffer here, so `readF64` returns 0.0 for each
|
|
661
715
|
// missing rate (DataReader is bounds-checked). No frame-version bump needed.
|
|
@@ -673,7 +727,7 @@ export class Analytics {
|
|
|
673
727
|
const r = new DataReader(buf);
|
|
674
728
|
const out = new Series();
|
|
675
729
|
const version = r.readU16();
|
|
676
|
-
if (version <
|
|
730
|
+
if (version < 3) return out; // pre-v3 edge: refuse rather than mis-decode (clean break, no v2)
|
|
677
731
|
out.metric = <MetricId>r.readU16();
|
|
678
732
|
out.bucketSecs = r.readU32();
|
|
679
733
|
out.headMs = r.readU64();
|
|
@@ -229,6 +229,16 @@ declare function admin(target: Object, propertyKey: string | symbol, descriptor:
|
|
|
229
229
|
// User>` and `App.users.get(...)`. Every handle is KEY-FIRST (`<K, V>`, matching
|
|
230
230
|
// `Map`/`Record`). `K`/`V`/`M` are `@data` types (the codec the host marshals).
|
|
231
231
|
|
|
232
|
+
/** The outcome of `Documents.upsert` (spec 8.3). */
|
|
233
|
+
declare const enum UpsertResult {
|
|
234
|
+
/** The row did not exist and was created. */
|
|
235
|
+
Created = 0,
|
|
236
|
+
/** The row already existed and was overwritten. */
|
|
237
|
+
Updated = 1,
|
|
238
|
+
/** A `@unique` field value is already owned by another record; nothing written. */
|
|
239
|
+
Conflict = 2,
|
|
240
|
+
}
|
|
241
|
+
|
|
232
242
|
/** A mutable keyed-entity collection (spec 7.1): user profiles, items, sessions. */
|
|
233
243
|
declare class Documents<K, V> {
|
|
234
244
|
get(key: K): V | null;
|
|
@@ -237,6 +247,13 @@ declare class Documents<K, V> {
|
|
|
237
247
|
getMany(keys: K[]): (V | null)[];
|
|
238
248
|
create(key: K, value: V): bool;
|
|
239
249
|
patch(key: K, value: V): V;
|
|
250
|
+
/** Create-or-overwrite in one op (blind last-writer-wins); replaces the two-op
|
|
251
|
+
* `if (!create(k,v)) patch(k,v)` idiom. Right for writing a whole value; wrong
|
|
252
|
+
* for read-modify-write of accumulating state (a balance/count), where a
|
|
253
|
+
* concurrent write would be lost - use `counter.add` or version-checked
|
|
254
|
+
* `patch` there. The key is tenant-scoped, so a race is only ever the same
|
|
255
|
+
* tenant's own writes. See {@link UpsertResult}. */
|
|
256
|
+
upsert(key: K, value: V): UpsertResult;
|
|
240
257
|
delete(key: K): void;
|
|
241
258
|
getDelete(key: K): V | null;
|
|
242
259
|
}
|
|
@@ -316,9 +333,14 @@ declare const enum AnalyticsRange {
|
|
|
316
333
|
H1 = 0, H6 = 1, H12 = 2, H24 = 3, D3 = 4, D7 = 5, D14 = 6, D30 = 7, D60 = 8, D90 = 9,
|
|
317
334
|
}
|
|
318
335
|
|
|
336
|
+
/** The sustained-rate window (seconds): requests are metered over a 3-minute ROLLING window, so a plan's
|
|
337
|
+
* sustained rate `X` (rps) permits `X * BURST_WINDOW_SECS` requests across it. */
|
|
338
|
+
declare const BURST_WINDOW_SECS: i32;
|
|
339
|
+
|
|
319
340
|
/** One tenant's analytics snapshot: lifetime totals (indexed by `MetricId`, no string keys), the two
|
|
320
|
-
* live gauge levels, and the request
|
|
321
|
-
* value by typed getter (`stats.requests`) or by id
|
|
341
|
+
* live gauge levels, and the request meters (burst 3-min bucket + 30-day quota, each with its cap;
|
|
342
|
+
* cap 0 = unmetered). Read a value by typed getter (`stats.requests`) or by id
|
|
343
|
+
* (`stats.metric(MetricId.Requests)`). */
|
|
322
344
|
declare class TenantStats {
|
|
323
345
|
metric(id: MetricId): u64;
|
|
324
346
|
readonly requests: u64;
|
|
@@ -368,10 +390,15 @@ declare class TenantStats {
|
|
|
368
390
|
readonly cacheRatio: f64;
|
|
369
391
|
connectedStreams: u64;
|
|
370
392
|
committedMemory: u64;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
393
|
+
/** Fleet-global requests in the trailing 3-minute ROLLING window, and its allowance
|
|
394
|
+
* (`sustained_rps * BURST_WINDOW_SECS`; 0 = unmetered). Spikes are NOT tier-capped. */
|
|
395
|
+
reqBurstUsed: u64;
|
|
396
|
+
reqBurstCap: u64;
|
|
397
|
+
/** Fleet-global requests in the current 30-day tumbling bucket, and the 30-day quota (0 = unmetered). */
|
|
398
|
+
req30dUsed: u64;
|
|
399
|
+
req30dCap: u64;
|
|
400
|
+
/** Derived: the plan's SUSTAINED request rate (`reqBurstCap / BURST_WINDOW_SECS`), 0.0 when unmetered. */
|
|
401
|
+
readonly sustainedRpsCap: f64;
|
|
375
402
|
/** LIVE per-second gauges (rates), computed edge-side from the last completed minute bucket (idle 0.0),
|
|
376
403
|
* so no client diffing is needed. `f64` cannot overflow for any real value. */
|
|
377
404
|
readonly rps: f64;
|