@lotics/app-sdk 0.113.1 → 0.114.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.
@@ -174,6 +174,42 @@ function transportErrorMessage(status, parsed) {
174
174
  return status < 500 || authored ? jsonMessage : gatewayErrorMessage(status);
175
175
  }
176
176
 
177
+ // ../shared/src/schemas/read_row.ts
178
+ var RECORD_ID_COLUMN = "__source_record_id";
179
+
180
+ // ../shared/src/schemas/app_query_aggregate.ts
181
+ var APP_QUERY_BATCH_MAX = 16;
182
+ var AGGREGATE_COUNT_COLUMN = "__count";
183
+ var AGGREGATE_SUM_COLUMN = "__sum";
184
+ var AGGREGATE_DAY_COLUMN = "__day";
185
+ function dayOf(cell) {
186
+ return typeof cell === "string" && cell !== "" ? cell.slice(0, 10) : null;
187
+ }
188
+ function aggregateRows(rows, aggregate) {
189
+ const by = aggregate.by ?? [];
190
+ const day = aggregate.day;
191
+ const sum = aggregate.sum;
192
+ const keyed = /* @__PURE__ */ new Map();
193
+ for (const row of rows) {
194
+ const key = JSON.stringify([...by.map((column) => row[column] ?? null), ...day === void 0 ? [] : [dayOf(row[day])]]);
195
+ keyed.set(key, [...keyed.get(key) ?? [], row]);
196
+ }
197
+ return [...keyed.values()].map((members) => {
198
+ const [first2] = members;
199
+ const figures = members.flatMap((member) => {
200
+ const value = sum === void 0 ? void 0 : member[sum];
201
+ return typeof value === "number" ? [value] : [];
202
+ });
203
+ return {
204
+ [RECORD_ID_COLUMN]: null,
205
+ ...Object.fromEntries(by.map((column) => [column, first2?.[column] ?? null])),
206
+ ...day === void 0 ? {} : { [AGGREGATE_DAY_COLUMN]: dayOf(first2?.[day]) },
207
+ [AGGREGATE_COUNT_COLUMN]: members.length,
208
+ ...sum === void 0 ? {} : { [AGGREGATE_SUM_COLUMN]: figures.length === 0 ? null : figures.reduce((total, value) => total + value, 0) }
209
+ };
210
+ });
211
+ }
212
+
177
213
  // src/upload/optimize.ts
178
214
  var DEFAULT_IMAGE_FIDELITY = "high";
