grist-widget-sdk 0.3.25 → 0.3.27

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/advanced.d.cts +3 -2
  3. package/dist/advanced.d.ts +3 -2
  4. package/dist/advanced.js +10 -13
  5. package/dist/advanced.js.map +1 -1
  6. package/dist/app.cjs +5176 -0
  7. package/dist/app.cjs.map +1 -0
  8. package/dist/app.d.cts +147 -0
  9. package/dist/app.d.ts +147 -0
  10. package/dist/app.js +23 -0
  11. package/dist/app.js.map +1 -0
  12. package/dist/chunk-6Y2IONGJ.js +519 -0
  13. package/dist/chunk-6Y2IONGJ.js.map +1 -0
  14. package/dist/chunk-B4AKVSWV.js +2362 -0
  15. package/dist/chunk-B4AKVSWV.js.map +1 -0
  16. package/dist/{chunk-TZB3OZA6.js → chunk-NO6FYX33.js} +782 -328
  17. package/dist/chunk-NO6FYX33.js.map +1 -0
  18. package/dist/{chunk-JASMM2YC.js → chunk-OPTOYD2Y.js} +36 -2
  19. package/dist/chunk-OPTOYD2Y.js.map +1 -0
  20. package/dist/{create-grist-emulator-_3_JK7Rr.d.ts → create-grist-emulator-BCqp1BF9.d.ts} +2 -2
  21. package/dist/{create-grist-emulator-K-iUKUCZ.d.cts → create-grist-emulator-DBM8OSIR.d.cts} +2 -2
  22. package/dist/emulator/devtools.js +2 -4
  23. package/dist/emulator/devtools.js.map +1 -1
  24. package/dist/emulator/testing.cjs +5098 -87
  25. package/dist/emulator/testing.cjs.map +1 -1
  26. package/dist/emulator/testing.d.cts +56 -5
  27. package/dist/emulator/testing.d.ts +56 -5
  28. package/dist/emulator/testing.js +142 -4
  29. package/dist/emulator/testing.js.map +1 -1
  30. package/dist/emulator/transports.d.cts +3 -3
  31. package/dist/emulator/transports.d.ts +3 -3
  32. package/dist/emulator.d.cts +5 -5
  33. package/dist/emulator.d.ts +5 -5
  34. package/dist/index.d.cts +9 -500
  35. package/dist/index.d.ts +9 -500
  36. package/dist/index.js +57 -2294
  37. package/dist/index.js.map +1 -1
  38. package/dist/{inline-A8yaddmp.d.cts → inline-C7DkvuET.d.cts} +1 -1
  39. package/dist/{inline-B7QQeWul.d.ts → inline-DpcT8tV9.d.ts} +1 -1
  40. package/dist/{templates-CB_VwjEU.d.cts → templates-8k2LKoO-.d.cts} +1 -1
  41. package/dist/{templates-pRIXc2RR.d.ts → templates-DIwi_mvI.d.ts} +1 -1
  42. package/dist/{types-YcRfo7oy.d.cts → types-D3JxUCfL.d.cts} +1 -1
  43. package/dist/{types-YcRfo7oy.d.ts → types-D3JxUCfL.d.ts} +1 -1
  44. package/dist/{use-grist-widget-options-from-context-hl-NwOwP.d.ts → use-grist-reads-DUMvJgV7.d.ts} +3 -44
  45. package/dist/{use-grist-widget-options-from-context-Bbd5-f39.d.cts → use-grist-reads-DpjVuI0H.d.cts} +3 -44
  46. package/dist/use-grist-types-DdOLEJXN.d.cts +499 -0
  47. package/dist/use-grist-types-kwxoFeub.d.ts +499 -0
  48. package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.cts +43 -0
  49. package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.ts +43 -0
  50. package/package.json +11 -1
  51. package/dist/chunk-EPEBKXVN.js +0 -470
  52. package/dist/chunk-EPEBKXVN.js.map +0 -1
  53. package/dist/chunk-JASMM2YC.js.map +0 -1
  54. package/dist/chunk-N3X46JW2.js +0 -37
  55. package/dist/chunk-N3X46JW2.js.map +0 -1
  56. package/dist/chunk-TZB3OZA6.js.map +0 -1
@@ -3,7 +3,64 @@ import {
3
3
  formatInsufficientAccessMessage,
4
4
  isAccessSufficient,
5
5
  parseGristRequiredAccess
6
- } from "./chunk-N3X46JW2.js";
6
+ } from "./chunk-OPTOYD2Y.js";
7
+
8
+ // src/sdk/helpers/grist-fetch-with-auth.ts
9
+ var TOKEN_TTL_MS = 45e3;
10
+ var cache = /* @__PURE__ */ new Map();
11
+ var inFlight = /* @__PURE__ */ new Map();
12
+ function cacheKey(readOnly) {
13
+ return readOnly ? "ro" : "rw";
14
+ }
15
+ async function getGristAccessToken(docApi, options) {
16
+ if (!docApi.getAccessToken) {
17
+ throw new Error("getAccessToken is not available on docApi (need higher access level)");
18
+ }
19
+ const readOnly = options?.readOnly ?? false;
20
+ const key = cacheKey(readOnly);
21
+ const now = Date.now();
22
+ const hit = cache.get(key);
23
+ if (hit && now - hit.fetchedAt < TOKEN_TTL_MS) {
24
+ return hit.token;
25
+ }
26
+ const pending = inFlight.get(key);
27
+ if (pending) return pending;
28
+ const next = docApi.getAccessToken({ readOnly }).then((token) => {
29
+ cache.set(key, { token, fetchedAt: Date.now() });
30
+ inFlight.delete(key);
31
+ return token;
32
+ });
33
+ inFlight.set(key, next);
34
+ return next;
35
+ }
36
+ function clearGristAccessTokenCache() {
37
+ cache.clear();
38
+ inFlight.clear();
39
+ }
40
+ async function fetchWithGristAuth(docApi, path, init) {
41
+ const readOnly = init?.readOnly ?? false;
42
+ let token;
43
+ try {
44
+ token = await getGristAccessToken(docApi, { readOnly });
45
+ } catch (err) {
46
+ clearGristAccessTokenCache();
47
+ throw err;
48
+ }
49
+ const isAbsolute = /^https?:\/\//i.test(path);
50
+ const cleanPath = isAbsolute ? path : path.startsWith("/") ? path : `/${path}`;
51
+ const url = isAbsolute ? new URL(path) : new URL(`${token.baseUrl.replace(/\/$/, "")}${cleanPath}`);
52
+ if (!url.searchParams.has("auth")) url.searchParams.set("auth", token.token);
53
+ const headers = new Headers(init?.headers ?? {});
54
+ headers.delete("Authorization");
55
+ const response = await fetch(url.toString(), { ...init, headers });
56
+ if (response.status === 401) {
57
+ clearGristAccessTokenCache();
58
+ const retryToken = await getGristAccessToken(docApi, { readOnly });
59
+ url.searchParams.set("auth", retryToken.token);
60
+ return fetch(url.toString(), { ...init, headers });
61
+ }
62
+ return response;
63
+ }
7
64
 
