@johannc-dev/zyrk 0.2.0 → 0.2.1
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/README.md +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -4
- package/dist/client.d.ts +6 -2
- package/dist/client.js +510 -17
- package/dist/errors.d.ts +9 -1
- package/dist/errors.js +6 -1
- package/dist/export.d.ts +16 -7
- package/dist/export.js +102 -18
- package/dist/hourfile.d.ts +3 -1
- package/dist/hourfile.js +7 -0
- package/dist/limits.d.ts +1 -1
- package/dist/local-data.d.ts +13 -4
- package/dist/local-data.js +29 -1
- package/dist/local-replay.d.ts +5 -5
- package/dist/local-replay.js +4 -3
- package/dist/local-storage.d.ts +2 -2
- package/dist/local.d.ts +2 -2
- package/dist/local.js +46 -32
- package/dist/sandbox-bootstrap.js +171 -9
- package/dist/sdk.d.ts +51 -28
- package/dist/sdk.js +2 -0
- package/dist/stream.d.ts +52 -4
- package/dist/stream.js +111 -13
- package/package.json +3 -3
|
@@ -299,25 +299,25 @@
|
|
|
299
299
|
const summary = (r) => freeze({ key: r.key, size: r.size, contentType: r.contentType, lastModified: r.lastModified, expiresAt: r.expiresAt });
|
|
300
300
|
const storageArea = (area) => {
|
|
301
301
|
const read = async (key, offset, length) => {
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
const end = Math.min(
|
|
305
|
-
const total = Math.max(0, end -
|
|
302
|
+
const first2 = await call({ StorageGet: { area, key, offset, length: length > CHUNK ? CHUNK : length } });
|
|
303
|
+
if (first2 === null) return null;
|
|
304
|
+
const end = Math.min(first2.size, offset + length);
|
|
305
|
+
const total = Math.max(0, end - first2.offset);
|
|
306
306
|
const out = new U8(total);
|
|
307
307
|
let at = 0;
|
|
308
|
-
let r =
|
|
308
|
+
let r = first2;
|
|
309
309
|
for (; ; ) {
|
|
310
310
|
const n = N.b64decodeInto(r.data, out, at);
|
|
311
311
|
if (n < 0) throw fail("unavailable", "Corrupt storage chunk");
|
|
312
312
|
at += n;
|
|
313
313
|
if (at >= total || n === 0) break;
|
|
314
314
|
r = await call({
|
|
315
|
-
StorageGet: { area, key, offset:
|
|
315
|
+
StorageGet: { area, key, offset: first2.offset + at, length: Math.min(CHUNK, total - at) }
|
|
316
316
|
});
|
|
317
|
-
if (r === null || r.etag !==
|
|
317
|
+
if (r === null || r.etag !== first2.etag)
|
|
318
318
|
throw fail("value_changed", "The object changed while it was read: read it again");
|
|
319
319
|
}
|
|
320
|
-
return { first, body: at === total ? out : out.subarray(0, at) };
|
|
320
|
+
return { first: first2, body: at === total ? out : out.subarray(0, at) };
|
|
321
321
|
};
|
|
322
322
|
const get = async (key, range) => {
|
|
323
323
|
keyOf(key);
|
|
@@ -449,7 +449,7 @@
|
|
|
449
449
|
version: execution.version,
|
|
450
450
|
attempt: execution.attempt
|
|
451
451
|
}),
|
|
452
|
-
data: freeze({
|
|
452
|
+
data: batching ? batchedData() : freeze({
|
|
453
453
|
walletTrades: trades("wallet"),
|
|
454
454
|
tokenTrades: trades("token"),
|
|
455
455
|
tokenTotals: async (mint, o) => {
|
|
@@ -515,5 +515,167 @@
|
|
|
515
515
|
return text === void 0 ? "null" : text;
|
|
516
516
|
})();
|
|
517
517
|
};
|
|
518
|
+
const batching = N.batchReads === true;
|
|
519
|
+
const BATCH = 64;
|
|
520
|
+
const MAX_REENTRY = 1e3;
|
|
521
|
+
const reflectApply = Reflect.apply;
|
|
522
|
+
const promiseThen = PromiseCtor.prototype.then;
|
|
523
|
+
const ready = PromiseCtor.resolve();
|
|
524
|
+
Object.defineProperty(ready, "constructor", { value: void 0 });
|
|
525
|
+
let open = false;
|
|
526
|
+
let direct = false;
|
|
527
|
+
let reentry = 0;
|
|
528
|
+
let first = null;
|
|
529
|
+
let last = null;
|
|
530
|
+
let epoch = 0;
|
|
531
|
+
const wait = (run, hint) => new PromiseCtor((resolve, reject) => {
|
|
532
|
+
const q = { run, hint, next: null, resolve, reject };
|
|
533
|
+
if (last === null) first = q;
|
|
534
|
+
else last.next = q;
|
|
535
|
+
last = q;
|
|
536
|
+
});
|
|
537
|
+
const checkpoint = () => {
|
|
538
|
+
let q = first;
|
|
539
|
+
first = null;
|
|
540
|
+
last = null;
|
|
541
|
+
direct = reentry >= MAX_REENTRY;
|
|
542
|
+
while (q !== null) {
|
|
543
|
+
epoch++;
|
|
544
|
+
let n = 0;
|
|
545
|
+
let hinted = 0;
|
|
546
|
+
for (let r = q; r !== null && n < BATCH; r = r.next) {
|
|
547
|
+
n++;
|
|
548
|
+
if (r.hint !== null && r.hint(epoch)) hinted++;
|
|
549
|
+
}
|
|
550
|
+
if (hinted > 0) N.hint();
|
|
551
|
+
for (; n > 0; n--) {
|
|
552
|
+
const c = q;
|
|
553
|
+
q = c.next;
|
|
554
|
+
let value;
|
|
555
|
+
try {
|
|
556
|
+
value = c.run();
|
|
557
|
+
} catch (e) {
|
|
558
|
+
c.reject(e);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
c.resolve(value);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
direct = false;
|
|
565
|
+
if (first === null) {
|
|
566
|
+
reentry = 0;
|
|
567
|
+
open = false;
|
|
568
|
+
} else {
|
|
569
|
+
reentry++;
|
|
570
|
+
reflectApply(promiseThen, ready, [checkpoint]);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const openBatch = () => {
|
|
574
|
+
open = true;
|
|
575
|
+
reflectApply(promiseThen, ready, [checkpoint]);
|
|
576
|
+
};
|
|
577
|
+
const waiting = () => open && !direct;
|
|
578
|
+
const tradesBatched = (kind) => (key, w) => {
|
|
579
|
+
if (typeof key !== "string") throw fail("invalid_argument", "Expected an address string");
|
|
580
|
+
const { since, until, side, allowPartial, withSignatures, withMarket, withEvents, withOrder, withTx } = checkWindow(w);
|
|
581
|
+
return freeze({
|
|
582
|
+
[asyncIterator]() {
|
|
583
|
+
let after = null;
|
|
584
|
+
let done = false;
|
|
585
|
+
let closed = false;
|
|
586
|
+
let mark = 0;
|
|
587
|
+
const end = () => ({ done: true, value: void 0 });
|
|
588
|
+
const read = () => {
|
|
589
|
+
if (done) return end();
|
|
590
|
+
const batch = N.read(kind, key, since, until, side, allowPartial, withSignatures, after, withMarket, withEvents, withOrder, withTx);
|
|
591
|
+
if (batch === null) {
|
|
592
|
+
done = true;
|
|
593
|
+
return end();
|
|
594
|
+
}
|
|
595
|
+
after = batch.hour;
|
|
596
|
+
return { done: false, value: batch };
|
|
597
|
+
};
|
|
598
|
+
const hint = (e) => {
|
|
599
|
+
if (done || mark === e) return false;
|
|
600
|
+
mark = e;
|
|
601
|
+
N.hint(kind, key, since, until, side, allowPartial, withSignatures, after, withMarket, withEvents, withOrder, withTx);
|
|
602
|
+
return true;
|
|
603
|
+
};
|
|
604
|
+
const nextNow = async () => {
|
|
605
|
+
if (done || closed) return end();
|
|
606
|
+
if (!direct) openBatch();
|
|
607
|
+
return read();
|
|
608
|
+
};
|
|
609
|
+
const returnNow = async (value) => {
|
|
610
|
+
closed = true;
|
|
611
|
+
return { done: true, value };
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
next() {
|
|
615
|
+
if (!waiting()) return nextNow();
|
|
616
|
+
if (closed) return wait(end, null);
|
|
617
|
+
return wait(read, hint);
|
|
618
|
+
},
|
|
619
|
+
return(value) {
|
|
620
|
+
if (!waiting()) return returnNow(value);
|
|
621
|
+
closed = true;
|
|
622
|
+
return wait(() => ({ done: true, value }), null);
|
|
623
|
+
},
|
|
624
|
+
[asyncIterator]() {
|
|
625
|
+
return this;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
};
|
|
631
|
+
const totalsNow = async (mint, o) => {
|
|
632
|
+
if (typeof mint !== "string") throw fail("invalid_argument", "Expected a mint address");
|
|
633
|
+
const rawQuote = o ? o.quote : void 0;
|
|
634
|
+
const quote = rawQuote !== void 0 ? StringCtor(rawQuote) : null;
|
|
635
|
+
return N.totals(mint, quote);
|
|
636
|
+
};
|
|
637
|
+
const infoNow = async (mint) => {
|
|
638
|
+
if (typeof mint !== "string") throw fail("invalid_argument", "Expected a mint address");
|
|
639
|
+
return N.info(mint);
|
|
640
|
+
};
|
|
641
|
+
const batchedData = () => freeze({
|
|
642
|
+
walletTrades: tradesBatched("wallet"),
|
|
643
|
+
tokenTrades: tradesBatched("token"),
|
|
644
|
+
tokenTotals: (mint, o) => {
|
|
645
|
+
if (!waiting()) return totalsNow(mint, o);
|
|
646
|
+
let quote;
|
|
647
|
+
try {
|
|
648
|
+
if (typeof mint !== "string") throw fail("invalid_argument", "Expected a mint address");
|
|
649
|
+
const rawQuote = o ? o.quote : void 0;
|
|
650
|
+
quote = rawQuote !== void 0 ? StringCtor(rawQuote) : null;
|
|
651
|
+
} catch (e) {
|
|
652
|
+
return PromiseCtor.reject(e);
|
|
653
|
+
}
|
|
654
|
+
return wait(() => N.totals(mint, quote), null);
|
|
655
|
+
},
|
|
656
|
+
tokenInfo: (mint) => {
|
|
657
|
+
if (!waiting()) return infoNow(mint);
|
|
658
|
+
if (typeof mint !== "string") return PromiseCtor.reject(fail("invalid_argument", "Expected a mint address"));
|
|
659
|
+
return wait(() => N.info(mint), null);
|
|
660
|
+
},
|
|
661
|
+
volumeSearch: async function* (params) {
|
|
662
|
+
let p = { ...params };
|
|
663
|
+
for (; ; ) {
|
|
664
|
+
let page;
|
|
665
|
+
if (waiting()) {
|
|
666
|
+
const text = flatParams(p);
|
|
667
|
+
page = await wait(() => N.volume(text), null);
|
|
668
|
+
} else page = N.volume(flatParams(p));
|
|
669
|
+
yield page;
|
|
670
|
+
const next = page === null || typeof page !== "object" ? null : page.nextCursor;
|
|
671
|
+
if (next === null || next === void 0) return;
|
|
672
|
+
if (typeof next !== "string" || next === p.cursor)
|
|
673
|
+
throw fail("history_unavailable", "Invalid next cursor");
|
|
674
|
+
const w = page.window;
|
|
675
|
+
if (w && typeof w === "object" && "since" in w) p = { ...p, since: w.since, until: w.until };
|
|
676
|
+
p = { ...p, cursor: next };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
});
|
|
518
680
|
return freeze({ start, describe });
|
|
519
681
|
});
|
package/dist/sdk.d.ts
CHANGED
|
@@ -278,7 +278,12 @@ export declare const MarketEvent: {
|
|
|
278
278
|
export declare const MarketFlag: {
|
|
279
279
|
/** `tokenReserve` and `quoteReserve` are set. */
|
|
280
280
|
readonly reserves: 1;
|
|
281
|
-
/**
|
|
281
|
+
/**
|
|
282
|
+
* `virtualTokenReserve` and `virtualQuoteReserve` are both set (always
|
|
283
|
+
* with {@link MarketFlag.virtualTokenReserve} and
|
|
284
|
+
* {@link MarketFlag.virtualQuoteReserve}). PumpSwap trades usually carry
|
|
285
|
+
* one side only: test the side you need instead.
|
|
286
|
+
*/
|
|
282
287
|
readonly virtualReserves: 2;
|
|
283
288
|
/**
|
|
284
289
|
* The provider row holds several operations on this pool (e.g. a buy and
|
|
@@ -315,6 +320,14 @@ export declare const MarketFlag: {
|
|
|
315
320
|
* trader receives 0.30 % less than `tokenRaw`. `chainBreak` accounts for it.
|
|
316
321
|
*/
|
|
317
322
|
readonly invertedPool: 512;
|
|
323
|
+
/** `virtualTokenReserve` is set. */
|
|
324
|
+
readonly virtualTokenReserve: 1024;
|
|
325
|
+
/**
|
|
326
|
+
* `virtualQuoteReserve` is set. On PumpSwap (pools created by pump.fun)
|
|
327
|
+
* it is added to the real reserve: the effective quote reserve is
|
|
328
|
+
* `quoteReserve + virtualQuoteReserve`.
|
|
329
|
+
*/
|
|
330
|
+
readonly virtualQuoteReserve: 2048;
|
|
318
331
|
};
|
|
319
332
|
/**
|
|
320
333
|
* Pool state and fees of each trade (`withMarket: true`), entry `i` for
|
|
@@ -328,9 +341,12 @@ export interface MarketColumns {
|
|
|
328
341
|
readonly tokenReserve: BigUint64Array;
|
|
329
342
|
readonly quoteReserve: BigUint64Array;
|
|
330
343
|
/**
|
|
331
|
-
* Pumpfun: the virtual reserves of the bonding curve, the ones its
|
|
332
|
-
* constant product uses. Pumpswap:
|
|
333
|
-
*
|
|
344
|
+
* Pumpfun: the total virtual reserves of the bonding curve, the ones its
|
|
345
|
+
* constant product uses. Pumpswap: a virtual amount added to the real
|
|
346
|
+
* reserve (effective quote reserve = `quoteReserve + virtualQuoteReserve`),
|
|
347
|
+
* usually on one side only. Each side is set when its flag is
|
|
348
|
+
* ({@link MarketFlag.virtualTokenReserve},
|
|
349
|
+
* {@link MarketFlag.virtualQuoteReserve}), `0` otherwise.
|
|
334
350
|
*/
|
|
335
351
|
readonly virtualTokenReserve: BigUint64Array;
|
|
336
352
|
readonly virtualQuoteReserve: BigUint64Array;
|
|
@@ -352,11 +368,11 @@ export interface MarketColumns {
|
|
|
352
368
|
readonly event: Uint8Array;
|
|
353
369
|
}
|
|
354
370
|
/**
|
|
355
|
-
* Latest known token metadata (
|
|
371
|
+
* Latest known token metadata (`ctx.data.tokenInfo`, also `metadata` of
|
|
356
372
|
* `tokenTotals`): provider values copied verbatim from the newest row of the
|
|
357
373
|
* mint that is cheap to reach, `null` when the row lacks them. `name`,
|
|
358
|
-
* `symbol` and `uri` of
|
|
359
|
-
*
|
|
374
|
+
* `symbol` and `uri` of sealed hours come from the token index of the newest
|
|
375
|
+
* sealed hour that has them, cut at 128, 64 and 512 UTF-8 bytes.
|
|
360
376
|
*/
|
|
361
377
|
export type TokenMetadata = {
|
|
362
378
|
readonly name: string | null;
|
|
@@ -393,8 +409,8 @@ export type TokenInfo = {
|
|
|
393
409
|
readonly metadata: TokenMetadata | null;
|
|
394
410
|
};
|
|
395
411
|
/**
|
|
396
|
-
* Parameters of
|
|
397
|
-
*
|
|
412
|
+
* Parameters of `ctx.data.volumeSearch` (`since`/`until` in **seconds**,
|
|
413
|
+
* multiples of 60, `[since, until)`, at most 31 days). Paging is handled by
|
|
398
414
|
* `ctx.data.volumeSearch`: there is no `cursor`.
|
|
399
415
|
*/
|
|
400
416
|
export interface VolumeSearchParams {
|
|
@@ -501,9 +517,9 @@ export interface Subscription {
|
|
|
501
517
|
readonly maxReplayEvents?: number;
|
|
502
518
|
}
|
|
503
519
|
/**
|
|
504
|
-
* A trade as published by the live feed
|
|
505
|
-
*
|
|
506
|
-
* typed; the others are kept as they come.
|
|
520
|
+
* A trade as published by the live feed (the `operation` of a `trade` or
|
|
521
|
+
* `volume` event), with `id` the trade id `"hour:line:op"`. Only the fields
|
|
522
|
+
* most programs read are typed; the others are kept as they come.
|
|
507
523
|
*/
|
|
508
524
|
export interface MarketOperation {
|
|
509
525
|
readonly id: string;
|
|
@@ -795,7 +811,7 @@ export interface ExecutionInfo {
|
|
|
795
811
|
/** 1 for the first attempt. */
|
|
796
812
|
readonly attempt: number;
|
|
797
813
|
}
|
|
798
|
-
/**
|
|
814
|
+
/** Options of `put` (every field optional). */
|
|
799
815
|
export interface StoragePutOptions {
|
|
800
816
|
/** `type/subtype`; parameters are dropped. Default: `text/plain` for a string, else `application/octet-stream`. */
|
|
801
817
|
readonly contentType?: string;
|
|
@@ -809,7 +825,7 @@ export interface StoragePutOptions {
|
|
|
809
825
|
*/
|
|
810
826
|
readonly ifMatch?: string;
|
|
811
827
|
}
|
|
812
|
-
/**
|
|
828
|
+
/** The head of a stored object, with where it stands in time. */
|
|
813
829
|
export interface StorageObjectHead {
|
|
814
830
|
readonly key: string;
|
|
815
831
|
readonly size: number;
|
|
@@ -824,11 +840,11 @@ export interface StorageObjectHead {
|
|
|
824
840
|
/** Changes at every write (HTTP `ETag`). */
|
|
825
841
|
readonly etag: string;
|
|
826
842
|
}
|
|
827
|
-
/**
|
|
843
|
+
/** What `get` answers. With a range, `body` is that range and `size` the whole object's. */
|
|
828
844
|
export interface StorageReadResult extends StorageObjectHead {
|
|
829
845
|
readonly body: Uint8Array;
|
|
830
846
|
}
|
|
831
|
-
/**
|
|
847
|
+
/** One object of a `list` answer, with its expiry. */
|
|
832
848
|
export interface StorageObjectSummary {
|
|
833
849
|
readonly key: string;
|
|
834
850
|
readonly size: number;
|
|
@@ -837,14 +853,14 @@ export interface StorageObjectSummary {
|
|
|
837
853
|
readonly expiresAt: number | null;
|
|
838
854
|
}
|
|
839
855
|
/**
|
|
840
|
-
* What `list` answers:
|
|
856
|
+
* What `list` answers: an array of summaries, plus `cursor` (pass it
|
|
841
857
|
* as `after` for the next page, `null` at the end) and `truncated`.
|
|
842
858
|
*/
|
|
843
859
|
export type StorageListPage = StorageObjectSummary[] & {
|
|
844
860
|
readonly cursor: string | null;
|
|
845
861
|
readonly truncated: boolean;
|
|
846
862
|
};
|
|
847
|
-
/**
|
|
863
|
+
/** Usage of one storage area of the account, with its limits. */
|
|
848
864
|
export interface StorageUsage {
|
|
849
865
|
readonly area: "short" | "long" | "durable";
|
|
850
866
|
readonly initialized: boolean;
|
|
@@ -886,9 +902,9 @@ export interface StorageWriter {
|
|
|
886
902
|
* execution fails afterwards.
|
|
887
903
|
*/
|
|
888
904
|
export interface StorageArea {
|
|
889
|
-
/**
|
|
905
|
+
/** Writes an object. Answers the new head. */
|
|
890
906
|
put(key: string, body: StorageBody, options?: StoragePutOptions): Promise<StorageObjectHead>;
|
|
891
|
-
/**
|
|
907
|
+
/** Reads an object; `range` reads part of a large object. `null` when absent or expired. */
|
|
892
908
|
get(key: string, range?: {
|
|
893
909
|
offset?: number;
|
|
894
910
|
length?: number;
|
|
@@ -899,7 +915,7 @@ export interface StorageArea {
|
|
|
899
915
|
ifMatch?: string;
|
|
900
916
|
}): Promise<void>;
|
|
901
917
|
/**
|
|
902
|
-
*
|
|
918
|
+
* Lists keys starting with `prefix`, in byte order, at most
|
|
903
919
|
* `maxObjects` (1000). For the next page, pass the last key as `after`.
|
|
904
920
|
*/
|
|
905
921
|
list(prefix?: string, options?: {
|
|
@@ -924,16 +940,23 @@ export interface Context {
|
|
|
924
940
|
walletTrades(wallet: Address, query: TradeQuery): AsyncIterable<HourBatch>;
|
|
925
941
|
/** Trades of a token, one hour per batch, oldest first. */
|
|
926
942
|
tokenTrades(mint: Address, query: TradeQuery): AsyncIterable<HourBatch>;
|
|
927
|
-
/**
|
|
943
|
+
/**
|
|
944
|
+
* Volume of a token since its creation (`cumulativeRaw`, `buyRaw`,
|
|
945
|
+
* `sellRaw`, `buys`, `sells`, `sinceCreation.complete`, `coverage`), its
|
|
946
|
+
* `creation` and `metadata`, as JSON.
|
|
947
|
+
*/
|
|
928
948
|
tokenTotals(mint: Address, options?: {
|
|
929
949
|
quote?: Address;
|
|
930
950
|
}): Promise<Json>;
|
|
931
951
|
/**
|
|
932
|
-
*
|
|
933
|
-
*
|
|
952
|
+
* Latest known metadata and the creation, at most two blocks read (no
|
|
953
|
+
* volumes).
|
|
934
954
|
*/
|
|
935
955
|
tokenInfo(mint: Address): Promise<TokenInfo>;
|
|
936
|
-
/**
|
|
956
|
+
/**
|
|
957
|
+
* Tokens ranked by volume over a window, page by page (`items` of
|
|
958
|
+
* `{mint, volume, buyVolume, sellVolume, buys, sells, volumeRaw}`).
|
|
959
|
+
*/
|
|
937
960
|
volumeSearch(params: VolumeSearchParams): AsyncIterable<Json>;
|
|
938
961
|
};
|
|
939
962
|
/** Private JSON state of the program, kept between executions. */
|
|
@@ -1047,7 +1070,7 @@ export declare function parseTradeId(id: string): TradeKey;
|
|
|
1047
1070
|
export declare function compareTradeIds(a: string, b: string): number;
|
|
1048
1071
|
/**
|
|
1049
1072
|
* Id of a live-feed operation, in the {@link tradeId} format (`operation.id`
|
|
1050
|
-
* as delivered
|
|
1073
|
+
* as delivered to subscriptions, else `order.ord`); `null` without order.
|
|
1051
1074
|
*/
|
|
1052
1075
|
export declare function operationTradeId(operation: MarketOperation): string | null;
|
|
1053
1076
|
/**
|
|
@@ -1115,9 +1138,9 @@ export declare function tradeAt(batch: HourBatch, i: number): Trade;
|
|
|
1115
1138
|
* `error.code`; `message` is for humans and may change.
|
|
1116
1139
|
*/
|
|
1117
1140
|
export type SdkErrorCode = "invalid_window" | "partial_coverage" | "history_preparing" | "storage_busy" | "history_unavailable" | "read_limit" | "read_time_limit" | "invalid_argument" | "unknown_function" | "execution_not_found" | "state_value_too_large" | "state_limit" | "children_limit" | "subscription_limit" | "price_subscription_limit" | "output_limit" | StorageErrorCode | (string & {});
|
|
1118
|
-
/**
|
|
1141
|
+
/** Error codes of user storage (`ctx.storage` and the HTTP storage routes). */
|
|
1119
1142
|
export type StorageErrorCode = "quota_exceeded" | "too_large" | "not_found" | "invalid_request" | "forbidden" | "unavailable" | "blocked_type" | "already_exists" | "value_changed" | "io_limit" | "upload_limit" | "precondition_failed";
|
|
1120
|
-
/** HTTP status of each storage code
|
|
1143
|
+
/** HTTP status of each storage code. */
|
|
1121
1144
|
export declare const STORAGE_STATUS_BY_CODE: Readonly<Record<StorageErrorCode, number>>;
|
|
1122
1145
|
/** Error thrown by a `ctx` call. `retryAfterMs` comes with `storage_busy`/`history_preparing`. */
|
|
1123
1146
|
export interface SdkError extends Error {
|
package/dist/sdk.js
CHANGED
|
@@ -47,6 +47,8 @@ export const MarketFlag = {
|
|
|
47
47
|
mayhem: 128,
|
|
48
48
|
feeWithoutToken: 256,
|
|
49
49
|
invertedPool: 512,
|
|
50
|
+
virtualTokenReserve: 1024,
|
|
51
|
+
virtualQuoteReserve: 2048,
|
|
50
52
|
};
|
|
51
53
|
export function isFinal(result) {
|
|
52
54
|
return result.status !== "queued" && result.status !== "running";
|
package/dist/stream.d.ts
CHANGED
|
@@ -15,18 +15,66 @@ export interface StreamOptions {
|
|
|
15
15
|
readonly apiKey: string;
|
|
16
16
|
/** Resume after this sequence number. */
|
|
17
17
|
readonly afterSeq?: number;
|
|
18
|
+
/**
|
|
19
|
+
* How long the server must keep refusing the key, with no successful join
|
|
20
|
+
* in between, before the stream ends with `invalid_key` (or `unauthorized`).
|
|
21
|
+
* Default 360 000 ms (6 min). See {@link executionEvents}: below the
|
|
22
|
+
* server's longest key sync delay, a key created just before the stream
|
|
23
|
+
* started could end it.
|
|
24
|
+
*/
|
|
25
|
+
readonly keyRefusalWindowMs?: number;
|
|
18
26
|
}
|
|
19
27
|
/**
|
|
20
|
-
* Live execution events of the account. Gaps are emitted as values
|
|
21
|
-
*
|
|
22
|
-
* The
|
|
28
|
+
* Live execution events of the account. Gaps are emitted as values.
|
|
29
|
+
*
|
|
30
|
+
* The stream reconnects by itself after a dropped or closed connection (no
|
|
31
|
+
* network, a server restart or deploy, a connection the server closed) and
|
|
32
|
+
* resumes after the last event. It fails, once, with:
|
|
33
|
+
* - a {@link DecodeError} on an undecodable payload;
|
|
34
|
+
* - a {@link NetworkError} when the server refuses the join, with the
|
|
35
|
+
* server's `reason` (`functions_disabled`, `functions_unavailable`);
|
|
36
|
+
* - a {@link NetworkError} whose `reason` is `functions_disabled` (Functions
|
|
37
|
+
* access removed) or `channel_closed` (the server closed the channel:
|
|
38
|
+
* subscribe again with `afterSeq`);
|
|
39
|
+
* - a {@link NetworkError} whose `reason` is `invalid_key` (the key was
|
|
40
|
+
* revoked, replaced or is unknown, or its account is not active) or
|
|
41
|
+
* `unauthorized` (the same, seen at the join): the server kept refusing
|
|
42
|
+
* the key for {@link StreamOptions.keyRefusalWindowMs} (default 6 min)
|
|
43
|
+
* with no successful join in between. A single refusal never ends the
|
|
44
|
+
* stream: a key created or changed less than a key sync ago, or a server
|
|
45
|
+
* whose key data is briefly behind, is refused too; the stream retries
|
|
46
|
+
* (every 5 s at most) and goes on as soon as a join succeeds. A refusal
|
|
47
|
+
* that the server does not name (a server starting, a network failure,
|
|
48
|
+
* the connection limit) is retried with no time limit;
|
|
49
|
+
* - a {@link NetworkError} whose `reason` is `account_changed`: a
|
|
50
|
+
* reconnection joined another account than the first join (the key was
|
|
51
|
+
* moved to another account). No event of the other account is delivered.
|
|
52
|
+
* This check needs a server that names the account in its join reply: a
|
|
53
|
+
* join answered without `account` (an older server, e.g. during a rollout
|
|
54
|
+
* or rollback) is accepted unchecked.
|
|
55
|
+
*
|
|
56
|
+
* A transient `status` `{available: false}` does not end it. An older
|
|
57
|
+
* server that does not say why it refused a connection gives no
|
|
58
|
+
* `invalid_key`: the stream then keeps reconnecting, as before. The socket
|
|
59
|
+
* is closed and no reconnection happens once the stream is interrupted or
|
|
60
|
+
* has failed.
|
|
23
61
|
*/
|
|
24
62
|
export declare const executionEvents: (options: StreamOptions) => Stream.Stream<FunctionsMessage, StreamError>;
|
|
25
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Callback API kept for existing users; errors are tagged `StreamError`s or
|
|
65
|
+
* raw socket errors (transient: it reconnects by itself). While the server
|
|
66
|
+
* refuses the key (see {@link executionEvents}) nothing is reported: it
|
|
67
|
+
* retries silently, and reports once if the refusal lasts the whole window.
|
|
68
|
+
* After a `NetworkError` (`functions_disabled`, `functions_unavailable`,
|
|
69
|
+
* `channel_closed`, `invalid_key`, `unauthorized`, `account_changed`) the
|
|
70
|
+
* socket is closed and nothing more arrives.
|
|
71
|
+
*/
|
|
26
72
|
export declare function subscribeFunctions(options: {
|
|
27
73
|
url: string;
|
|
28
74
|
apiKey: string;
|
|
29
75
|
afterSeq?: number;
|
|
76
|
+
/** See {@link StreamOptions.keyRefusalWindowMs}. */
|
|
77
|
+
keyRefusalWindowMs?: number;
|
|
30
78
|
onEvent: (event: ExecutionEvent) => void;
|
|
31
79
|
onGap?: (gap: Gap) => void;
|
|
32
80
|
onError?: (error: unknown) => void;
|
package/dist/stream.js
CHANGED
|
@@ -6,19 +6,33 @@ export { ExecutionEvent, Gap } from "./contracts.js";
|
|
|
6
6
|
const decoder = (schema, what) => decodeWith(schema, undefined, (message) => new DecodeError({ what, message }));
|
|
7
7
|
const decodeEvent = decoder(ExecutionEvent, "execution event");
|
|
8
8
|
const decodeGap = decoder(Gap, "stream gap");
|
|
9
|
+
const refusedKey = 4401;
|
|
10
|
+
const defaultKeyRefusalWindowMs = 360_000;
|
|
11
|
+
const reconnectDelay = (closes) => Math.min(5_000, 500 * 2 ** closes) * (0.5 + Math.random() / 2);
|
|
9
12
|
function connect(options, handlers) {
|
|
10
13
|
let cursor = options.afterSeq;
|
|
14
|
+
const window = options.keyRefusalWindowMs ?? defaultKeyRefusalWindowMs;
|
|
11
15
|
const endpoint = new URL("/v1/stream", options.url);
|
|
12
16
|
endpoint.protocol =
|
|
13
17
|
endpoint.protocol === "https:" || endpoint.protocol === "wss:"
|
|
14
18
|
? "wss:"
|
|
15
19
|
: "ws:";
|
|
16
20
|
const socket = new Socket(endpoint.toString(), {
|
|
17
|
-
params: { api_key: options.apiKey },
|
|
21
|
+
params: { api_key: options.apiKey, on_refusal: "close" },
|
|
18
22
|
transport: WebSocket,
|
|
19
23
|
});
|
|
20
24
|
const channel = socket.channel("functions", () => cursor === undefined ? {} : { after_seq: cursor });
|
|
21
|
-
|
|
25
|
+
let account;
|
|
26
|
+
let checked = false;
|
|
27
|
+
let held = [];
|
|
28
|
+
const gated = (handle) => (raw) => {
|
|
29
|
+
if (closed)
|
|
30
|
+
return;
|
|
31
|
+
if (!checked)
|
|
32
|
+
return void held.push(() => handle(raw));
|
|
33
|
+
handle(raw);
|
|
34
|
+
};
|
|
35
|
+
channel.on("execution", gated((raw) => {
|
|
22
36
|
const result = decodeEvent(raw);
|
|
23
37
|
if (Result.isFailure(result))
|
|
24
38
|
return handlers.fail(result.failure);
|
|
@@ -27,26 +41,110 @@ function connect(options, handlers) {
|
|
|
27
41
|
return;
|
|
28
42
|
cursor = event.seq;
|
|
29
43
|
handlers.message({ _tag: "Execution", event });
|
|
30
|
-
});
|
|
31
|
-
channel.on("gap", (raw) => {
|
|
44
|
+
}));
|
|
45
|
+
channel.on("gap", gated((raw) => {
|
|
32
46
|
const result = decodeGap(raw);
|
|
33
47
|
if (Result.isFailure(result))
|
|
34
48
|
return handlers.fail(result.failure);
|
|
35
49
|
cursor = result.success.resumeAfter;
|
|
36
50
|
handlers.message({ _tag: "Gap", gap: result.success });
|
|
51
|
+
}));
|
|
52
|
+
let closed = false;
|
|
53
|
+
let timer;
|
|
54
|
+
const stop = () => {
|
|
55
|
+
closed = true;
|
|
56
|
+
held = [];
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
channel.leave();
|
|
59
|
+
socket.disconnect();
|
|
60
|
+
};
|
|
61
|
+
const fail = (message, cause, reason) => {
|
|
62
|
+
if (closed)
|
|
63
|
+
return;
|
|
64
|
+
stop();
|
|
65
|
+
handlers.fail(new NetworkError({
|
|
66
|
+
url: endpoint.toString(),
|
|
67
|
+
message,
|
|
68
|
+
cause,
|
|
69
|
+
...(reason === undefined ? {} : { reason }),
|
|
70
|
+
}));
|
|
71
|
+
};
|
|
72
|
+
channel.on("status", gated((raw) => {
|
|
73
|
+
const status = raw;
|
|
74
|
+
if (status?.available === false && status.reason === "functions_disabled")
|
|
75
|
+
fail("Functions access was removed from this account: the server closed the stream", raw, "functions_disabled");
|
|
76
|
+
}));
|
|
77
|
+
channel.onClose((cause) => fail(`The server closed the stream: subscribe again with afterSeq ${cursor ?? "(unset)"}`, cause, "channel_closed"));
|
|
78
|
+
let closes = 0;
|
|
79
|
+
let pending = false;
|
|
80
|
+
const reconnect = () => {
|
|
81
|
+
if (pending)
|
|
82
|
+
return;
|
|
83
|
+
pending = true;
|
|
84
|
+
socket.disconnect();
|
|
85
|
+
timer = setTimeout(() => {
|
|
86
|
+
pending = false;
|
|
87
|
+
socket.connect();
|
|
88
|
+
}, reconnectDelay(closes++));
|
|
89
|
+
};
|
|
90
|
+
let refusedSince;
|
|
91
|
+
const refused = (reason, message, cause) => {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
refusedSince ??= now;
|
|
94
|
+
if (now - refusedSince >= window)
|
|
95
|
+
return fail(message, cause, reason);
|
|
96
|
+
reconnect();
|
|
97
|
+
};
|
|
98
|
+
socket.onClose((event) => {
|
|
99
|
+
if (closed || pending)
|
|
100
|
+
return;
|
|
101
|
+
if (event?.code === refusedKey)
|
|
102
|
+
return refused("invalid_key", `The API key is no longer valid (revoked, unknown, or its account is not active): the server refused it for ${Math.round(window / 1000)} s`, event);
|
|
103
|
+
if (event?.code === 1000)
|
|
104
|
+
reconnect();
|
|
105
|
+
});
|
|
106
|
+
const transient = (error) => queueMicrotask(() => {
|
|
107
|
+
if (!closed && refusedSince === undefined)
|
|
108
|
+
handlers.transient(error);
|
|
109
|
+
});
|
|
110
|
+
socket.onError(transient);
|
|
111
|
+
channel.onError((error) => {
|
|
112
|
+
checked = false;
|
|
113
|
+
transient(error);
|
|
37
114
|
});
|
|
38
|
-
socket.onError((error) => handlers.transient(error));
|
|
39
|
-
channel.onError((error) => handlers.transient(error));
|
|
40
115
|
socket.connect();
|
|
41
|
-
channel
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
116
|
+
channel
|
|
117
|
+
.join()
|
|
118
|
+
.receive("ok", (reply) => {
|
|
119
|
+
if (closed)
|
|
120
|
+
return;
|
|
121
|
+
const named = reply?.account;
|
|
122
|
+
if (typeof named === "string") {
|
|
123
|
+
account ??= named;
|
|
124
|
+
if (named !== account)
|
|
125
|
+
return fail("The API key now belongs to another account: the stream of the previous account ended (its events are not followed on the new one)", reply, "account_changed");
|
|
126
|
+
}
|
|
127
|
+
checked = true;
|
|
128
|
+
closes = 0;
|
|
129
|
+
refusedSince = undefined;
|
|
130
|
+
const waiting = held;
|
|
131
|
+
held = [];
|
|
132
|
+
for (const handle of waiting)
|
|
133
|
+
if (!closed)
|
|
134
|
+
handle();
|
|
135
|
+
})
|
|
136
|
+
.receive("error", (reason) => {
|
|
137
|
+
if (closed)
|
|
138
|
+
return;
|
|
139
|
+
const code = reason?.reason;
|
|
140
|
+
const message = `Stream join refused: ${JSON.stringify(reason)}`;
|
|
141
|
+
if (code === "unauthorized")
|
|
142
|
+
return refused(code, message, reason);
|
|
143
|
+
fail(message, reason, typeof code === "string" ? code : undefined);
|
|
144
|
+
});
|
|
46
145
|
return {
|
|
47
146
|
close() {
|
|
48
|
-
|
|
49
|
-
socket.disconnect();
|
|
147
|
+
stop();
|
|
50
148
|
},
|
|
51
149
|
get cursor() {
|
|
52
150
|
return cursor;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johannc-dev/zyrk",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "SDK and CLI for Zyrk hosted functions
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "SDK and CLI for Zyrk hosted functions",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/build.mjs",
|
|
41
41
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
42
|
-
"test": "npm run build && npm run typecheck && node --test test/*.test.ts",
|
|
42
|
+
"test": "npm run build && npm run typecheck && node --test --test-timeout=60000 --test-force-exit test/*.test.ts",
|
|
43
43
|
"format": "prettier --write src types scripts test examples package.json tsconfig.json tsconfig.test.json README.md",
|
|
44
44
|
"format:check": "prettier --check src types scripts test examples package.json tsconfig.json tsconfig.test.json README.md",
|
|
45
45
|
"prepack": "npm run build"
|