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/web.js CHANGED
@@ -1,8 +1,8 @@
1
- var ASSEMBLYSCRIPT_VERSION = "0.1.57";
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.57/dist/toilscript.js",
5
- "toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.57/dist/cli.js",
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
@@ -8,7 +8,7 @@
8
8
  "toilscript",
9
9
  "wasm"
10
10
  ],
11
- "version": "0.1.57",
11
+ "version": "0.1.59",
12
12
  "author": "Daniel Wirtz <dcode+assemblyscript@dcode.io>",
13
13
  "license": "Apache-2.0",
14
14
  "homepage": "https://github.com/dacely-cloud/toilscript",
@@ -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")
@@ -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 windows: current bucket usage + plan cap (0 = unlimited).
524
- reqMinuteUsed: u64 = 0;
525
- reqMinuteCap: u64 = 0;
526
- reqDayUsed: u64 = 0;
527
- reqDayCap: u64 = 0;
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 < 2) return stats; // pre-v2 edge: refuse rather than mis-decode
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.reqMinuteUsed = r.readU64();
656
- stats.reqMinuteCap = r.readU64();
657
- stats.reqDayUsed = r.readU64();
658
- stats.reqDayCap = r.readU64();
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 < 2) return out;
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 windows (current usage + plan cap; cap 0 = unlimited). Read a
321
- * value by typed getter (`stats.requests`) or by id (`stats.metric(MetricId.Requests)`). */
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
- reqMinuteUsed: u64;
372
- reqMinuteCap: u64;
373
- reqDayUsed: u64;
374
- reqDayCap: u64;
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;