@colixsystems/widget-sdk 0.22.0 → 0.24.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 +19 -1
- package/dist/contract.cjs +128 -1
- package/dist/contract.js +128 -1
- package/dist/hooks.js +87 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,7 +45,25 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
45
45
|
|
|
46
46
|
## Status
|
|
47
47
|
|
|
48
|
-
`v0.
|
|
48
|
+
`v0.23.0` — 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**.
|
|
49
|
+
|
|
50
|
+
### What's new in 0.23.0
|
|
51
|
+
|
|
52
|
+
**Curated cross-platform package expansion to the vetted import allowlist (REQ-WSDK-PKG-EXPAND).**
|
|
53
|
+
|
|
54
|
+
- The linter's vetted import allowlist (`CONTRACT.vettedImports`) gains a curated set of popular React Native packages, each of which runs on **both** web and native (directly, or via a documented platform-split counterpart):
|
|
55
|
+
- **`react-native-reanimated`** (`web`/`native`) — declarative animations.
|
|
56
|
+
- **`react-native-gesture-handler`** (`web`/`native`) — native-driven touch gestures.
|
|
57
|
+
- **`react-native-safe-area-context`** (`web`/`native`) — safe-area insets.
|
|
58
|
+
- **`@shopify/flash-list`** (`web`/`native`) — high-performance virtualised list.
|
|
59
|
+
- **`react-native-paper`** (`web`/`native`) — Material Design components.
|
|
60
|
+
- **`react-native-vector-icons`** (`web`/`native`) — icon font families.
|
|
61
|
+
- **`@react-native-community/slider`** (`web`/`native`) — slider input.
|
|
62
|
+
- **`expo-linear-gradient`** (`web`/`native`) — the cross-platform gradient.
|
|
63
|
+
- **`lottie-react-native`** (`native`) + **`lottie-react`** (`web`) — Lottie animations, split-impl.
|
|
64
|
+
- **`react-native-webview`** (`native`) — embedded web content (pair with an `<iframe>` on web).
|
|
65
|
+
- **Parity (CLAUDE.md §3):** the compiler now pins the native-module members in the exported Expo app's `package.json` and emits the **`react-native-reanimated/plugin`** in `babel.config.js` **unconditionally** (previously only for the sidebar-drawer shell), so a baked widget that imports any of these resolves and bundles on native exactly as it renders in the web Player.
|
|
66
|
+
- **`CONTRACT.version` → `1.13.0`** (additive: new vetted import entries only). No existing export, type, manifest field, hook, or banned-API list changed.
|
|
49
67
|
|
|
50
68
|
### What's new in 0.22.0
|
|
51
69
|
|
package/dist/contract.cjs
CHANGED
|
@@ -314,6 +314,31 @@ const HOOKS = [
|
|
|
314
314
|
requiredContextSlice: ["datastore.records"],
|
|
315
315
|
scopes: ["acl.write:records"],
|
|
316
316
|
},
|
|
317
|
+
// REQ-RT-07 — realtime table subscription.
|
|
318
|
+
{
|
|
319
|
+
name: "useDatastoreSubscription",
|
|
320
|
+
signature: "useDatastoreSubscription(tableId, handlers, options?)",
|
|
321
|
+
description:
|
|
322
|
+
"Subscribe to a table's realtime change stream via the injected " +
|
|
323
|
+
"datastore-client at ctx.datastore.records(tableId).subscribe(...). " +
|
|
324
|
+
"handlers is { onCreated?, onUpdated?, onDeleted? }; each receives the " +
|
|
325
|
+
"snake_case record verbatim (same shape REST returns). The socket opens " +
|
|
326
|
+
"on mount, re-opens when tableId changes, and is torn down on unmount. " +
|
|
327
|
+
"Returns { status } where status is 'connecting' | 'live' | " +
|
|
328
|
+
"'reconnecting' | 'fallback'. The server gates each subscribe by the " +
|
|
329
|
+
"same read ACL REST honours; on an ACL reject, a missing WebSocket, or a " +
|
|
330
|
+
"host whose client predates realtime, the hook resolves to " +
|
|
331
|
+
"{ status: 'fallback' } WITHOUT throwing so the widget can poll instead. " +
|
|
332
|
+
"Reads the same datastore.read scope as useDatastoreQuery — declare " +
|
|
333
|
+
"datastore.read for the table you subscribe to. Pair with " +
|
|
334
|
+
"useDatastoreQuery for the initial load and merge the streamed envelopes.",
|
|
335
|
+
returnShape: {
|
|
336
|
+
status:
|
|
337
|
+
"'connecting' | 'live' | 'reconnecting' | 'fallback' // transport state; 'fallback' → poll",
|
|
338
|
+
},
|
|
339
|
+
requiredContextSlice: ["datastore.records"],
|
|
340
|
+
scopes: ["datastore.read:<table>"],
|
|
341
|
+
},
|
|
317
342
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
318
343
|
{
|
|
319
344
|
name: "useClipboard",
|
|
@@ -913,6 +938,89 @@ const VETTED_IMPORTS = [
|
|
|
913
938
|
description:
|
|
914
939
|
"Native haptic feedback. Pair with navigator.vibrate in widget.web.jsx.",
|
|
915
940
|
},
|
|
941
|
+
// REQ-WSDK-PKG-EXPAND — curated cross-platform package expansion. Each entry
|
|
942
|
+
// below runs on web AND native (directly, or via the documented platform-split
|
|
943
|
+
// counterpart). Native-module packages additionally carry a pinned version in
|
|
944
|
+
// the compiler's generatePackageJson so a baked marketplace widget resolves in
|
|
945
|
+
// the Expo export (CLAUDE.md §3 parity). See the `adding-vetted-packages` skill
|
|
946
|
+
// for the full add checklist (contract ×2, version bump, compiler pin, docs).
|
|
947
|
+
{
|
|
948
|
+
specifier: "react-native-reanimated",
|
|
949
|
+
platforms: ["web", "native"],
|
|
950
|
+
category: "animation",
|
|
951
|
+
description:
|
|
952
|
+
"Declarative, performant animations. Works on both platforms; the export always emits the required react-native-reanimated/plugin in babel.config.js so a widget using worklets bundles on native.",
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
specifier: "react-native-gesture-handler",
|
|
956
|
+
platforms: ["web", "native"],
|
|
957
|
+
category: "gesture",
|
|
958
|
+
description:
|
|
959
|
+
"Native-driven touch gestures (pan, pinch, swipe, long-press). Works on both platforms; wrap interactive trees in GestureHandlerRootView.",
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
specifier: "react-native-safe-area-context",
|
|
963
|
+
platforms: ["web", "native"],
|
|
964
|
+
category: "layout",
|
|
965
|
+
description:
|
|
966
|
+
"Safe-area insets (notch / status bar). useSafeAreaInsets() returns zeros on web and the real insets on native, so the same layout code is safe on both.",
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
specifier: "@shopify/flash-list",
|
|
970
|
+
platforms: ["web", "native"],
|
|
971
|
+
category: "list",
|
|
972
|
+
description:
|
|
973
|
+
"High-performance virtualised list with a FlatList-compatible API. Prefer it over FlatList for long datasets; renders on both platforms.",
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
specifier: "react-native-paper",
|
|
977
|
+
platforms: ["web", "native"],
|
|
978
|
+
category: "ui",
|
|
979
|
+
description:
|
|
980
|
+
"Material Design component library built on react-native + react-native-web, so the same components render identically on both platforms.",
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
specifier: "react-native-vector-icons",
|
|
984
|
+
platforms: ["web", "native"],
|
|
985
|
+
category: "iconography",
|
|
986
|
+
description:
|
|
987
|
+
"Icon font families (MaterialIcons, FontAwesome, Ionicons, …). Works on both platforms; prefer the SDK's <Icon> (lucide) primitive for the common case.",
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
specifier: "@react-native-community/slider",
|
|
991
|
+
platforms: ["web", "native"],
|
|
992
|
+
category: "input",
|
|
993
|
+
description:
|
|
994
|
+
"Cross-platform slider input. Controlled — value + onValueChange. Works on both platforms.",
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
specifier: "expo-linear-gradient",
|
|
998
|
+
platforms: ["web", "native"],
|
|
999
|
+
category: "drawing",
|
|
1000
|
+
description:
|
|
1001
|
+
"Linear-gradient fill (<LinearGradient colors={[...]} />). The cross-platform gradient — works on both platforms (react-native-linear-gradient is native-only; prefer this).",
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
specifier: "lottie-react-native",
|
|
1005
|
+
platforms: ["native"],
|
|
1006
|
+
category: "animation",
|
|
1007
|
+
description:
|
|
1008
|
+
"After Effects (Bodymovin) JSON animations. Native-only; pair with lottie-react in widget.web.jsx for the web variant.",
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
specifier: "lottie-react",
|
|
1012
|
+
platforms: ["web"],
|
|
1013
|
+
category: "animation",
|
|
1014
|
+
description:
|
|
1015
|
+
"Web Lottie player. Web-only counterpart to lottie-react-native; bundle it into widget.web.jsx.",
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
specifier: "react-native-webview",
|
|
1019
|
+
platforms: ["native"],
|
|
1020
|
+
category: "media",
|
|
1021
|
+
description:
|
|
1022
|
+
"Embeds web content in a native WebView. Native-only; on web render an <iframe> (or a View with the URL) in widget.web.jsx.",
|
|
1023
|
+
},
|
|
916
1024
|
];
|
|
917
1025
|
|
|
918
1026
|
// Back-compat shape — every existing consumer (widgetLoader, the static
|
|
@@ -1042,7 +1150,26 @@ const CONTRACT = deepFreeze({
|
|
|
1042
1150
|
// cascading dropdowns; tenant-copy remaps `tableId` and nulls `recordId`
|
|
1043
1151
|
// (records are business data, never copied). No existing type changed
|
|
1044
1152
|
// shape, so this is additive — minor bump on the pre-1.0 channel.
|
|
1045
|
-
|
|
1153
|
+
//
|
|
1154
|
+
// 1.13.0: additive (REQ-WSDK-PKG-EXPAND) — the vetted import allowlist gains
|
|
1155
|
+
// a curated cross-platform package set: react-native-reanimated,
|
|
1156
|
+
// react-native-gesture-handler, react-native-safe-area-context,
|
|
1157
|
+
// @shopify/flash-list, react-native-paper, react-native-vector-icons,
|
|
1158
|
+
// @react-native-community/slider, expo-linear-gradient, react-native-webview,
|
|
1159
|
+
// and lottie-react-native (+ lottie-react as its web counterpart). No
|
|
1160
|
+
// existing entry changed shape and no other contract field moved, so this
|
|
1161
|
+
// is additive — minor bump on the pre-1.0 channel. The compiler pins the
|
|
1162
|
+
// native-module members in the exported Expo app's package.json and now
|
|
1163
|
+
// always emits the react-native-reanimated/plugin (CLAUDE.md §3 parity).
|
|
1164
|
+
//
|
|
1165
|
+
// 1.14.0: additive (REQ-RT-07) — new `useDatastoreSubscription(tableId,
|
|
1166
|
+
// handlers, options?)` hook reading ctx.datastore.records(t).subscribe.
|
|
1167
|
+
// Backed by the datastore-client 0.6.0 `subscribe` method (WebSocket to
|
|
1168
|
+
// `/datastore/ws`). Safe-by-default: resolves to { status: 'fallback' }
|
|
1169
|
+
// when the host's client predates realtime or no WebSocket is available,
|
|
1170
|
+
// so widgets degrade to polling on both platforms. No existing hook,
|
|
1171
|
+
// primitive, or contract field changed — minor bump on the pre-1.0 channel.
|
|
1172
|
+
version: "1.14.0",
|
|
1046
1173
|
hooks: HOOKS,
|
|
1047
1174
|
primitives: PRIMITIVES,
|
|
1048
1175
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/contract.js
CHANGED
|
@@ -321,6 +321,31 @@ const HOOKS = [
|
|
|
321
321
|
requiredContextSlice: ["datastore.records"],
|
|
322
322
|
scopes: ["acl.write:records"],
|
|
323
323
|
},
|
|
324
|
+
// REQ-RT-07 — realtime table subscription.
|
|
325
|
+
{
|
|
326
|
+
name: "useDatastoreSubscription",
|
|
327
|
+
signature: "useDatastoreSubscription(tableId, handlers, options?)",
|
|
328
|
+
description:
|
|
329
|
+
"Subscribe to a table's realtime change stream via the injected " +
|
|
330
|
+
"datastore-client at ctx.datastore.records(tableId).subscribe(...). " +
|
|
331
|
+
"handlers is { onCreated?, onUpdated?, onDeleted? }; each receives the " +
|
|
332
|
+
"snake_case record verbatim (same shape REST returns). The socket opens " +
|
|
333
|
+
"on mount, re-opens when tableId changes, and is torn down on unmount. " +
|
|
334
|
+
"Returns { status } where status is 'connecting' | 'live' | " +
|
|
335
|
+
"'reconnecting' | 'fallback'. The server gates each subscribe by the " +
|
|
336
|
+
"same read ACL REST honours; on an ACL reject, a missing WebSocket, or a " +
|
|
337
|
+
"host whose client predates realtime, the hook resolves to " +
|
|
338
|
+
"{ status: 'fallback' } WITHOUT throwing so the widget can poll instead. " +
|
|
339
|
+
"Reads the same datastore.read scope as useDatastoreQuery — declare " +
|
|
340
|
+
"datastore.read for the table you subscribe to. Pair with " +
|
|
341
|
+
"useDatastoreQuery for the initial load and merge the streamed envelopes.",
|
|
342
|
+
returnShape: {
|
|
343
|
+
status:
|
|
344
|
+
"'connecting' | 'live' | 'reconnecting' | 'fallback' // transport state; 'fallback' → poll",
|
|
345
|
+
},
|
|
346
|
+
requiredContextSlice: ["datastore.records"],
|
|
347
|
+
scopes: ["datastore.read:<table>"],
|
|
348
|
+
},
|
|
324
349
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks.
|
|
325
350
|
{
|
|
326
351
|
name: "useClipboard",
|
|
@@ -896,6 +921,89 @@ const VETTED_IMPORTS = [
|
|
|
896
921
|
description:
|
|
897
922
|
"Native haptic feedback. Pair with navigator.vibrate in widget.web.jsx.",
|
|
898
923
|
},
|
|
924
|
+
// REQ-WSDK-PKG-EXPAND — curated cross-platform package expansion. Each entry
|
|
925
|
+
// below runs on web AND native (directly, or via the documented platform-split
|
|
926
|
+
// counterpart). Native-module packages additionally carry a pinned version in
|
|
927
|
+
// the compiler's generatePackageJson so a baked marketplace widget resolves in
|
|
928
|
+
// the Expo export (CLAUDE.md §3 parity). See the `adding-vetted-packages` skill
|
|
929
|
+
// for the full add checklist (contract ×2, version bump, compiler pin, docs).
|
|
930
|
+
{
|
|
931
|
+
specifier: "react-native-reanimated",
|
|
932
|
+
platforms: ["web", "native"],
|
|
933
|
+
category: "animation",
|
|
934
|
+
description:
|
|
935
|
+
"Declarative, performant animations. Works on both platforms; the export always emits the required react-native-reanimated/plugin in babel.config.js so a widget using worklets bundles on native.",
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
specifier: "react-native-gesture-handler",
|
|
939
|
+
platforms: ["web", "native"],
|
|
940
|
+
category: "gesture",
|
|
941
|
+
description:
|
|
942
|
+
"Native-driven touch gestures (pan, pinch, swipe, long-press). Works on both platforms; wrap interactive trees in GestureHandlerRootView.",
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
specifier: "react-native-safe-area-context",
|
|
946
|
+
platforms: ["web", "native"],
|
|
947
|
+
category: "layout",
|
|
948
|
+
description:
|
|
949
|
+
"Safe-area insets (notch / status bar). useSafeAreaInsets() returns zeros on web and the real insets on native, so the same layout code is safe on both.",
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
specifier: "@shopify/flash-list",
|
|
953
|
+
platforms: ["web", "native"],
|
|
954
|
+
category: "list",
|
|
955
|
+
description:
|
|
956
|
+
"High-performance virtualised list with a FlatList-compatible API. Prefer it over FlatList for long datasets; renders on both platforms.",
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
specifier: "react-native-paper",
|
|
960
|
+
platforms: ["web", "native"],
|
|
961
|
+
category: "ui",
|
|
962
|
+
description:
|
|
963
|
+
"Material Design component library built on react-native + react-native-web, so the same components render identically on both platforms.",
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
specifier: "react-native-vector-icons",
|
|
967
|
+
platforms: ["web", "native"],
|
|
968
|
+
category: "iconography",
|
|
969
|
+
description:
|
|
970
|
+
"Icon font families (MaterialIcons, FontAwesome, Ionicons, …). Works on both platforms; prefer the SDK's <Icon> (lucide) primitive for the common case.",
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
specifier: "@react-native-community/slider",
|
|
974
|
+
platforms: ["web", "native"],
|
|
975
|
+
category: "input",
|
|
976
|
+
description:
|
|
977
|
+
"Cross-platform slider input. Controlled — value + onValueChange. Works on both platforms.",
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
specifier: "expo-linear-gradient",
|
|
981
|
+
platforms: ["web", "native"],
|
|
982
|
+
category: "drawing",
|
|
983
|
+
description:
|
|
984
|
+
"Linear-gradient fill (<LinearGradient colors={[...]} />). The cross-platform gradient — works on both platforms (react-native-linear-gradient is native-only; prefer this).",
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
specifier: "lottie-react-native",
|
|
988
|
+
platforms: ["native"],
|
|
989
|
+
category: "animation",
|
|
990
|
+
description:
|
|
991
|
+
"After Effects (Bodymovin) JSON animations. Native-only; pair with lottie-react in widget.web.jsx for the web variant.",
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
specifier: "lottie-react",
|
|
995
|
+
platforms: ["web"],
|
|
996
|
+
category: "animation",
|
|
997
|
+
description:
|
|
998
|
+
"Web Lottie player. Web-only counterpart to lottie-react-native; bundle it into widget.web.jsx.",
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
specifier: "react-native-webview",
|
|
1002
|
+
platforms: ["native"],
|
|
1003
|
+
category: "media",
|
|
1004
|
+
description:
|
|
1005
|
+
"Embeds web content in a native WebView. Native-only; on web render an <iframe> (or a View with the URL) in widget.web.jsx.",
|
|
1006
|
+
},
|
|
899
1007
|
];
|
|
900
1008
|
|
|
901
1009
|
const ALLOWED_BARE_IMPORTS = VETTED_IMPORTS.map((v) => v.specifier);
|
|
@@ -995,7 +1103,26 @@ const CONTRACT = deepFreeze({
|
|
|
995
1103
|
// cascading dropdowns; tenant-copy remaps `tableId` and nulls `recordId`
|
|
996
1104
|
// (records are business data, never copied). No existing type changed
|
|
997
1105
|
// shape, so this is additive — minor bump on the pre-1.0 channel.
|
|
998
|
-
|
|
1106
|
+
//
|
|
1107
|
+
// 1.13.0: additive (REQ-WSDK-PKG-EXPAND) — the vetted import allowlist gains
|
|
1108
|
+
// a curated cross-platform package set: react-native-reanimated,
|
|
1109
|
+
// react-native-gesture-handler, react-native-safe-area-context,
|
|
1110
|
+
// @shopify/flash-list, react-native-paper, react-native-vector-icons,
|
|
1111
|
+
// @react-native-community/slider, expo-linear-gradient, react-native-webview,
|
|
1112
|
+
// and lottie-react-native (+ lottie-react as its web counterpart). No
|
|
1113
|
+
// existing entry changed shape and no other contract field moved, so this
|
|
1114
|
+
// is additive — minor bump on the pre-1.0 channel. The compiler pins the
|
|
1115
|
+
// native-module members in the exported Expo app's package.json and now
|
|
1116
|
+
// always emits the react-native-reanimated/plugin (CLAUDE.md §3 parity).
|
|
1117
|
+
//
|
|
1118
|
+
// 1.14.0: additive (REQ-RT-07) — new `useDatastoreSubscription(tableId,
|
|
1119
|
+
// handlers, options?)` hook reading ctx.datastore.records(t).subscribe.
|
|
1120
|
+
// Backed by the datastore-client 0.6.0 `subscribe` method (WebSocket to
|
|
1121
|
+
// `/datastore/ws`). Safe-by-default: resolves to { status: 'fallback' }
|
|
1122
|
+
// when the host's client predates realtime or no WebSocket is available,
|
|
1123
|
+
// so widgets degrade to polling on both platforms. No existing hook,
|
|
1124
|
+
// primitive, or contract field changed — minor bump on the pre-1.0 channel.
|
|
1125
|
+
version: "1.14.0",
|
|
999
1126
|
hooks: HOOKS,
|
|
1000
1127
|
primitives: PRIMITIVES,
|
|
1001
1128
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/hooks.js
CHANGED
|
@@ -921,6 +921,93 @@ export function useRecordPermissions(tableId, recordId) {
|
|
|
921
921
|
return { permissions, loading, error, grant, revoke, update, refetch };
|
|
922
922
|
}
|
|
923
923
|
|
|
924
|
+
/**
|
|
925
|
+
* REQ-RT-07: subscribe to a table's realtime change stream. Reads
|
|
926
|
+
* `ctx.datastore.records(table).subscribe({ onCreated, onUpdated, onDeleted,
|
|
927
|
+
* onStatus })` and wires it to a React lifecycle: the socket is opened on
|
|
928
|
+
* mount (and re-opened when `table` changes) and torn down on unmount via the
|
|
929
|
+
* unsubscribe function the client returns. The author's `handlers` callbacks
|
|
930
|
+
* are held in a ref so re-renders with fresh callback identities never tear
|
|
931
|
+
* the socket down — only a `table` change does.
|
|
932
|
+
*
|
|
933
|
+
* Returns `{ status }` where status is "connecting" | "live" | "reconnecting"
|
|
934
|
+
* | "fallback". A widget typically pairs this with `useDatastoreQuery` for the
|
|
935
|
+
* initial load and merges the streamed create/update/delete envelopes into its
|
|
936
|
+
* local list; when `status === "fallback"` (no socket support on the host, the
|
|
937
|
+
* subscribe was ACL-rejected, or the connect timed out) the widget should run
|
|
938
|
+
* its own REST polling instead.
|
|
939
|
+
*
|
|
940
|
+
* Cross-platform + safe-by-default: if the host's datastore client doesn't
|
|
941
|
+
* expose `subscribe` (an older host, or a runtime with no WebSocket), the hook
|
|
942
|
+
* resolves to `{ status: "fallback" }` WITHOUT throwing, so a widget that
|
|
943
|
+
* subscribes degrades to polling on both the web Player and the native export
|
|
944
|
+
* rather than crashing at render (CLAUDE.md §11).
|
|
945
|
+
*
|
|
946
|
+
* @param {string} table Bound table id (falsy → no subscription, status "fallback").
|
|
947
|
+
* @param {{ onCreated?, onUpdated?, onDeleted? }} [handlers] Per-event callbacks; each receives the snake_case record.
|
|
948
|
+
* @param {{ fallbackAfterMs?: number }} [options]
|
|
949
|
+
* @returns {{ status: "connecting" | "live" | "reconnecting" | "fallback" }}
|
|
950
|
+
*/
|
|
951
|
+
export function useDatastoreSubscription(table, handlers, options) {
|
|
952
|
+
const ctx = useWidgetContextOrThrow("useDatastoreSubscription");
|
|
953
|
+
const [status, setStatus] = useState("connecting");
|
|
954
|
+
|
|
955
|
+
// Author callbacks live in a ref so a new closure each render does not
|
|
956
|
+
// re-run the effect (which would tear down + re-open the socket).
|
|
957
|
+
const handlersRef = useRef(handlers);
|
|
958
|
+
handlersRef.current = handlers;
|
|
959
|
+
// `ctx` is a fresh object identity per host render; hold the records
|
|
960
|
+
// factory in a ref so the effect depends only on `table`.
|
|
961
|
+
const recordsRef = useRef(
|
|
962
|
+
ctx.datastore && typeof ctx.datastore.records === "function"
|
|
963
|
+
? ctx.datastore.records
|
|
964
|
+
: null,
|
|
965
|
+
);
|
|
966
|
+
recordsRef.current =
|
|
967
|
+
ctx.datastore && typeof ctx.datastore.records === "function"
|
|
968
|
+
? ctx.datastore.records
|
|
969
|
+
: null;
|
|
970
|
+
|
|
971
|
+
const fallbackAfterMs =
|
|
972
|
+
options && Number.isFinite(options.fallbackAfterMs)
|
|
973
|
+
? options.fallbackAfterMs
|
|
974
|
+
: undefined;
|
|
975
|
+
|
|
976
|
+
useEffect(() => {
|
|
977
|
+
if (!table || typeof recordsRef.current !== "function") {
|
|
978
|
+
setStatus("fallback");
|
|
979
|
+
return undefined;
|
|
980
|
+
}
|
|
981
|
+
let ns;
|
|
982
|
+
try {
|
|
983
|
+
ns = recordsRef.current(table);
|
|
984
|
+
} catch (_) {
|
|
985
|
+
setStatus("fallback");
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
if (!ns || typeof ns.subscribe !== "function") {
|
|
989
|
+
// Host's datastore client predates realtime — degrade to polling.
|
|
990
|
+
setStatus("fallback");
|
|
991
|
+
return undefined;
|
|
992
|
+
}
|
|
993
|
+
const stop = ns.subscribe(
|
|
994
|
+
{
|
|
995
|
+
onCreated: (r) => handlersRef.current?.onCreated?.(r),
|
|
996
|
+
onUpdated: (r) => handlersRef.current?.onUpdated?.(r),
|
|
997
|
+
onDeleted: (r) => handlersRef.current?.onDeleted?.(r),
|
|
998
|
+
onStatus: (s) => setStatus(s),
|
|
999
|
+
},
|
|
1000
|
+
fallbackAfterMs != null ? { fallbackAfterMs } : undefined,
|
|
1001
|
+
);
|
|
1002
|
+
return () => {
|
|
1003
|
+
if (typeof stop === "function") stop();
|
|
1004
|
+
};
|
|
1005
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1006
|
+
}, [table, fallbackAfterMs]);
|
|
1007
|
+
|
|
1008
|
+
return { status };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
924
1011
|
/* ============================================================================
|
|
925
1012
|
* FILES CLIENT — ctx.files (@colixsystems/files-client)
|
|
926
1013
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -624,6 +624,37 @@ export function useDatastoreSchema(
|
|
|
624
624
|
tableId: string | null | undefined,
|
|
625
625
|
): SchemaResult;
|
|
626
626
|
|
|
627
|
+
// REQ-RT-07 realtime subscription transport state.
|
|
628
|
+
export type DatastoreSubscriptionStatus =
|
|
629
|
+
| "connecting"
|
|
630
|
+
| "live"
|
|
631
|
+
| "reconnecting"
|
|
632
|
+
| "fallback";
|
|
633
|
+
|
|
634
|
+
export interface DatastoreSubscriptionHandlers {
|
|
635
|
+
onCreated?: (record: Record<string, unknown>) => void;
|
|
636
|
+
onUpdated?: (record: Record<string, unknown>) => void;
|
|
637
|
+
onDeleted?: (record: Record<string, unknown>) => void;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export interface DatastoreSubscriptionOptions {
|
|
641
|
+
fallbackAfterMs?: number;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* REQ-RT-07: subscribe to a table's realtime change stream via
|
|
646
|
+
* `ctx.datastore.records(tableId).subscribe(...)`. Opens on mount, re-opens on
|
|
647
|
+
* `tableId` change, tears down on unmount. Returns `{ status }`; when status
|
|
648
|
+
* is `"fallback"` (no socket support, ACL-rejected, or connect timed out) run
|
|
649
|
+
* REST polling instead. Never throws — degrades to `{ status: "fallback" }` on
|
|
650
|
+
* a host whose datastore client predates realtime.
|
|
651
|
+
*/
|
|
652
|
+
export function useDatastoreSubscription(
|
|
653
|
+
tableId: string | null | undefined,
|
|
654
|
+
handlers?: DatastoreSubscriptionHandlers,
|
|
655
|
+
options?: DatastoreSubscriptionOptions,
|
|
656
|
+
): { status: DatastoreSubscriptionStatus };
|
|
657
|
+
|
|
627
658
|
/**
|
|
628
659
|
* Stateful file-asset resolver hook. Returns `{ url, file, loading, error,
|
|
629
660
|
* refetch }`. The `url` is an absolute URL composed against the host's API
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/build.js",
|
|
38
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js"
|
|
38
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|