8
65
  // src/sdk/helpers/grist-mappings.ts
9
66
  function asColumnSpec(entry) {
@@ -42,6 +99,9 @@ var pendingColumnMappingStatus = {
42
99
  emptyMultiples: []
43
100
  };
44
101
 
102
+ // src/sdk/internal/hooks/use-grist-core.ts
103
+ import { useCallback, useMemo, useSyncExternalStore } from "react";
104
+
45
105
  // src/sdk/internal/handshake/types.ts
46
106
  var ZERO_GENERATION = 0;
47
107
  function nextGeneration(current) {
@@ -70,142 +130,6 @@ function initialGristSnapshot() {
70
130
  };
71
131
  }
72
132
 
73
- // src/sdk/internal/handshake/derive.ts
74
- function deriveStatus(snapshot) {
75
- const { lifecycle, authz, link } = snapshot;
76
- if (lifecycle.phase === "idle" || lifecycle.phase === "detecting") return "booting";
77
- if (lifecycle.phase === "negotiating") return "booting";
78
- if (lifecycle.phase === "terminated") {
79
- switch (lifecycle.reason.kind) {
80
- case "script_timeout":
81
- case "not_embedded":
82
- return "unavailable";
83
- case "host_rejected":
84
- case "ready_failed":
85
- case "ready_timeout":
86
- case "manual_terminate":
87
- return "error";
88
- }
89
- }
90
- if (authz.state === "denied") return "error";
91
- if (link.state === "lost") return "error";
92
- return "ready";
93
- }
94
- function deriveErrorMessage(snapshot) {
95
- if (snapshot.lifecycle.phase === "terminated") {
96
- switch (snapshot.lifecycle.reason.kind) {
97
- case "ready_failed":
98
- return snapshot.lifecycle.reason.message;
99
- case "ready_timeout":
100
- return `Grist handshake timed out after ${snapshot.lifecycle.reason.afterMs}ms`;
101
- case "host_rejected":
102
- return snapshot.lifecycle.reason.message;
103
- case "script_timeout":
104
- return `Grist plugin API never loaded (${snapshot.lifecycle.reason.attempts} attempts)`;
105
- case "not_embedded":
106
- return "This page is not embedded in a Grist iframe";
107
- case "manual_terminate":
108
- return null;
109
- }
110
- }
111
- if (snapshot.authz.state === "denied") {
112
- return snapshot.authz.message ?? `Grist denied requested access (${snapshot.authz.declared})`;
113
- }
114
- if (snapshot.lifecycle.phase === "online" && snapshot.link.state === "lost") {
115
- return `Grist link lost (${snapshot.link.missedPings} consecutive failures)`;
116
- }
117
- return null;
118
- }
119
- function deriveCapabilities(snapshot) {
120
- const { lifecycle, authz, link, config, sync } = snapshot;
121
- const online = lifecycle.phase === "online";
122
- const granted = authz.state === "granted";
123
- const grantedReadOrMore = granted && ACCESS_RANK[authz.level] >= ACCESS_RANK["read table"];
124
- const grantedFull = granted && authz.level === "full";
125
- const linkUsable = link.state !== "lost";
126
- const canRead = online && linkUsable && grantedReadOrMore;
127
- const canFetchTable = canRead && link.state === "connected";
128
- const mappingsUsable = hasUsableMappings(config.mappings);
129
- const missing = mappingsMissing(config.mappings);
130
- const emptyMultiples = mappingsEmptyMultiples(config.mappings);
131
- const canRender = canRead && mappingsUsable;
132
- const canWriteRecords = canRender && grantedFull;
133
- const canWriteSchema = canWriteRecords && sync.currentTable.state === "known";
134
- const hasFreshSelection = canRead && sync.record.state === "hot" && config.mappings.state !== "invalidating";
135
- return {
136
- canRead,
137
- canRender,
138
- canWriteRecords,
139
- canWriteSchema,
140
- canFetchTable,
141
- hasFreshSelection,
142
- hasUsableMappings: mappingsUsable,
143
- missingMappings: missing,
144
- emptyMultipleMappings: emptyMultiples
145
- };
146
- }
147
- function hasUsableMappings(state) {
148
- if (state.state === "not_declared") return true;
149
- if (state.state === "complete") return true;
150
- if (state.state === "invalidating") {
151
- return state.previous.state === "complete";
152
- }
153
- return false;
154
- }
155
- function mappingsMissing(state) {
156
- if (state.state === "incomplete") return state.missing;
157
- if (state.state === "invalidating" && state.previous.state === "incomplete") {
158
- return state.previous.missing;
159
- }
160
- return EMPTY;
161
- }
162
- function mappingsEmptyMultiples(state) {
163
- if (state.state === "incomplete") return state.emptyMultiples;
164
- if (state.state === "invalidating" && state.previous.state === "incomplete") {
165
- return state.previous.emptyMultiples;
166
- }
167
- return EMPTY;
168
- }
169
- var EMPTY = Object.freeze([]);
170
- function extractResolvedMappings(state) {
171
- switch (state.state) {
172
- case "complete":
173
- case "incomplete":
174
- return state.mappings;
175
- case "invalidating":
176
- if (state.previous.state === "complete" || state.previous.state === "incomplete") {
177
- return state.previous.mappings;
178
- }
179
- return null;
180
- default:
181
- return null;
182
- }
183
- }
184
- function deriveColumnMappingStatus(mappingState, columns) {
185
- if (!columns?.length) {
186
- return { ok: true, pending: false, missing: [], emptyMultiples: [] };
187
- }
188
- switch (mappingState.state) {
189
- case "not_declared":
190
- return { ok: true, pending: false, missing: [], emptyMultiples: [] };
191
- case "pending":
192
- return pendingColumnMappingStatus;
193
- case "unreported":
194
- return pendingColumnMappingStatus;
195
- case "complete":
196
- return { ok: true, pending: false, missing: [], emptyMultiples: [] };
197
- case "incomplete":
198
- return {
199
- ok: false,
200
- pending: false,
201
- missing: [...mappingState.missing],
202
- emptyMultiples: [...mappingState.emptyMultiples]
203
- };
204
- case "invalidating":
205
- return deriveColumnMappingStatus(mappingState.previous, columns);
206
- }
207
- }
208
-
209
133
  // src/sdk/internal/handshake/actions.ts
210
134
  function hasGenerationStamp(action) {
211
135
  return "generation" in action && typeof action.generation === "number";
@@ -707,197 +631,212 @@ function updateSync(snapshot, sync) {
707
631
  return { ...snapshot, sync };
708
632
  }
709
633
 
710
- // src/sdk/helpers/grist-widget-interaction.ts
711
- function parseGristWidgetLinkingInfo(raw) {
712
- if (!raw || typeof raw !== "object" || !("asTarget" in raw)) return void 0;
713
- const linking = raw;
714
- return {
715
- asTarget: linking.asTarget == null ? null : String(linking.asTarget),
716
- asSource: Boolean(linking.asSource)
717
- };
634
+ // src/sdk/internal/handshake/derive.ts
635
+ function deriveStatus(snapshot) {
636
+ const { lifecycle, authz, link } = snapshot;
637
+ if (lifecycle.phase === "idle" || lifecycle.phase === "detecting") return "booting";
638
+ if (lifecycle.phase === "negotiating") return "booting";
639
+ if (lifecycle.phase === "terminated") {
640
+ switch (lifecycle.reason.kind) {
641
+ case "script_timeout":
642
+ case "not_embedded":
643
+ return "unavailable";
644
+ case "host_rejected":
645
+ case "ready_failed":
646
+ case "ready_timeout":
647
+ case "manual_terminate":
648
+ return "error";
649
+ }
650
+ }
651
+ if (authz.state === "denied") return "error";
652
+ if (link.state === "lost") return "error";
653
+ return "ready";
718
654
  }
719
- function normalizeWidgetInteraction(interaction) {
720
- if (!interaction || typeof interaction !== "object") return interaction;
721
- const raw = interaction;
722
- const access_level = typeof raw.access_level === "string" ? raw.access_level : typeof raw.accessLevel === "string" ? raw.accessLevel : void 0;
723
- const linking = parseGristWidgetLinkingInfo(raw.linking);
655
+ function deriveErrorMessage(snapshot) {
656
+ if (snapshot.lifecycle.phase === "terminated") {
657
+ switch (snapshot.lifecycle.reason.kind) {
658
+ case "ready_failed":
659
+ return snapshot.lifecycle.reason.message;
660
+ case "ready_timeout":
661
+ return `Grist handshake timed out after ${snapshot.lifecycle.reason.afterMs}ms`;
662
+ case "host_rejected":
663
+ return snapshot.lifecycle.reason.message;
664
+ case "script_timeout":
665
+ return `Grist plugin API never loaded (${snapshot.lifecycle.reason.attempts} attempts)`;
666
+ case "not_embedded":
667
+ return "This page is not embedded in a Grist iframe";
668
+ case "manual_terminate":
669
+ return null;
670
+ }
671
+ }
672
+ if (snapshot.authz.state === "denied") {
673
+ return snapshot.authz.message ?? `Grist denied requested access (${snapshot.authz.declared})`;
674
+ }
675
+ if (snapshot.lifecycle.phase === "online" && snapshot.link.state === "lost") {
676
+ return `Grist link lost (${snapshot.link.missedPings} consecutive failures)`;
677
+ }
678
+ return null;
679
+ }
680
+ function deriveCapabilities(snapshot) {
681
+ const { lifecycle, authz, link, config, sync } = snapshot;
682
+ const online = lifecycle.phase === "online";
683
+ const granted = authz.state === "granted";
684
+ const grantedReadOrMore = granted && ACCESS_RANK[authz.level] >= ACCESS_RANK["read table"];
685
+ const grantedFull = granted && authz.level === "full";
686
+ const linkUsable = link.state !== "lost";
687
+ const canRead = online && linkUsable && grantedReadOrMore;
688
+ const canFetchTable = canRead && link.state === "connected";
689
+ const mappingsUsable = hasUsableMappings(config.mappings);
690
+ const missing = mappingsMissing(config.mappings);
691
+ const emptyMultiples = mappingsEmptyMultiples(config.mappings);
692
+ const canRender = canRead && mappingsUsable;
693
+ const canWriteRecords = canRender && grantedFull;
694
+ const canWriteSchema = canWriteRecords && sync.currentTable.state === "known";
695
+ const hasFreshSelection = canRead && sync.record.state === "hot" && config.mappings.state !== "invalidating";
724
696
  return {
725
- ...interaction,
726
- ...access_level ? { access_level } : {},
727
- ...linking ? { linking } : {}
697
+ canRead,
698
+ canRender,
699
+ canWriteRecords,
700
+ canWriteSchema,
701
+ canFetchTable,
702
+ hasFreshSelection,
703
+ hasUsableMappings: mappingsUsable,
704
+ missingMappings: missing,
705
+ emptyMultipleMappings: emptyMultiples
728
706
  };
729
707
  }
730
-
731
- // src/sdk/internal/ready-singleton.ts
732
- var ACCESS_RANK2 = {
733
- none: 0,
734
- "read table": 1,
735
- full: 2
736
- };
737
- var inFlight = null;
738
- var lastAccess = null;
739
- var lastColumnNames = null;
740
- function columnNamesOf(columns) {
741
- return new Set((columns ?? []).map((c) => typeof c === "string" ? c : c.name));
742
- }
743
- function shouldReinit(next, nextColumns) {
744
- if (!lastAccess) return true;
745
- if (ACCESS_RANK2[next] > ACCESS_RANK2[lastAccess]) return true;
746
- for (const name of nextColumns) {
747
- if (!lastColumnNames?.has(name)) return true;
708
+ function hasUsableMappings(state) {
709
+ if (state.state === "not_declared") return true;
710
+ if (state.state === "complete") return true;
711
+ if (state.state === "invalidating") {
712
+ return state.previous.state === "complete";
748
713
  }
749
714
  return false;
750
715
  }
751
- function ensureGristReady(payload) {
752
- const access = payload.requiredAccess ?? "read table";
753
- const columns = columnNamesOf(payload.columns);
754
- if (inFlight && !shouldReinit(access, columns)) return inFlight;
755
- lastAccess = access;
756
- lastColumnNames = columns;
757
- const next = Promise.resolve().then(() => {
758
- const result = grist.ready(payload);
759
- return result instanceof Promise ? result : Promise.resolve();
760
- });
761
- inFlight = next.catch((err) => {
762
- inFlight = null;
763
- lastAccess = null;
764
- lastColumnNames = null;
765
- throw err;
766
- });
767
- return inFlight;
768
- }
769
- function resetGristReadySingleton() {
770
- inFlight = null;
771
- lastAccess = null;
772
- lastColumnNames = null;
773
- }
774
- var resetGristReadyCacheForTests = resetGristReadySingleton;
775
-
776
- // src/sdk/internal/subscriptions.ts
777
- var GristEvent = class {
778
- constructor(bindOnce) {
779
- this.bindOnce = bindOnce;
780
- this.listeners = /* @__PURE__ */ new Set();
781
- this.hasValue = false;
782
- this.bind = null;
783
- }
784
- subscribe(listener) {
785
- this.listeners.add(listener);
786
- if (!this.bind) {
787
- this.bind = () => {
788
- this.bind = () => {
789
- };
790
- try {
791
- this.bindOnce((value) => {
792
- this.hasValue = true;
793
- this.last = value;
794
- for (const l of this.listeners) l(value);
795
- });
796
- } catch (err) {
797
- if (typeof console !== "undefined") {
798
- console.error("[lib/grist] failed to subscribe to Grist event:", err);
799
- }
800
- }
801
- };
802
- this.bind();
803
- }
804
- if (this.hasValue && this.last !== void 0) {
805
- listener(this.last);
806
- }
807
- return () => {
808
- this.listeners.delete(listener);
809
- };
716
+ function mappingsMissing(state) {
717
+ if (state.state === "incomplete") return state.missing;
718
+ if (state.state === "invalidating" && state.previous.state === "incomplete") {
719
+ return state.previous.missing;
810
720
  }
811
- /** Last cached value (or `undefined` if no event has fired yet). */
812
- peek() {
813
- return this.last;
721
+ return EMPTY;
722
+ }
723
+ function mappingsEmptyMultiples(state) {
724
+ if (state.state === "incomplete") return state.emptyMultiples;
725
+ if (state.state === "invalidating" && state.previous.state === "incomplete") {
726
+ return state.previous.emptyMultiples;
814
727
  }
815
- /** True once at least one event has fired. */
816
- get ready() {
817
- return this.hasValue;
728
+ return EMPTY;
729
+ }
730
+ var EMPTY = Object.freeze([]);
731
+ function extractResolvedMappings(state) {
732
+ switch (state.state) {
733
+ case "complete":
734
+ case "incomplete":
735
+ return state.mappings;
736
+ case "invalidating":
737
+ if (state.previous.state === "complete" || state.previous.state === "incomplete") {
738
+ return state.previous.mappings;
739
+ }
740
+ return null;
741
+ default:
742
+ return null;
818
743
  }
819
- /**
820
- * Test helper: fan out a payload without calling `grist.on*`.
821
- * Used to simulate hosts that reuse one record object and mutate it in place.
822
- */
823
- deliverForTests(value) {
824
- this.hasValue = true;
825
- this.last = value;
826
- for (const l of this.listeners) l(value);
744
+ }
745
+ function deriveColumnMappingStatus(mappingState, columns) {
746
+ if (!columns?.length) {
747
+ return { ok: true, pending: false, missing: [], emptyMultiples: [] };
827
748
  }
828
- /**
829
- * Test helper: forget the bound state and cached value. Lets a test swap
830
- * `window.grist` (e.g. via the emulator's `dispose()` + `createGristEmulator()`)
831
- * and re-bind cleanly on the next subscribe.
832
- */
833
- _resetForTests() {
834
- this.listeners.clear();
835
- this.hasValue = false;
836
- this.last = void 0;
837
- this.bind = null;
749
+ switch (mappingState.state) {
750
+ case "not_declared":
751
+ return { ok: true, pending: false, missing: [], emptyMultiples: [] };
752
+ case "pending":
753
+ return pendingColumnMappingStatus;
754
+ case "unreported":
755
+ return pendingColumnMappingStatus;
756
+ case "complete":
757
+ return { ok: true, pending: false, missing: [], emptyMultiples: [] };
758
+ case "incomplete":
759
+ return {
760
+ ok: false,
761
+ pending: false,
762
+ missing: [...mappingState.missing],
763
+ emptyMultiples: [...mappingState.emptyMultiples]
764
+ };
765
+ case "invalidating":
766
+ return deriveColumnMappingStatus(mappingState.previous, columns);
838
767
  }
768
+ }
769
+
770
+ // src/sdk/internal/handshake-manager-context.ts
771
+ import { createContext, useContext, useEffect, useRef } from "react";
772
+
773
+ // src/sdk/internal/ready-singleton.ts
774
+ var ACCESS_RANK2 = {
775
+ none: 0,
776
+ "read table": 1,
777
+ full: 2
839
778
  };
840
- var recordEvent = new GristEvent((emit) => {
841
- const deliver = (record, mappings) => {
842
- emit({ record, mappings });
843
- };
844
- grist.onRecord(
845
- (record, mappings) => {
846
- deliver(record, mappings ?? null);
847
- },
848
- { keepEncoded: false }
849
- );
850
- const fetchSelectedRecord = grist.fetchSelectedRecord ?? grist.docApi?.fetchSelectedRecord;
851
- if (typeof grist.on !== "function" || typeof fetchSelectedRecord !== "function") {
852
- return;
853
- }
854
- let lastRowId;
855
- grist.on("message", async (msg) => {
856
- if (!msg || typeof msg !== "object") return;
857
- const host = msg;
858
- if (!host.tableId || typeof host.rowId !== "number") return;
859
- if (host.rowId === lastRowId) return;
860
- lastRowId = host.rowId;
861
- try {
862
- const record = await fetchSelectedRecord(host.rowId, { keepEncoded: false });
863
- deliver(record, recordEvent.peek()?.mappings ?? null);
864
- } catch (err) {
865
- if (typeof console !== "undefined") {
866
- console.error("[grist-widget-sdk] fetchSelectedRecord on cursor message failed:", err);
867
- }
868
- }
869
- });
870
- });
871
- var recordsEvent = new GristEvent((emit) => {
872
- grist.onRecords(
873
- (records, mappings) => {
874
- emit({ records, mappings: mappings ?? null });
875
- },
876
- { keepEncoded: false, format: "rows" }
877
- );
878
- });
879
- var optionsEvent = new GristEvent((emit) => {
880
- grist.onOptions((options, interaction) => {
881
- const normalized = normalizeWidgetInteraction(
882
- interaction ?? null
883
- );
884
- emit({
885
- options: options ? { ...options } : null,
886
- interaction: normalized
887
- });
779
+ var inFlight2 = null;
780
+ var lastAccess = null;
781
+ var lastColumnNames = null;
782
+ function columnNamesOf(columns) {
783
+ return new Set((columns ?? []).map((c) => typeof c === "string" ? c : c.name));
784
+ }
785
+ function shouldReinit(next, nextColumns) {
786
+ if (!lastAccess) return true;
787
+ if (ACCESS_RANK2[next] > ACCESS_RANK2[lastAccess]) return true;
788
+ for (const name of nextColumns) {
789
+ if (!lastColumnNames?.has(name)) return true;
790
+ }
791
+ return false;
792
+ }
793
+ function ensureGristReady(payload) {
794
+ const access = payload.requiredAccess ?? "read table";
795
+ const columns = columnNamesOf(payload.columns);
796
+ if (inFlight2 && !shouldReinit(access, columns)) return inFlight2;
797
+ lastAccess = access;
798
+ lastColumnNames = columns;
799
+ const next = Promise.resolve().then(() => {
800
+ const result = grist.ready(payload);
801
+ return result instanceof Promise ? result : Promise.resolve();
888
802
  });
889
- });
890
- var newRecordEvent = new GristEvent((emit) => {
891
- if (typeof grist.onNewRecord !== "function") return;
892
- grist.onNewRecord((mappings) => {
893
- emit({ mappings: mappings ?? null });
803
+ inFlight2 = next.catch((err) => {
804
+ inFlight2 = null;
805
+ lastAccess = null;
806
+ lastColumnNames = null;
807
+ throw err;
894
808
  });
895
- });
896
- function resetGristEventsForTests() {
897
- recordEvent._resetForTests();
898
- recordsEvent._resetForTests();
899
- optionsEvent._resetForTests();
900
- newRecordEvent._resetForTests();
809
+ return inFlight2;
810
+ }
811
+ function resetGristReadySingleton() {
812
+ inFlight2 = null;
813
+ lastAccess = null;
814
+ lastColumnNames = null;
815
+ }
816
+ var resetGristReadyCacheForTests = resetGristReadySingleton;
817
+
818
+ // src/sdk/internal/build-manager-options.ts
819
+ function buildManagerOptionsFromGristOptions(options) {
820
+ const opts = {
821
+ requiredAccess: options?.requiredAccess ?? "read table",
822
+ columns: options?.columns ?? [],
823
+ allowSelectBy: options?.allowSelectBy,
824
+ hasCustomOptions: options?.hasCustomOptions,
825
+ onEditOptions: options?.onEditOptions,
826
+ negotiate: {
827
+ readyImpl: (payload) => ensureGristReady(payload)
828
+ },
829
+ onBeforeReload: () => resetGristReadySingleton()
830
+ };
831
+ if (options?.availabilityMaxAttempts !== void 0 || options?.availabilityIntervalMs !== void 0) {
832
+ const interval = options?.availabilityIntervalMs ?? 1e3;
833
+ const max = options?.availabilityMaxAttempts ?? 10;
834
+ opts.detect = {
835
+ pollSequenceMs: Array.from({ length: max }, () => interval),
836
+ budgetMs: interval * max
837
+ };
838
+ }
839
+ return opts;
901
840
  }
902
841
 
903
842
  // src/sdk/internal/handshake/effects/detect.ts
@@ -1049,6 +988,154 @@ function startNegotiateEffect(env, dispatch, signal, payload, options = {}) {
1049
988
  }) };
1050
989
  }
1051
990
 
991
+ // src/sdk/helpers/grist-widget-interaction.ts
992
+ function parseGristWidgetLinkingInfo(raw) {
993
+ if (!raw || typeof raw !== "object" || !("asTarget" in raw)) return void 0;
994
+ const linking = raw;
995
+ return {
996
+ asTarget: linking.asTarget == null ? null : String(linking.asTarget),
997
+ asSource: Boolean(linking.asSource)
998
+ };
999
+ }
1000
+ function normalizeWidgetInteraction(interaction) {
1001
+ if (!interaction || typeof interaction !== "object") return interaction;
1002
+ const raw = interaction;
1003
+ const access_level = typeof raw.access_level === "string" ? raw.access_level : typeof raw.accessLevel === "string" ? raw.accessLevel : void 0;
1004
+ const linking = parseGristWidgetLinkingInfo(raw.linking);
1005
+ return {
1006
+ ...interaction,
1007
+ ...access_level ? { access_level } : {},
1008
+ ...linking ? { linking } : {}
1009
+ };
1010
+ }
1011
+
1012
+ // src/sdk/internal/subscriptions.ts
1013
+ var GristEvent = class {
1014
+ constructor(bindOnce) {
1015
+ this.bindOnce = bindOnce;
1016
+ this.listeners = /* @__PURE__ */ new Set();
1017
+ this.hasValue = false;
1018
+ this.bind = null;
1019
+ }
1020
+ subscribe(listener) {
1021
+ this.listeners.add(listener);
1022
+ if (!this.bind) {
1023
+ this.bind = () => {
1024
+ this.bind = () => {
1025
+ };
1026
+ try {
1027
+ this.bindOnce((value) => {
1028
+ this.hasValue = true;
1029
+ this.last = value;
1030
+ for (const l of this.listeners) l(value);
1031
+ });
1032
+ } catch (err) {
1033
+ if (typeof console !== "undefined") {
1034
+ console.error("[lib/grist] failed to subscribe to Grist event:", err);
1035
+ }
1036
+ }
1037
+ };
1038
+ this.bind();
1039
+ }
1040
+ if (this.hasValue && this.last !== void 0) {
1041
+ listener(this.last);
1042
+ }
1043
+ return () => {
1044
+ this.listeners.delete(listener);
1045
+ };
1046
+ }
1047
+ /** Last cached value (or `undefined` if no event has fired yet). */
1048
+ peek() {
1049
+ return this.last;
1050
+ }
1051
+ /** True once at least one event has fired. */
1052
+ get ready() {
1053
+ return this.hasValue;
1054
+ }
1055
+ /**
1056
+ * Test helper: fan out a payload without calling `grist.on*`.
1057
+ * Used to simulate hosts that reuse one record object and mutate it in place.
1058
+ */
1059
+ deliverForTests(value) {
1060
+ this.hasValue = true;
1061
+ this.last = value;
1062
+ for (const l of this.listeners) l(value);
1063
+ }
1064
+ /**
1065
+ * Test helper: forget the bound state and cached value. Lets a test swap
1066
+ * `window.grist` (e.g. via the emulator's `dispose()` + `createGristEmulator()`)
1067
+ * and re-bind cleanly on the next subscribe.
1068
+ */
1069
+ _resetForTests() {
1070
+ this.listeners.clear();
1071
+ this.hasValue = false;
1072
+ this.last = void 0;
1073
+ this.bind = null;
1074
+ }
1075
+ };
1076
+ var recordEvent = new GristEvent((emit) => {
1077
+ const deliver = (record, mappings) => {
1078
+ emit({ record, mappings });
1079
+ };
1080
+ grist.onRecord(
1081
+ (record, mappings) => {
1082
+ deliver(record, mappings ?? null);
1083
+ },
1084
+ { keepEncoded: false }
1085
+ );
1086
+ const fetchSelectedRecord = grist.fetchSelectedRecord ?? grist.docApi?.fetchSelectedRecord;
1087
+ if (typeof grist.on !== "function" || typeof fetchSelectedRecord !== "function") {
1088
+ return;
1089
+ }
1090
+ let lastRowId;
1091
+ grist.on("message", async (msg) => {
1092
+ if (!msg || typeof msg !== "object") return;
1093
+ const host = msg;
1094
+ if (!host.tableId || typeof host.rowId !== "number") return;
1095
+ if (host.rowId === lastRowId) return;
1096
+ lastRowId = host.rowId;
1097
+ try {
1098
+ const record = await fetchSelectedRecord(host.rowId, { keepEncoded: false });
1099
+ deliver(record, recordEvent.peek()?.mappings ?? null);
1100
+ } catch (err) {
1101
+ if (typeof console !== "undefined") {
1102
+ console.error("[grist-widget-sdk] fetchSelectedRecord on cursor message failed:", err);
1103
+ }
1104
+ }
1105
+ });
1106
+ });
1107
+ var recordsEvent = new GristEvent((emit) => {
1108
+ grist.onRecords(
1109
+ (records, mappings) => {
1110
+ emit({ records, mappings: mappings ?? null });
1111
+ },
1112
+ { keepEncoded: false, format: "rows" }
1113
+ );
1114
+ });
1115
+ var optionsEvent = new GristEvent((emit) => {
1116
+ grist.onOptions((options, interaction) => {
1117
+ const normalized = normalizeWidgetInteraction(
1118
+ interaction ?? null
1119
+ );
1120
+ emit({
1121
+ options: options ? { ...options } : null,
1122
+ interaction: normalized
1123
+ });
1124
+ });
1125
+ });
1126
+ var newRecordEvent = new GristEvent((emit) => {
1127
+ if (typeof grist.onNewRecord !== "function") return;
1128
+ grist.onNewRecord((mappings) => {
1129
+ emit({ mappings: mappings ?? null });
1130
+ });
1131
+ });
1132
+ function resetGristEventsForTests() {
1133
+ recordEvent._resetForTests();
1134
+ recordsEvent._resetForTests();
1135
+ optionsEvent._resetForTests();
1136
+ newRecordEvent._resetForTests();
1137
+ }
1138
+
1052
1139
  // src/sdk/internal/handshake/effects/subscriptions.ts
1053
1140
  function defaultSubscriptionsBinder(ctx) {
1054
1141
  const offs = [];
@@ -1200,7 +1287,7 @@ function startHeartbeatEffect(env, dispatch, signal, options = {}) {
1200
1287
  let cancelled = false;
1201
1288
  let paused = false;
1202
1289
  let intervalTimer = null;
1203
- let inFlight2 = false;
1290
+ let inFlight3 = false;
1204
1291
  let inFlightTimeoutTimer = null;
1205
1292
  const clearInterval = () => {
1206
1293
  if (intervalTimer !== null) {
@@ -1222,13 +1309,13 @@ function startHeartbeatEffect(env, dispatch, signal, options = {}) {
1222
1309
  const ping = () => {
1223
1310
  if (cancelled || paused) return;
1224
1311
  intervalTimer = null;
1225
- if (inFlight2) {
1312
+ if (inFlight3) {
1226
1313
  return;
1227
1314
  }
1228
- inFlight2 = true;
1315
+ inFlight3 = true;
1229
1316
  inFlightTimeoutTimer = env.setTimeout(() => {
1230
- if (!inFlight2) return;
1231
- inFlight2 = false;
1317
+ if (!inFlight3) return;
1318
+ inFlight3 = false;
1232
1319
  clearInFlightTimeout();
1233
1320
  if (!cancelled) {
1234
1321
  dispatch({ type: "HEARTBEAT_FAIL", atMs: env.now() });
@@ -1236,16 +1323,16 @@ function startHeartbeatEffect(env, dispatch, signal, options = {}) {
1236
1323
  }
1237
1324
  }, pingTimeoutMs);
1238
1325
  const handleOk = () => {
1239
- if (!inFlight2) return;
1240
- inFlight2 = false;
1326
+ if (!inFlight3) return;
1327
+ inFlight3 = false;
1241
1328
  clearInFlightTimeout();
1242
1329
  if (cancelled) return;
1243
1330
  dispatch({ type: "HEARTBEAT_OK", atMs: env.now() });
1244
1331
  scheduleNext();
1245
1332
  };
1246
1333
  const handleFail = () => {
1247
- if (!inFlight2) return;
1248
- inFlight2 = false;
1334
+ if (!inFlight3) return;
1335
+ inFlight3 = false;
1249
1336
  clearInFlightTimeout();
1250
1337
  if (cancelled) return;
1251
1338
  dispatch({ type: "HEARTBEAT_FAIL", atMs: env.now() });
@@ -1285,7 +1372,7 @@ function startHeartbeatEffect(env, dispatch, signal, options = {}) {
1285
1372
  cancelled = true;
1286
1373
  clearInterval();
1287
1374
  clearInFlightTimeout();
1288
- inFlight2 = false;
1375
+ inFlight3 = false;
1289
1376
  offVisibility?.();
1290
1377
  offOnline?.();
1291
1378
  });
@@ -1295,7 +1382,7 @@ function startHeartbeatEffect(env, dispatch, signal, options = {}) {
1295
1382
  cancelled = true;
1296
1383
  clearInterval();
1297
1384
  clearInFlightTimeout();
1298
- inFlight2 = false;
1385
+ inFlight3 = false;
1299
1386
  offVisibility?.();
1300
1387
  offOnline?.();
1301
1388
  },
@@ -1605,7 +1692,351 @@ function resetEmbeddedHandshakeManagerForTests() {
1605
1692
  }
1606
1693
  }
1607
1694
 
1695
+ // src/sdk/internal/handshake-manager-context.ts
1696
+ var GristHandshakeManagerContext = createContext(null);
1697
+ function useGristManagerOptional() {
1698
+ return useContext(GristHandshakeManagerContext);
1699
+ }
1700
+ function useGristManager(options) {
1701
+ const fromProvider = useContext(GristHandshakeManagerContext);
1702
+ const optionsRef = useRef(options);
1703
+ optionsRef.current = options;
1704
+ const embeddedRef = useRef(null);
1705
+ const ownsEmbeddedRef = useRef(false);
1706
+ if (fromProvider) {
1707
+ return fromProvider;
1708
+ }
1709
+ if (embeddedRef.current === null) {
1710
+ embeddedRef.current = acquireEmbeddedHandshakeManager(
1711
+ buildManagerOptionsFromGristOptions(optionsRef.current)
1712
+ );
1713
+ ownsEmbeddedRef.current = true;
1714
+ }
1715
+ useEffect(() => {
1716
+ if (!ownsEmbeddedRef.current) return;
1717
+ return () => {
1718
+ releaseEmbeddedHandshakeManager();
1719
+ embeddedRef.current = null;
1720
+ ownsEmbeddedRef.current = false;
1721
+ };
1722
+ }, []);
1723
+ return embeddedRef.current;
1724
+ }
1725
+
1726
+ // src/sdk/internal/hooks/use-grist-core.ts
1727
+ function computeAvailabilityFromSnapshot(snapshot) {
1728
+ const { phase } = snapshot.lifecycle;
1729
+ if (phase === "idle" || phase === "detecting") return null;
1730
+ if (phase === "terminated") {
1731
+ const reason = snapshot.lifecycle.reason.kind;
1732
+ if (reason === "not_embedded" || reason === "script_timeout") return false;
1733
+ return true;
1734
+ }
1735
+ return true;
1736
+ }
1737
+ function useGristCore(options) {
1738
+ const manager = useGristManager(options);
1739
+ return useGristCoreFromManager(manager);
1740
+ }
1741
+ function useGristCoreFromManager(manager) {
1742
+ const snapshot = useSyncExternalStore(
1743
+ manager.subscribe,
1744
+ manager.getSnapshot,
1745
+ initialGristSnapshot
1746
+ );
1747
+ const status = deriveStatus(snapshot);
1748
+ const error = deriveErrorMessage(snapshot);
1749
+ const docApi = snapshot.lifecycle.phase === "online" && typeof grist !== "undefined" ? grist.docApi ?? null : null;
1750
+ const isAvailable = computeAvailabilityFromSnapshot(snapshot);
1751
+ const reload = useCallback(async () => {
1752
+ manager.reload();
1753
+ }, [manager]);
1754
+ const capabilities = useMemo(() => deriveCapabilities(snapshot), [snapshot]);
1755
+ return useMemo(
1756
+ () => ({
1757
+ status,
1758
+ isAvailable,
1759
+ isReady: status === "ready",
1760
+ error,
1761
+ docApi,
1762
+ capabilities,
1763
+ reload
1764
+ }),
1765
+ [status, isAvailable, error, docApi, capabilities, reload]
1766
+ );
1767
+ }
1768
+
1769
+ // src/sdk/low-level-hooks/use-grist-ready.ts
1770
+ function useGristReady(options) {
1771
+ const { isAvailable, docApi, error, reload } = useGristCore(options);
1772
+ return { isAvailable, docApi, error, reload };
1773
+ }
1774
+
1775
+ // src/sdk/mid-level-hooks/use-grist-widget-options-from-context.ts
1776
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState } from "react";
1777
+
1778
+ // src/sdk/internal/contexts.tsx
1779
+ import { createContext as createContext2, useContext as useContext2, useMemo as useMemo2 } from "react";
1780
+
1781
+ // src/sdk/internal/merge-grist-result.ts
1782
+ function mergeGristResultFromSlices(slices) {
1783
+ const { core, selection, currentTable, writes, reads, widgetOptionsApi, linking, attachmentsRest, theme, sectionApi, suppressAlerts, allowSelectBy } = slices;
1784
+ return {
1785
+ status: core.status,
1786
+ isAvailable: core.isAvailable,
1787
+ isReady: core.isReady,
1788
+ error: core.error,
1789
+ capabilities: core.capabilities,
1790
+ record: selection.record,
1791
+ records: selection.records,
1792
+ mappedRecord: selection.mappedRecord,
1793
+ mappings: selection.mappings,
1794
+ recordsMappings: selection.recordsMappings,
1795
+ columnMappingStatus: selection.columnMappingStatus,
1796
+ mode: selection.mode,
1797
+ resolveMappedColumnId: selection.resolveMappedColumnId,
1798
+ mapBack: selection.mapBack,
1799
+ mapBackSkipped: selection.mapBackSkipped,
1800
+ isNewRecord: selection.isNewRecord,
1801
+ newRecordMappings: selection.newRecordMappings,
1802
+ currentTableId: currentTable.currentTableId,
1803
+ currentTableLoading: currentTable.currentTableLoading,
1804
+ refreshCurrentTable: currentTable.refreshCurrentTable,
1805
+ table: writes.table,
1806
+ getTable: writes.getTable,
1807
+ actionStatus: writes.actionStatus,
1808
+ actionError: writes.actionError,
1809
+ applyActions: writes.applyActions,
1810
+ reload: core.reload,
1811
+ fetchTable: reads.fetchTable,
1812
+ fetchTableRows: reads.fetchTableRows,
1813
+ fetchRow: reads.fetchRow,
1814
+ listTables: reads.listTables,
1815
+ listColumns: reads.listColumns,
1816
+ getDocName: reads.getDocName,
1817
+ buildReplicaDocumentFromDocApi: reads.buildReplicaDocumentFromDocApi,
1818
+ getAttachmentUrl: attachmentsRest.getAttachmentUrl,
1819
+ fetchAttachmentBase64: attachmentsRest.fetchAttachmentBase64,
1820
+ fetchAttachmentBlob: attachmentsRest.fetchAttachmentBlob,
1821
+ widgetOptions: selection.widgetOptions,
1822
+ widgetInteraction: selection.widgetInteraction,
1823
+ getWidgetOptions: widgetOptionsApi.getWidgetOptions,
1824
+ getWidgetOption: widgetOptionsApi.getWidgetOption,
1825
+ setWidgetOption: widgetOptionsApi.setWidgetOption,
1826
+ setWidgetOptions: widgetOptionsApi.setWidgetOptions,
1827
+ patchWidgetOptions: widgetOptionsApi.patchWidgetOptions,
1828
+ clearWidgetOptions: widgetOptionsApi.clearWidgetOptions,
1829
+ fetchSelectedTable: reads.fetchSelectedTable,
1830
+ fetchSelectedRecord: reads.fetchSelectedRecord,
1831
+ readError: reads.readError ?? attachmentsRest.readError,
1832
+ setCursorPosition: linking.setCursorPosition,
1833
+ setLinkedRowSelection: linking.setLinkedRowSelection,
1834
+ getAccessToken: attachmentsRest.getAccessToken,
1835
+ fetchWithAuth: attachmentsRest.fetchWithAuth,
1836
+ uploadAttachment: attachmentsRest.uploadAttachment,
1837
+ theme: theme.theme,
1838
+ configure: sectionApi.configure,
1839
+ refreshMappings: sectionApi.refreshMappings,
1840
+ suppressAlerts,
1841
+ allowSelectBy
1842
+ };
1843
+ }
1844
+
1845
+ // src/sdk/internal/contexts.tsx
1846
+ var GristCoreContext = createContext2(null);
1847
+ var GristSelectionContext = createContext2(null);
1848
+ var GristCurrentTableContext = createContext2(null);
1849
+ var GristWritesContext = createContext2(null);
1850
+ var GristReadsContext = createContext2(null);
1851
+ var GristWidgetOptionsApiContext = createContext2(null);
1852
+ var GristLinkingContext = createContext2(null);
1853
+ var GristAttachmentsRestContext = createContext2(null);
1854
+ var GristThemeContext = createContext2(null);
1855
+ var GristSectionApiContext = createContext2(null);
1856
+ function useRequiredContext(ctx, name) {
1857
+ const value = useContext2(ctx);
1858
+ if (!value) {
1859
+ throw new Error(`${name} must be used inside <GristWidgetProvider>`);
1860
+ }
1861
+ return value;
1862
+ }
1863
+ function useGristCoreContext() {
1864
+ return useRequiredContext(GristCoreContext, "useGristCoreContext()");
1865
+ }
1866
+ function useGristSelectionContext() {
1867
+ return useRequiredContext(GristSelectionContext, "useGristSelectionContext()");
1868
+ }
1869
+ function useGristCurrentTableContext() {
1870
+ return useRequiredContext(GristCurrentTableContext, "useGristCurrentTableContext()");
1871
+ }
1872
+ function useGristWritesContext() {
1873
+ return useRequiredContext(GristWritesContext, "useGristWritesContext()");
1874
+ }
1875
+ function useGristReadsContext() {
1876
+ return useRequiredContext(GristReadsContext, "useGristReadsContext()");
1877
+ }
1878
+ function useGristWidgetOptionsApiContext() {
1879
+ return useRequiredContext(GristWidgetOptionsApiContext, "useGristWidgetOptionsApiContext()");
1880
+ }
1881
+ function useGristLinkingContext() {
1882
+ return useRequiredContext(GristLinkingContext, "useGristLinkingContext()");
1883
+ }
1884
+ function useGristAttachmentsRestContext() {
1885
+ return useRequiredContext(GristAttachmentsRestContext, "useGristAttachmentsRestContext()");
1886
+ }
1887
+ function useGristThemeContext() {
1888
+ return useRequiredContext(GristThemeContext, "useGristThemeContext()");
1889
+ }
1890
+ function useGristSectionApiContext() {
1891
+ return useRequiredContext(GristSectionApiContext, "useGristSectionApiContext()");
1892
+ }
1893
+ function useGristFromProvider() {
1894
+ const core = useGristCoreContext();
1895
+ const selection = useGristSelectionContext();
1896
+ const currentTable = useGristCurrentTableContext();
1897
+ const writes = useGristWritesContext();
1898
+ const reads = useGristReadsContext();
1899
+ const widgetOptionsApi = useGristWidgetOptionsApiContext();
1900
+ const linking = useGristLinkingContext();
1901
+ const attachmentsRest = useGristAttachmentsRestContext();
1902
+ const theme = useGristThemeContext();
1903
+ const sectionApi = useGristSectionApiContext();
1904
+ return useMemo2(
1905
+ () => mergeGristResultFromSlices({
1906
+ core,
1907
+ selection,
1908
+ currentTable,
1909
+ writes,
1910
+ reads,
1911
+ widgetOptionsApi,
1912
+ linking,
1913
+ attachmentsRest,
1914
+ theme,
1915
+ sectionApi
1916
+ }),
1917
+ [
1918
+ core,
1919
+ selection,
1920
+ currentTable,
1921
+ writes,
1922
+ reads,
1923
+ widgetOptionsApi,
1924
+ linking,
1925
+ attachmentsRest,
1926
+ theme,
1927
+ sectionApi
1928
+ ]
1929
+ );
1930
+ }
1931
+
1932
+ // src/sdk/mid-level-hooks/use-grist-widget-options-from-context.ts
1933
+ function mergeWithDefaults(defaults, stored, namespace) {
1934
+ if (stored === null || stored === void 0) return { ...defaults };
1935
+ const source = namespace ? stored[namespace] : stored;
1936
+ if (!source || typeof source !== "object") return { ...defaults };
1937
+ return { ...defaults, ...source };
1938
+ }
1939
+ function useGristWidgetOptionsFromContext(config) {
1940
+ const debounceMs = config.debounceMs ?? 250;
1941
+ const selection = useGristSelectionContext();
1942
+ const optionsApi = useGristWidgetOptionsApiContext();
1943
+ const defaultsRef = useRef2(config.defaults);
1944
+ useEffect2(() => {
1945
+ defaultsRef.current = config.defaults;
1946
+ }, [config.defaults]);
1947
+ const rawOptions = selection.widgetOptions;
1948
+ const hasReceived = rawOptions !== null;
1949
+ const merged = useMemo3(
1950
+ () => mergeWithDefaults(defaultsRef.current, rawOptions, config.namespace),
1951
+ [rawOptions, config.namespace]
1952
+ );
1953
+ const [localOverride, setLocalOverride] = useState(null);
1954
+ const options = localOverride ?? merged;
1955
+ useEffect2(() => {
1956
+ setLocalOverride(null);
1957
+ }, [rawOptions]);
1958
+ const debounceRef = useRef2(null);
1959
+ const flushWrite = useCallback2(
1960
+ async (value) => {
1961
+ if (config.namespace) {
1962
+ await optionsApi.setWidgetOption(config.namespace, value);
1963
+ } else {
1964
+ await optionsApi.setWidgetOptions({ ...value });
1965
+ }
1966
+ },
1967
+ [config.namespace, optionsApi]
1968
+ );
1969
+ const scheduleWrite = useCallback2(
1970
+ (value) => {
1971
+ if (debounceRef.current) window.clearTimeout(debounceRef.current);
1972
+ debounceRef.current = window.setTimeout(() => {
1973
+ debounceRef.current = null;
1974
+ void flushWrite(value);
1975
+ }, debounceMs);
1976
+ },
1977
+ [debounceMs, flushWrite]
1978
+ );
1979
+ useEffect2(() => {
1980
+ return () => {
1981
+ if (debounceRef.current) window.clearTimeout(debounceRef.current);
1982
+ };
1983
+ }, []);
1984
+ const setOptions = useCallback2(
1985
+ (next) => {
1986
+ setLocalOverride(next);
1987
+ scheduleWrite(next);
1988
+ },
1989
+ [scheduleWrite]
1990
+ );
1991
+ const patchOptions = useCallback2(
1992
+ (patch) => {
1993
+ setLocalOverride((prev) => {
1994
+ const base = prev ?? merged;
1995
+ const next = { ...base, ...patch };
1996
+ scheduleWrite(next);
1997
+ return next;
1998
+ });
1999
+ },
2000
+ [merged, scheduleWrite]
2001
+ );
2002
+ const reset = useCallback2(async () => {
2003
+ if (debounceRef.current) {
2004
+ window.clearTimeout(debounceRef.current);
2005
+ debounceRef.current = null;
2006
+ }
2007
+ const next = { ...defaultsRef.current };
2008
+ setLocalOverride(null);
2009
+ await flushWrite(next);
2010
+ }, [flushWrite]);
2011
+ return useMemo3(
2012
+ () => ({
2013
+ options,
2014
+ loading: !hasReceived,
2015
+ setOptions,
2016
+ patchOptions,
2017
+ reset
2018
+ }),
2019
+ [options, hasReceived, setOptions, patchOptions, reset]
2020
+ );
2021
+ }
2022
+
2023
+ // src/sdk/internal/hooks/use-rpc-heartbeat-coalesce.ts
2024
+ import { useCallback as useCallback3 } from "react";
2025
+ function useRpcHeartbeatCoalesce() {
2026
+ const manager = useGristManagerOptional();
2027
+ return useCallback3(
2028
+ async (fn) => {
2029
+ const result = await fn();
2030
+ manager?.recordRpcSuccess();
2031
+ return result;
2032
+ },
2033
+ [manager]
2034
+ );
2035
+ }
2036
+
1608
2037
  export {
2038
+ getGristAccessToken,
2039
+ fetchWithGristAuth,
1609
2040
  validateColumnMappings,
1610
2041
  initialGristSnapshot,
1611
2042
  deriveStatus,
@@ -1617,14 +2048,37 @@ export {
1617
2048
  ensureGristReady,
1618
2049
  resetGristReadySingleton,
1619
2050
  resetGristReadyCacheForTests,
2051
+ buildManagerOptionsFromGristOptions,
1620
2052
  recordEvent,
1621
2053
  recordsEvent,
1622
2054
  optionsEvent,
1623
2055
  newRecordEvent,
1624
2056
  resetGristEventsForTests,
1625
2057
  createGristHandshakeManager,
1626
- acquireEmbeddedHandshakeManager,
1627
- releaseEmbeddedHandshakeManager,
1628
- resetEmbeddedHandshakeManagerForTests
2058
+ resetEmbeddedHandshakeManagerForTests,
2059
+ GristHandshakeManagerContext,
2060
+ useGristManagerOptional,
2061
+ useGristManager,
2062
+ mergeGristResultFromSlices,
2063
+ GristCoreContext,
2064
+ GristSelectionContext,
2065
+ GristCurrentTableContext,
2066
+ GristWritesContext,
2067
+ GristReadsContext,
2068
+ GristWidgetOptionsApiContext,
2069
+ GristLinkingContext,
2070
+ GristAttachmentsRestContext,
2071
+ GristThemeContext,
2072
+ GristSectionApiContext,
2073
+ useGristCoreContext,
2074
+ useGristSelectionContext,
2075
+ useGristCurrentTableContext,
2076
+ useGristWritesContext,
2077
+ useGristThemeContext,
2078
+ useGristFromProvider,
2079
+ useGristCore,
2080
+ useRpcHeartbeatCoalesce,
2081
+ useGristReady,
2082
+ useGristWidgetOptionsFromContext
1629
2083
  };
1630
- //# sourceMappingURL=chunk-TZB3OZA6.js.map
2084
+ //# sourceMappingURL=chunk-NO6FYX33.js.map