179
215
  var IMAGE_POLICIES = {
@@ -1032,9 +1068,47 @@ async function standaloneWriteModel() {
1032
1068
  const { app_id } = await boot();
1033
1069
  return apiCall("GET", `/v1/apps/${app_id}/write_model`, void 0, { appId: app_id });
1034
1070
  }
1035
- async function standaloneQuery(payload) {
1036
- const { app_id } = await boot();
1037
- return apiCall("POST", `/v1/apps/${app_id}/query`, payload, { appId: app_id });
1071
+ var READ_BATCH_WINDOW_MS = 5;
1072
+ var askedReads = [];
1073
+ function standaloneQuery(payload) {
1074
+ return new Promise((resolve, reject) => {
1075
+ askedReads = [...askedReads, { payload, resolve, reject }];
1076
+ if (askedReads.length === 1) setTimeout(() => void sendAskedReads(), READ_BATCH_WINDOW_MS);
1077
+ });
1078
+ }
1079
+ async function sendAskedReads() {
1080
+ const asked = askedReads;
1081
+ askedReads = [];
1082
+ const booted = await boot().catch((error) => {
1083
+ asked.forEach((read) => read.reject(error));
1084
+ return void 0;
1085
+ });
1086
+ if (booted === void 0) return;
1087
+ for (let at = 0; at < asked.length; at += APP_QUERY_BATCH_MAX) {
1088
+ const chunk = asked.slice(at, at + APP_QUERY_BATCH_MAX);
1089
+ apiCall("POST", `/v1/apps/${booted.app_id}/query_batch`, { reads: chunk.map((read) => read.payload) }, { appId: booted.app_id }).then((answered) => {
1090
+ const results = batchResults(answered);
1091
+ chunk.forEach((read, k) => {
1092
+ const answer = results[k];
1093
+ if (answer === void 0) read.reject(new Error(`The server answered ${results.length} of ${chunk.length} reads.`));
1094
+ else if ("result" in answer) read.resolve(answer.result);
1095
+ else read.reject(new ApiRefusal(transportErrorMessage(answer.error.status, answer.error)));
1096
+ });
1097
+ }).catch((error) => chunk.forEach((read) => read.reject(error)));
1098
+ }
1099
+ }
1100
+ function batchResults(answered) {
1101
+ if (answered === null || typeof answered !== "object" || !("results" in answered) || !Array.isArray(answered.results)) {
1102
+ throw new Error("The server's batch answer carried no results.");
1103
+ }
1104
+ return answered.results.map((answer, at) => {
1105
+ if (answer !== null && typeof answer === "object" && "result" in answer) return { result: answer.result };
1106
+ const error = answer !== null && typeof answer === "object" && "error" in answer ? answer.error : void 0;
1107
+ if (error !== null && typeof error === "object" && "status" in error && typeof error.status === "number" && "message" in error && typeof error.message === "string") {
1108
+ return { error: { status: error.status, message: error.message, code: "code" in error ? error.code : void 0 } };
1109
+ }
1110
+ throw new Error(`The server's batch answer ${at} is neither a result nor a refusal.`);
1111
+ });
1038
1112
  }
1039
1113
  async function standaloneExport(payload) {
1040
1114
  const { app_id } = await boot();
@@ -1119,6 +1193,9 @@ async function standaloneUpload(file, fidelity) {
1119
1193
 
1120
1194
  export {
1121
1195
  __export,
1196
+ AGGREGATE_COUNT_COLUMN,
1197
+ AGGREGATE_SUM_COLUMN,
1198
+ aggregateRows,
1122
1199
  DEFAULT_IMAGE_FIDELITY,
1123
1200
  recordingsVersion,
1124
1201
  subscribeRecordings,
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import {
2
+ AGGREGATE_COUNT_COLUMN,
3
+ AGGREGATE_SUM_COLUMN,
2
4
  DEFAULT_IMAGE_FIDELITY,
3
5
  __export,
6
+ aggregateRows,
4
7
  anyRecordingLive,
5
8
  applyPatch,
6
9
  decodeAll,
@@ -22,7 +25,7 @@ import {
22
25
  subscribeRecordings,
23
26
  subscribeUrlParams,
24
27
  urlParam
25
- } from "./chunk-CRJQYSDI.js";
28
+ } from "./chunk-EWCMZCKA.js";
26
29
 
27
30
  // src/mount.tsx
28
31
  import { createRoot } from "react-dom/client";
@@ -29066,41 +29069,6 @@ function useAgentRuns(sessionId, opts) {
29066
29069
  // src/queries.ts
29067
29070
  import { useCallback as useCallback5, useMemo as useMemo3, useState as useState3 } from "react";
29068
29071
 
29069
- // ../shared/src/schemas/read_row.ts
29070
- var RECORD_ID_COLUMN = "__source_record_id";
29071
-
29072
- // ../shared/src/schemas/app_query_aggregate.ts
29073
- var AGGREGATE_COUNT_COLUMN = "__count";
29074
- var AGGREGATE_SUM_COLUMN = "__sum";
29075
- var AGGREGATE_DAY_COLUMN = "__day";
29076
- function dayOf(cell) {
29077
- return typeof cell === "string" && cell !== "" ? cell.slice(0, 10) : null;
29078
- }
29079
- function aggregateRows(rows, aggregate) {
29080
- const by = aggregate.by ?? [];
29081
- const day = aggregate.day;
29082
- const sum2 = aggregate.sum;
29083
- const keyed = /* @__PURE__ */ new Map();
29084
- for (const row2 of rows) {
29085
- const key = JSON.stringify([...by.map((column) => row2[column] ?? null), ...day === void 0 ? [] : [dayOf(row2[day])]]);
29086
- keyed.set(key, [...keyed.get(key) ?? [], row2]);
29087
- }
29088
- return [...keyed.values()].map((members2) => {
29089
- const [first2] = members2;
29090
- const figures = members2.flatMap((member) => {
29091
- const value = sum2 === void 0 ? void 0 : member[sum2];
29092
- return typeof value === "number" ? [value] : [];
29093
- });
29094
- return {
29095
- [RECORD_ID_COLUMN]: null,
29096
- ...Object.fromEntries(by.map((column) => [column, first2?.[column] ?? null])),
29097
- ...day === void 0 ? {} : { [AGGREGATE_DAY_COLUMN]: dayOf(first2?.[day]) },
29098
- [AGGREGATE_COUNT_COLUMN]: members2.length,
29099
- ...sum2 === void 0 ? {} : { [AGGREGATE_SUM_COLUMN]: figures.length === 0 ? null : figures.reduce((total, value) => total + value, 0) }
29100
- };
29101
- });
29102
- }
29103
-
29104
29072
  // src/row.ts
29105
29073
  var PENDING = "__pending";
29106
29074
  function opt(v) {
package/dist/router.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  isEmbedded,
3
3
  postHostNotification,
4
4
  setUrlParams
5
- } from "./chunk-CRJQYSDI.js";
5
+ } from "./chunk-EWCMZCKA.js";
6
6
 
7
7
  // src/router.tsx
8
8
  import { useEffect } from "react";
@@ -143,7 +143,7 @@ write it as `{ node_type: "condition", type: "record_id", operator, value }`.
143
143
  inline `{ status: "open" }` each render is the same key; you never need to memoize params.
144
144
  - **The cache survives unmount/remount**, and **arrival is a refresh event**: returning to a screen
145
145
  renders the cached rows instantly *and* re-reads them in the background, so a list reflects what
146
- another screen changed while you were away. Identical concurrent reads share one request.
146
+ another screen changed while you were away. Identical concurrent reads share one request, and reads asked within a few milliseconds of each other go to the server together, each answered or refused on its own.
147
147
  Freshness comes from four places — arrival, window focus / tab return / network reconnect
148
148
  (`revalidateOnFocus`, default on; a read asked within the last few seconds is not asked again),
149
149
  a **realtime push**, and **this app's own successful write**, which re-reads the mounted queries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.113.1",
3
+ "version": "0.114.0",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {