@colixsystems/widget-sdk 0.268.1 → 0.269.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 +10 -2
- package/dist/contract.cjs +16 -2
- package/dist/contract.js +16 -2
- package/dist/hooks.js +106 -59
- package/dist/stats-hooks.js +7 -1
- package/package.json +1 -1
- package/src/contract.js +16 -2
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
58
58
|
| **DATASTORE** | `useInterpretDraft(tableId)` | `{ interpret, interpreting, error, result, available }` | `interpret(tableId, body)` — `datastore.read:<table>`. Turns ONE sentence a user typed ("walk at 11 am tomorrow") into DRAFT column values so a form can prefill itself. IMPERATIVE: call `interpret(text, { fields?, timeZone? })` from an event handler, never on mount. It DRAFTS and writes nothing — show the values for review, then submit through `useDatastoreMutation().create`. Resolves to `{ values, unresolved }`; `values` is keyed by column NAME (the shape `create()` takes) and `unresolved` names the fields the sentence did not state. Only text / number / boolean / date / datetime / array columns are drafted — `FILE`, `RELATION`, `USER` and `USER_GROUP` carry ids and are never guessed. Fails closed to an empty draft. **Every call spends the workspace's AI credits** and is rate-limited per actor, so call it once per user action (never on mount or in a render loop); once the workspace runs out the call is refused with a generic 429 — an app user is deliberately **not** told the workspace's billing state, since they have never heard of an AI credit and cannot buy one. Never surface a raw error to the person filling the form: say drafting is unavailable and keep every field editable by hand. `available` is false where the host brokers no interpreter. |
|
|
59
59
|
| **AGENTS** | `useAgent(agentRef)` | `{ send, cancel, reset, messages, sending, resuming, error, available }` | `ctx.agents` — `ai.invoke:*`. Hold a conversation with one of the WORKSPACE's own AI agents (an author-written name + system prompt). Sending is IMPERATIVE: call `send(text)` from an event handler, never on mount — the first send opens the conversation, so a widget nobody types into costs nothing. On mount the hook DOES reopen the caller's most recent thread with this agent, so a refresh or a navigation keeps the conversation instead of starting a blank one; that resume only READS and spends no credit. `resuming` is true while it runs — render a loading state, not an empty thread. `messages` is the running transcript, oldest first. You **cannot** set or read the system prompt, replay an edited transcript, or pick a model: the server assembles every turn from the stored prompt plus the stored history, and `content` is the only thing that reaches the wire. `reset()` drops the thread (a "new chat" affordance), and the new thread is the one a later mount resumes. **Every send spends the workspace's AI credits** and is rate-limited per caller AND per workspace. Rejects with `AgentError`; branch on `.code` — `AI_QUOTA_EXCEEDED` (429) is the workspace's credit ceiling and will NOT clear on a retry, unlike `RATE_LIMITED`, so never offer "try again" for it, and `AGENT_DISABLED` (409) means the author turned it off, so hide the affordance. Never surface a raw error or the workspace's billing state to an app user. `available` is false when no agent is bound or the host brokers none — render a visible unbound state, never a blank box. An assistant message that used a tool also carries **`activity`** — what the agent DID with the user's data: the table it read (with the `search` text and `filters` it read with), or the table and columns it CHANGED (never the values, and never a row the permissions refused). **Render a write.** A reply saying "done!" is not evidence a row was written, and if the agent got it wrong the user has no other way to notice. A talk-only turn omits the key entirely. |
|
|
60
60
|
| **ACTIONS** | `useAction(actionRef)` | `{ available, run, running, result, error }` | `ctx.actions` — `actions.run:*`. Run one of the workspace's Actions from an event handler (never on mount) and read the value it passed to `respond()`. The Action needs the app trigger. `available` is false when nothing is bound or the host brokers no client. Errors are `ActionError` with a stable `code`. |
|
|
61
|
-
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*`. **To CLEAR a cell, pass it empty** — `update(id, { when: null })` (also `""` or `[]`) empties that cell and it reads back as `null` (`[]` on an array column). There is no separate clear call, so never write a sentinel such as `0` or a placeholder date to mean "no value". `false` and `0` are values, not clears, and a REQUIRED column cannot be cleared. |
|
|
61
|
+
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*`. A successful write re-reads every query, record, figure and aggregate on that table in every widget on the screen (a run of writes at most once a second, always once more after the last), so never refetch after it, and never write from an effect that re-runs on the data it reads unless a ref or Set marks what it already wrote. **To CLEAR a cell, pass it empty** — `update(id, { when: null })` (also `""` or `[]`) empties that cell and it reads back as `null` (`[]` on an array column). There is no separate clear call, so never write a sentinel such as `0` or a placeholder date to mean "no value". `false` and `0` are values, not clears, and a REQUIRED column cannot be cleared. |
|
|
62
62
|
| **DATASTORE** | `useDatastoreSubscription(table, handlers, options)` (optional: options) | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the realtime socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). For a live list of the signed-in user's own rows pass `{ kind: "granted" }`: every row they read through a grant to them or one of their groups, plus `onAccessGained(record)` / `onAccessLost({ id })` as rows enter and leave that set. Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
|
|
63
63
|
| **DATASTORE** | `useTyping(tableId, recordId)` | `{ typingUserIds, setTyping, status }` — status `"connecting" \| "live" \| "unavailable"` | `records(table).typing` — `datastore.read:<table>`. Who else is typing in one record, over the same socket as `useDatastoreSubscription`. Call `setTyping(true)` on every keystroke (throttled to one signal per 3 s; `false` is sent after 5 s idle) and `setTyping(false)` on send, clear or blur. `typingUserIds` lists the OTHER users' ids; each drops out 6 s after their last signal. Needs a signed-in user who can read the record. Never throws: an anonymous visitor, a refused subscription or a host whose client lacks `typing` reads `{ typingUserIds: [], status: "unavailable" }`. Re-subscribes when either id changes. |
|
|
64
64
|
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
@@ -86,7 +86,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
86
86
|
|
|
87
87
|
## Status
|
|
88
88
|
|
|
89
|
-
`v0.
|
|
89
|
+
`v0.269.1` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
90
|
+
|
|
91
|
+
### What's new in 0.269.1 (contract 1.219.0)
|
|
92
|
+
|
|
93
|
+
**Re-reads after a write are paced, and a write loop is refused.** A run of writes re-reads each widget at most once a second, and always once more after the last write; a reader that fetched on its own after the write is not re-read again. So a bulk loop of 500 saves costs each reader about one read a second, not 500, and the list still ends on the last save. An effect that saves and re-runs on the data it reads (or has no dependency list) would now save forever, because every save re-reads that data: keep the rows it already saved in a ref or a Set and return early, or save from the user's action. The publish checker refuses such an effect (`widget.writeLoopEffect`).
|
|
94
|
+
|
|
95
|
+
### What's new in 0.269.0 (contract 1.219.0)
|
|
96
|
+
|
|
97
|
+
**A write refreshes every widget that shows the table.** After a successful `create`, `update` or `delete` through `useDatastoreMutation`, every `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreFigure` and `useDatastoreAggregate` on the same table re-reads itself, in the widget that wrote and in every other widget on the screen, on the web Player and in the native app alike. Drop the `refetch()` you called after your own writes and any subscription you wired for them; `useDatastoreSubscription` stays for changes other people make. Widgets must address the table the same way (its id, as a `tableRef` property binds it) for the refresh to reach them.
|
|
90
98
|
|
|
91
99
|
### What's new in 0.268.1 (contract 1.218.0)
|
|
92
100
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1938,7 +1938,10 @@ const HOOKS = [
|
|
|
1938
1938
|
"enforce, computed for the whole page in one lookup. Gate a row's " +
|
|
1939
1939
|
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`. " +
|
|
1940
1940
|
"`total` is how many rows the query matches in all (the envelope's " +
|
|
1941
|
-
"meta.total), not just the page in `data`; null until known."
|
|
1941
|
+
"meta.total), not just the page in `data`; null until known. A " +
|
|
1942
|
+
"successful useDatastoreMutation write to the table, from this widget " +
|
|
1943
|
+
"or any other on the screen, re-reads it on its own: never refetch or " +
|
|
1944
|
+
"subscribe for your own writes.",
|
|
1942
1945
|
returnShape: {
|
|
1943
1946
|
data: "Record[]",
|
|
1944
1947
|
total: "number | null",
|
|
@@ -2212,6 +2215,15 @@ const HOOKS = [
|
|
|
2212
2215
|
{
|
|
2213
2216
|
name: "useDatastoreMutation",
|
|
2214
2217
|
signature: "useDatastoreMutation(tableId)",
|
|
2218
|
+
description:
|
|
2219
|
+
"Creates, updates (partial) and deletes rows. A successful write re-reads " +
|
|
2220
|
+
"every useDatastoreQuery, useDatastoreRecord, useDatastoreFigure and " +
|
|
2221
|
+
"useDatastoreAggregate on the same table reference, in every widget on " +
|
|
2222
|
+
"the screen, so no widget calls refetch() or subscribes for it. Readers " +
|
|
2223
|
+
"must name the table the same way (its id, as a tableRef property binds it). " +
|
|
2224
|
+
"A run of writes re-reads each reader at most once a second, always once " +
|
|
2225
|
+
"more after the last. Never write from a useEffect that re-runs on the data " +
|
|
2226
|
+
"it reads unless a ref or Set marks each row as already written: it loops.",
|
|
2215
2227
|
returnShape: {
|
|
2216
2228
|
create:
|
|
2217
2229
|
"(record) => Promise<Record> // rejects with DatastoreError { code, message, retryable } — render the message when retryable is false, never 'try again'",
|
|
@@ -6481,7 +6493,9 @@ const CONTRACT = deepFreeze({
|
|
|
6481
6493
|
// 1.218.0: additive (sc-14224) — the actions client's `status(actionId)`;
|
|
6482
6494
|
// `useAction().available` turns false when the server says a run would
|
|
6483
6495
|
// not reach the script.
|
|
6484
|
-
|
|
6496
|
+
// 1.219.0: behaviour (sc-14407) — a useDatastoreMutation write re-reads
|
|
6497
|
+
// every query, record, figure and aggregate on that table, app-wide.
|
|
6498
|
+
version: "1.219.0",
|
|
6485
6499
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6486
6500
|
hooks: HOOKS,
|
|
6487
6501
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1935,7 +1935,10 @@ const HOOKS = [
|
|
|
1935
1935
|
"enforce, computed for the whole page in one lookup. Gate a row's " +
|
|
1936
1936
|
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`. " +
|
|
1937
1937
|
"`total` is how many rows the query matches in all (the envelope's " +
|
|
1938
|
-
"meta.total), not just the page in `data`; null until known."
|
|
1938
|
+
"meta.total), not just the page in `data`; null until known. A " +
|
|
1939
|
+
"successful useDatastoreMutation write to the table, from this widget " +
|
|
1940
|
+
"or any other on the screen, re-reads it on its own: never refetch or " +
|
|
1941
|
+
"subscribe for your own writes.",
|
|
1939
1942
|
returnShape: {
|
|
1940
1943
|
data: "Record[]",
|
|
1941
1944
|
total: "number | null",
|
|
@@ -2209,6 +2212,15 @@ const HOOKS = [
|
|
|
2209
2212
|
{
|
|
2210
2213
|
name: "useDatastoreMutation",
|
|
2211
2214
|
signature: "useDatastoreMutation(tableId)",
|
|
2215
|
+
description:
|
|
2216
|
+
"Creates, updates (partial) and deletes rows. A successful write re-reads " +
|
|
2217
|
+
"every useDatastoreQuery, useDatastoreRecord, useDatastoreFigure and " +
|
|
2218
|
+
"useDatastoreAggregate on the same table reference, in every widget on " +
|
|
2219
|
+
"the screen, so no widget calls refetch() or subscribes for it. Readers " +
|
|
2220
|
+
"must name the table the same way (its id, as a tableRef property binds it). " +
|
|
2221
|
+
"A run of writes re-reads each reader at most once a second, always once " +
|
|
2222
|
+
"more after the last. Never write from a useEffect that re-runs on the data " +
|
|
2223
|
+
"it reads unless a ref or Set marks each row as already written: it loops.",
|
|
2212
2224
|
returnShape: {
|
|
2213
2225
|
create:
|
|
2214
2226
|
"(record) => Promise<Record> // rejects with DatastoreError { code, message, retryable } — render the message when retryable is false, never 'try again'",
|
|
@@ -6478,7 +6490,9 @@ const CONTRACT = deepFreeze({
|
|
|
6478
6490
|
// 1.218.0: additive (sc-14224) — the actions client's `status(actionId)`;
|
|
6479
6491
|
// `useAction().available` turns false when the server says a run would
|
|
6480
6492
|
// not reach the script.
|
|
6481
|
-
|
|
6493
|
+
// 1.219.0: behaviour (sc-14407) — a useDatastoreMutation write re-reads
|
|
6494
|
+
// every query, record, figure and aggregate on that table, app-wide.
|
|
6495
|
+
version: "1.219.0",
|
|
6482
6496
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6483
6497
|
hooks: HOOKS,
|
|
6484
6498
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -2888,6 +2888,84 @@ function serializeQuery(query) {
|
|
|
2888
2888
|
}
|
|
2889
2889
|
|
|
2890
2890
|
const queryResultKey = (table, queryKey) => JSON.stringify([table, queryKey]);
|
|
2891
|
+
|
|
2892
|
+
// sc-14407 — every widget of a running app shares this module (the web host
|
|
2893
|
+
// hands custom widgets its own SDK; the native bundle has one copy), so a
|
|
2894
|
+
// write through any widget re-reads every other widget's rows on that table.
|
|
2895
|
+
// A burst of writes folds into one re-read; a long run of them re-reads each
|
|
2896
|
+
// widget at most once a second, and always once more after the last write.
|
|
2897
|
+
const TABLE_WRITE_SETTLE_MS = 50;
|
|
2898
|
+
const TABLE_REREAD_INTERVAL_MS = 1000;
|
|
2899
|
+
const tableWrites = new Map();
|
|
2900
|
+
// Orders writes and fetch starts: a fetch begun after a write already has its rows.
|
|
2901
|
+
let readWriteOrder = 0;
|
|
2902
|
+
|
|
2903
|
+
/** Internal: stamps the start of a reader's fetch, for useRereadOnWrite. */
|
|
2904
|
+
export function stampFetchStart(ref) {
|
|
2905
|
+
readWriteOrder += 1;
|
|
2906
|
+
ref.current = readWriteOrder;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
function tableWritesOf(table) {
|
|
2910
|
+
let entry = tableWrites.get(table);
|
|
2911
|
+
if (!entry) {
|
|
2912
|
+
entry = { listeners: new Set(), timer: null, lastRereadAt: -Infinity, wroteAt: 0 };
|
|
2913
|
+
tableWrites.set(table, entry);
|
|
2914
|
+
}
|
|
2915
|
+
return entry;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function forgetIfIdle(table, entry) {
|
|
2919
|
+
if (entry.listeners.size === 0 && !entry.timer && tableWrites.get(table) === entry) tableWrites.delete(table);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
function rereadTable(table, entry) {
|
|
2923
|
+
entry.timer = null;
|
|
2924
|
+
entry.lastRereadAt = Date.now();
|
|
2925
|
+
for (const listener of Array.from(entry.listeners)) {
|
|
2926
|
+
try {
|
|
2927
|
+
listener(entry.wroteAt);
|
|
2928
|
+
} catch (_e) {
|
|
2929
|
+
// A reader's failure must never reach the write that already succeeded.
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
forgetIfIdle(table, entry);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
function announceTableWrite(table) {
|
|
2936
|
+
const entry = tableWritesOf(table);
|
|
2937
|
+
readWriteOrder += 1;
|
|
2938
|
+
entry.wroteAt = readWriteOrder;
|
|
2939
|
+
if (entry.timer) return;
|
|
2940
|
+
const wait = Math.max(TABLE_WRITE_SETTLE_MS, entry.lastRereadAt + TABLE_REREAD_INTERVAL_MS - Date.now());
|
|
2941
|
+
entry.timer = setTimeout(() => rereadTable(table, entry), wait);
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
/**
|
|
2945
|
+
* Internal: shared with stats-hooks.js, not re-exported by the index. Runs
|
|
2946
|
+
* `reread` after a successful useDatastoreMutation write to `table`, from any
|
|
2947
|
+
* widget, unless the reader began a fetch of its own (stamped into
|
|
2948
|
+
* `fetchStartRef` by stampFetchStart) after that write. Keyed by the table
|
|
2949
|
+
* reference exactly as the widgets pass it.
|
|
2950
|
+
*/
|
|
2951
|
+
export function useRereadOnWrite(table, reread, fetchStartRef) {
|
|
2952
|
+
const rereadRef = useRef(reread);
|
|
2953
|
+
rereadRef.current = reread;
|
|
2954
|
+
useEffect(() => {
|
|
2955
|
+
if (!table) return undefined;
|
|
2956
|
+
const entry = tableWritesOf(table);
|
|
2957
|
+
const listener = (wroteAt) => {
|
|
2958
|
+
if (fetchStartRef && fetchStartRef.current > wroteAt) return;
|
|
2959
|
+
rereadRef.current();
|
|
2960
|
+
};
|
|
2961
|
+
entry.listeners.add(listener);
|
|
2962
|
+
return () => {
|
|
2963
|
+
entry.listeners.delete(listener);
|
|
2964
|
+
forgetIfIdle(table, entry);
|
|
2965
|
+
};
|
|
2966
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2967
|
+
}, [table]);
|
|
2968
|
+
}
|
|
2891
2969
|
const recordResultKey = (table, recordId, includeCapabilities = false) =>
|
|
2892
2970
|
JSON.stringify([table, recordId, includeCapabilities]);
|
|
2893
2971
|
|
|
@@ -2961,6 +3039,7 @@ export function useDatastoreQuery(table, query) {
|
|
|
2961
3039
|
recordsRef.current = ctx.datastore.records;
|
|
2962
3040
|
|
|
2963
3041
|
const runRef = useRef(0);
|
|
3042
|
+
const fetchStartRef = useRef(0);
|
|
2964
3043
|
|
|
2965
3044
|
const doFetch = useCallback(async () => {
|
|
2966
3045
|
const myRun = ++runRef.current;
|
|
@@ -2972,6 +3051,7 @@ export function useDatastoreQuery(table, query) {
|
|
|
2972
3051
|
setSettled(NO_QUERY_RESULT);
|
|
2973
3052
|
return;
|
|
2974
3053
|
}
|
|
3054
|
+
stampFetchStart(fetchStartRef);
|
|
2975
3055
|
const key = queryResultKey(t, serializeQuery(queryRef.current));
|
|
2976
3056
|
setInFlight(true);
|
|
2977
3057
|
try {
|
|
@@ -3041,6 +3121,7 @@ export function useDatastoreQuery(table, query) {
|
|
|
3041
3121
|
// sc-1179 — auto-subscribe to the page-level refresh tick so a
|
|
3042
3122
|
// pull-to-refresh on mobile re-runs the query without per-widget code.
|
|
3043
3123
|
useRefresh(refetch);
|
|
3124
|
+
useRereadOnWrite(table, refetch, fetchStartRef);
|
|
3044
3125
|
|
|
3045
3126
|
const currentKey = table ? queryResultKey(table, queryKey) : null;
|
|
3046
3127
|
const busy = Boolean(table) && (settled.key !== currentKey || inFlight);
|
|
@@ -3106,6 +3187,7 @@ export function useDatastoreRecord(table, recordId, options) {
|
|
|
3106
3187
|
recordsRef.current = ctx.datastore.records;
|
|
3107
3188
|
|
|
3108
3189
|
const runRef = useRef(0);
|
|
3190
|
+
const fetchStartRef = useRef(0);
|
|
3109
3191
|
|
|
3110
3192
|
const doFetch = useCallback(async () => {
|
|
3111
3193
|
const myRun = ++runRef.current;
|
|
@@ -3116,6 +3198,7 @@ export function useDatastoreRecord(table, recordId, options) {
|
|
|
3116
3198
|
setSettled(NO_RECORD_RESULT);
|
|
3117
3199
|
return;
|
|
3118
3200
|
}
|
|
3201
|
+
stampFetchStart(fetchStartRef);
|
|
3119
3202
|
const key = recordResultKey(t, id, includeCapabilitiesRef.current);
|
|
3120
3203
|
setInFlight(true);
|
|
3121
3204
|
try {
|
|
@@ -3149,6 +3232,7 @@ export function useDatastoreRecord(table, recordId, options) {
|
|
|
3149
3232
|
|
|
3150
3233
|
// sc-1179 — auto-subscribe to the page-level refresh tick.
|
|
3151
3234
|
useRefresh(refetch);
|
|
3235
|
+
useRereadOnWrite(recordId ? table : null, refetch, fetchStartRef);
|
|
3152
3236
|
|
|
3153
3237
|
const currentKey =
|
|
3154
3238
|
table && recordId ? recordResultKey(table, recordId, includeCapabilities) : null;
|
|
@@ -4044,68 +4128,31 @@ export function useDatastoreMutation(table) {
|
|
|
4044
4128
|
tableRef.current = table;
|
|
4045
4129
|
recordsRef.current = ctx.datastore.records;
|
|
4046
4130
|
|
|
4047
|
-
// Resolve records(table) lazily
|
|
4048
|
-
//
|
|
4049
|
-
//
|
|
4050
|
-
const
|
|
4131
|
+
// Resolve records(table) lazily, so an unbound widget rejects with a clear
|
|
4132
|
+
// VALIDATION error instead of crashing at render. A successful write
|
|
4133
|
+
// re-reads every widget showing this table (sc-14407).
|
|
4134
|
+
const write = useCallback(async (verb, run) => {
|
|
4051
4135
|
const t = tableRef.current;
|
|
4052
|
-
if (!t)
|
|
4053
|
-
|
|
4136
|
+
if (!t) {
|
|
4137
|
+
throw new DatastoreError(
|
|
4138
|
+
"VALIDATION",
|
|
4139
|
+
`useDatastoreMutation: no table bound — set a source table before ${verb} a record.`,
|
|
4140
|
+
);
|
|
4141
|
+
}
|
|
4142
|
+
const ns = recordsRef.current(t);
|
|
4143
|
+
let result;
|
|
4144
|
+
try {
|
|
4145
|
+
result = await run(ns);
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
throw toDatastoreError(err);
|
|
4148
|
+
}
|
|
4149
|
+
announceTableWrite(t);
|
|
4150
|
+
return result;
|
|
4054
4151
|
}, []);
|
|
4055
4152
|
|
|
4056
|
-
const create = useCallback(
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
if (!ns) {
|
|
4060
|
-
throw new DatastoreError(
|
|
4061
|
-
"VALIDATION",
|
|
4062
|
-
"useDatastoreMutation: no table bound — set a source table before creating a record.",
|
|
4063
|
-
);
|
|
4064
|
-
}
|
|
4065
|
-
try {
|
|
4066
|
-
return await ns.create(values);
|
|
4067
|
-
} catch (err) {
|
|
4068
|
-
throw toDatastoreError(err);
|
|
4069
|
-
}
|
|
4070
|
-
},
|
|
4071
|
-
[resolveNs],
|
|
4072
|
-
);
|
|
4073
|
-
|
|
4074
|
-
const update = useCallback(
|
|
4075
|
-
async (id, values) => {
|
|
4076
|
-
const ns = resolveNs();
|
|
4077
|
-
if (!ns) {
|
|
4078
|
-
throw new DatastoreError(
|
|
4079
|
-
"VALIDATION",
|
|
4080
|
-
"useDatastoreMutation: no table bound — set a source table before updating a record.",
|
|
4081
|
-
);
|
|
4082
|
-
}
|
|
4083
|
-
try {
|
|
4084
|
-
return await ns.update(id, values);
|
|
4085
|
-
} catch (err) {
|
|
4086
|
-
throw toDatastoreError(err);
|
|
4087
|
-
}
|
|
4088
|
-
},
|
|
4089
|
-
[resolveNs],
|
|
4090
|
-
);
|
|
4091
|
-
|
|
4092
|
-
const del = useCallback(
|
|
4093
|
-
async (id) => {
|
|
4094
|
-
const ns = resolveNs();
|
|
4095
|
-
if (!ns) {
|
|
4096
|
-
throw new DatastoreError(
|
|
4097
|
-
"VALIDATION",
|
|
4098
|
-
"useDatastoreMutation: no table bound — set a source table before deleting a record.",
|
|
4099
|
-
);
|
|
4100
|
-
}
|
|
4101
|
-
try {
|
|
4102
|
-
return await ns.delete(id);
|
|
4103
|
-
} catch (err) {
|
|
4104
|
-
throw toDatastoreError(err);
|
|
4105
|
-
}
|
|
4106
|
-
},
|
|
4107
|
-
[resolveNs],
|
|
4108
|
-
);
|
|
4153
|
+
const create = useCallback((values) => write("creating", (ns) => ns.create(values)), [write]);
|
|
4154
|
+
const update = useCallback((id, values) => write("updating", (ns) => ns.update(id, values)), [write]);
|
|
4155
|
+
const del = useCallback((id) => write("deleting", (ns) => ns.delete(id)), [write]);
|
|
4109
4156
|
|
|
4110
4157
|
// sc-10268: one object while the methods are unchanged, so a widget can list
|
|
4111
4158
|
// the mutation in a dependency array without re-running every render.
|
package/dist/stats-hooks.js
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
toDatastoreError,
|
|
7
7
|
toDirectoryError,
|
|
8
8
|
useRefresh,
|
|
9
|
+
stampFetchStart,
|
|
10
|
+
useRereadOnWrite,
|
|
9
11
|
useWidgetContextOrThrow,
|
|
10
12
|
} from "./hooks.js";
|
|
11
13
|
|
|
@@ -27,7 +29,7 @@ function serialize(value) {
|
|
|
27
29
|
* keeps the previous data on screen (`fetching`); a new identity starts over
|
|
28
30
|
* (`loading`). A null key is "nothing to read" and settles empty.
|
|
29
31
|
*/
|
|
30
|
-
function useKeyedRead(read, identity, key, mapError) {
|
|
32
|
+
function useKeyedRead(read, identity, key, mapError, fetchStartRef) {
|
|
31
33
|
const readRef = useRef(read);
|
|
32
34
|
readRef.current = read;
|
|
33
35
|
const runRef = useRef(0);
|
|
@@ -42,6 +44,7 @@ function useKeyedRead(read, identity, key, mapError) {
|
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
setFetching(true);
|
|
47
|
+
if (fetchStartRef) stampFetchStart(fetchStartRef);
|
|
45
48
|
try {
|
|
46
49
|
const data = await readRef.current();
|
|
47
50
|
if (runRef.current === myRun) setSettled({ identity, key, data, error: null });
|
|
@@ -97,6 +100,7 @@ export function useDatastoreAggregate(table, spec) {
|
|
|
97
100
|
? serialize(byDate ? [groupBy, bucket, from, to, sumColumn, filter] : [groupBy, sumColumn, filter, order, limit])
|
|
98
101
|
: null;
|
|
99
102
|
const records = ctx.datastore.records;
|
|
103
|
+
const fetchStartRef = useRef(0);
|
|
100
104
|
|
|
101
105
|
const result = useKeyedRead(
|
|
102
106
|
async () => {
|
|
@@ -111,7 +115,9 @@ export function useDatastoreAggregate(table, spec) {
|
|
|
111
115
|
table ? `${table}|${byDate ? "date" : "column"}` : null,
|
|
112
116
|
key,
|
|
113
117
|
toDatastoreError,
|
|
118
|
+
fetchStartRef,
|
|
114
119
|
);
|
|
120
|
+
useRereadOnWrite(table, result.refetch, fetchStartRef);
|
|
115
121
|
|
|
116
122
|
const { data, ...state } = result;
|
|
117
123
|
const rows = data || [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.269.1",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -1935,7 +1935,10 @@ const HOOKS = [
|
|
|
1935
1935
|
"enforce, computed for the whole page in one lookup. Gate a row's " +
|
|
1936
1936
|
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`. " +
|
|
1937
1937
|
"`total` is how many rows the query matches in all (the envelope's " +
|
|
1938
|
-
"meta.total), not just the page in `data`; null until known."
|
|
1938
|
+
"meta.total), not just the page in `data`; null until known. A " +
|
|
1939
|
+
"successful useDatastoreMutation write to the table, from this widget " +
|
|
1940
|
+
"or any other on the screen, re-reads it on its own: never refetch or " +
|
|
1941
|
+
"subscribe for your own writes.",
|
|
1939
1942
|
returnShape: {
|
|
1940
1943
|
data: "Record[]",
|
|
1941
1944
|
total: "number | null",
|
|
@@ -2209,6 +2212,15 @@ const HOOKS = [
|
|
|
2209
2212
|
{
|
|
2210
2213
|
name: "useDatastoreMutation",
|
|
2211
2214
|
signature: "useDatastoreMutation(tableId)",
|
|
2215
|
+
description:
|
|
2216
|
+
"Creates, updates (partial) and deletes rows. A successful write re-reads " +
|
|
2217
|
+
"every useDatastoreQuery, useDatastoreRecord, useDatastoreFigure and " +
|
|
2218
|
+
"useDatastoreAggregate on the same table reference, in every widget on " +
|
|
2219
|
+
"the screen, so no widget calls refetch() or subscribes for it. Readers " +
|
|
2220
|
+
"must name the table the same way (its id, as a tableRef property binds it). " +
|
|
2221
|
+
"A run of writes re-reads each reader at most once a second, always once " +
|
|
2222
|
+
"more after the last. Never write from a useEffect that re-runs on the data " +
|
|
2223
|
+
"it reads unless a ref or Set marks each row as already written: it loops.",
|
|
2212
2224
|
returnShape: {
|
|
2213
2225
|
create:
|
|
2214
2226
|
"(record) => Promise<Record> // rejects with DatastoreError { code, message, retryable } — render the message when retryable is false, never 'try again'",
|
|
@@ -6478,7 +6490,9 @@ const CONTRACT = deepFreeze({
|
|
|
6478
6490
|
// 1.218.0: additive (sc-14224) — the actions client's `status(actionId)`;
|
|
6479
6491
|
// `useAction().available` turns false when the server says a run would
|
|
6480
6492
|
// not reach the script.
|
|
6481
|
-
|
|
6493
|
+
// 1.219.0: behaviour (sc-14407) — a useDatastoreMutation write re-reads
|
|
6494
|
+
// every query, record, figure and aggregate on that table, app-wide.
|
|
6495
|
+
version: "1.219.0",
|
|
6482
6496
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6483
6497
|
hooks: HOOKS,
|
|
6484
6498
|
primitives: PRIMITIVES,
|