@isomorph.ai/app-sdk 1.32.0 → 1.33.0

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 CHANGED
@@ -473,6 +473,32 @@ Without `onProgress` the call is unchanged and `ctx.progress` does nothing. At
473
473
  most 256 steps of up to 1 KiB each reach the page; further steps are dropped.
474
474
  The action still has the 300 s action budget. (Unreleased)
475
475
 
476
+ ## Watching a query: `data.watch`
477
+
478
+ Progress, status and export screens re-read one query on a timer:
479
+
480
+ ```ts
481
+ const stop = isomorph.data.watch(
482
+ () => isomorph.data.from('imports').select('*').eq('id', importId).single(),
483
+ ({ data }) => setImport(data),
484
+ { every: 3000, maxEvery: 15000 }, // the defaults
485
+ );
486
+ // on unmount
487
+ stop();
488
+ ```
489
+
490
+ `build()` runs at once and then every `every` ms (3 s). While the tab is
491
+ hidden no read is made (where there is no `document`, as in Node, nothing is
492
+ skipped). While the answer is unchanged the interval doubles up to `maxEvery`
493
+ (15 s) and goes back to `every` on the first change; every result, changed or
494
+ not, reaches `onResult`. A read that fails (a 401, the network) reaches it
495
+ too, as `{ data: null, error }` with the error `await`ing the query would
496
+ have thrown, and the timer keeps running. There is no new transport or route: each tick is an ordinary
497
+ `QueryBuilder.execute()`, so it counts as app activity and is billed and
498
+ authorized like any read. Watch the job row, not the data table. Use
499
+ `realtime.channel` only when several people edit the same rows and sub-second
500
+ updates matter. (1.32.0)
501
+
476
502
  ## Running on Lambda
477
503
 
478
504
  On a serverless cell Harbour runs an app's server code as Lambda functions, from a `runtime.zip` Kit Build makes; nothing in the app changes. The Web Function starts `dist/lambda-entry.js` (the zip's `index.js` is `import "@isomorph.ai/app-sdk/dist/lambda-entry.js"`), which serves actions on the port in `AWS_LAMBDA_HTTP_ENDPOINT` and stops on SIGTERM. The Event Function's handler is `node_modules/@isomorph.ai/app-sdk/dist/lambda-events-entry.handler`: `{"job": "<name>"}` runs that job once, as `--run-job <name>` does, and SQS records work queued runs until none are left.
package/dist/index.d.ts CHANGED
@@ -39,6 +39,15 @@ export interface IsomorphResult<T> {
39
39
  data: T;
40
40
  count?: number;
41
41
  }
42
+ /** `data.watch` timing: `every` ms between reads (default 3000), backing off to `maxEvery` (default 15000) while the answer does not change. */
43
+ export interface WatchOptions {
44
+ every?: number;
45
+ maxEvery?: number;
46
+ }
47
+ /** What `data.watch` delivers: the read's result, or `{ data: null, error }` when the read failed (the error `await`ing the query would have thrown). */
48
+ export type WatchResult = IsomorphResult<any> & {
49
+ error?: Error;
50
+ };
42
51
  /** How Slack renders a message's `text`: plain text, or Slack mrkdwn (`*bold*`, `<url|label>`, `<@U123>`); plain when absent. */
43
52
  export type SlackTextFormat = 'plain' | 'mrkdwn';
44
53
  /**
@@ -1895,6 +1904,7 @@ export declare function createClient(options?: ClientOptions): {
1895
1904
  data: {
1896
1905
  from: <T = Record<string, unknown>>(table: string) => QueryBuilder<T, T[]>;
1897
1906
  rpc: <T = unknown>(name: string, args?: Record<string, unknown>) => Promise<T>;
1907
+ watch: <T = Record<string, unknown>>(build: () => QueryBuilder<T, any>, onResult: (result: WatchResult) => void, options?: WatchOptions) => (() => void);
1898
1908
  };
1899
1909
  files: {
1900
1910
  createUpload: (input: {
package/dist/index.js CHANGED
@@ -707,6 +707,45 @@ export function createClient(options = {}) {
707
707
  data: {
708
708
  from: (table) => new QueryBuilder(transport, table),
709
709
  rpc: (name, args = {}) => transport.json(`/_harbour/data/rpc/${encodeURIComponent(name)}`, { method: 'POST', body: JSON.stringify(args) }),
710
+ // A query on a timer: QueryBuilder.execute() now and every `every` ms,
711
+ // no read while the tab is hidden, the interval doubling up to
712
+ // `maxEvery` while the answer is unchanged. No new transport or route;
713
+ // each read is an ordinary read (it counts as app activity), so watch
714
+ // the job row, not the data table. A failed read is delivered as
715
+ // { data: null, error } and the timer keeps going.
716
+ watch: (build, onResult, options = {}) => {
717
+ const every = options.every ?? 3_000;
718
+ const maxEvery = options.maxEvery ?? 15_000;
719
+ let delay = every;
720
+ let previous;
721
+ let stopped = false;
722
+ let timer;
723
+ const schedule = () => { if (!stopped)
724
+ timer = setTimer(tick, delay); };
725
+ const tick = async () => {
726
+ if (typeof document !== 'undefined' && document.hidden) {
727
+ schedule();
728
+ return;
729
+ }
730
+ let result;
731
+ try {
732
+ result = await build().execute();
733
+ }
734
+ catch (error) {
735
+ result = { data: null, error: error instanceof Error ? error : new Error(String(error)) };
736
+ }
737
+ if (stopped)
738
+ return;
739
+ const json = JSON.stringify({ ...result, error: result.error?.message });
740
+ delay = json === previous ? Math.min(delay * 2, maxEvery) : every;
741
+ previous = json;
742
+ onResult(result);
743
+ schedule();
744
+ };
745
+ void tick();
746
+ return () => { stopped = true; if (timer !== undefined)
747
+ clearTimer(timer); };
748
+ },
710
749
  },
711
750
  files: {
712
751
  createUpload: (input) => transport.json('/_harbour/files/uploads', { method: 'POST', body: JSON.stringify(input) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",