@ossy/sdk-react 1.40.2 → 1.40.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # React bindings
2
2
 
3
- Thin React layer over `@ossy/sdk`. One hook — **`useSdk()`** — for reads, writes, and cache invalidation.
3
+ Thin React layer over `@ossy/sdk`. One hook — **`useSdk()`** — for reads, writes, cache invalidation, and optimistic updates.
4
4
 
5
5
  ## Getting started
6
6
 
7
7
  ```bash
8
- npm install @ossy/sdk-react @ossy/sdk
8
+ npm install @ossy/sdk-react @ossy/sdk @ossy/fold
9
9
  ```
10
10
 
11
11
  ```jsx
@@ -45,17 +45,24 @@ function MyComponent() {
45
45
  }
46
46
  ```
47
47
 
48
+ `WorkspaceProvider` subscribes to SSE push invalidation automatically (ADR 0008 §9) and drops stale read-cache keys when the server emits `invalidate`.
49
+
48
50
  ## API
49
51
 
50
52
  | Export | Purpose |
51
53
  |---|---|
52
- | `WorkspaceProvider` | Provides SDK + shared read cache |
53
- | `useSdk()` | Returns `{ invoke, read, invalidate, cacheKey, sdk }` |
54
+ | `WorkspaceProvider` | Provides SDK + shared read cache + SSE invalidation |
55
+ | `useSdk()` | Returns `{ invoke, invokeOptimistic, read, invalidate, cacheKey, sdk }` |
54
56
  | `useRead()` | Same as `sdk.read` — hook for reactive reads |
57
+ | `usePushInvalidation()` | Wire SSE push to a read-cache store (used by `WorkspaceProvider`) |
55
58
  | `cacheKey(action, payload)` | Stable cache key for an action + payload |
56
- | `ReactSdk` | Browser SDK with `invoke(action, payload)` |
59
+ | `projectionKey(projectionId, scopeId)` | Cache key for projection reads |
60
+ | `applyOptimisticResource()` | Fold a pending event into cached resource state |
61
+ | `rollbackOptimisticResource()` | Restore cache entry after failed optimistic invoke |
62
+ | `useReadCacheStore()` | Access the shared read-cache store inside `<Cache>` |
63
+ | `ReactSdk` | Browser SDK with `invoke` and `subscribePush` |
57
64
  | `ActionRef` | Platform action POJO type `{ id, access? }` |
58
- | `resolveActionId()` | Normalize dot ids to slash (`booking.create` → `booking/create`) |
65
+ | `resolveActionId()` | Normalize dot ids to slash (`@ossy.booking.actions.create` → `@ossy/booking/actions/create`) |
59
66
  | `AsyncStatus` | Loading state constants for UI |
60
67
  | `normalizeLocation()` | Normalize resource location paths |
61
68
  | `stableSerialize()` | Deterministic JSON for cache keys |
@@ -71,6 +78,14 @@ const { status, data, error, refetch } = sdk.read(ListResources, { location })
71
78
  // Command
72
79
  await sdk.invoke(CreateResource, payload)
73
80
 
81
+ // Optimistic command — folds pending event into cached resource, rolls back on error
82
+ await sdk.invokeOptimistic(UpdateResource, payload, {
83
+ type: '@ossy/booking/schema/booking',
84
+ resourceId: 'booking-1',
85
+ event: 'Updated',
86
+ payload: { status: 'confirmed' },
87
+ })
88
+
74
89
  // Invalidate cache (triggers re-fetch on next read)
75
90
  sdk.invalidate('location:/@ossy/domains/')
76
91
  sdk.invalidate(sdk.cacheKey(ListResources, { location }))
@@ -78,19 +93,39 @@ sdk.invalidate(sdk.cacheKey(ListResources, { location }))
78
93
 
79
94
  `read` is a React hook — call it unconditionally at the top of your component (same rules as `useState`).
80
95
 
96
+ ## Push invalidation
97
+
98
+ `WorkspaceProvider` calls `usePushInvalidation` internally. When the server sends SSE frames with `invalidate: string[]`, matching read-cache keys are cleared and active `useRead` hooks refetch.
99
+
100
+ For custom setups, subscribe manually:
101
+
102
+ ```ts
103
+ import { usePushInvalidation, useReadCacheStore } from '@ossy/sdk-react'
104
+
105
+ function MyBridge({ sdk }) {
106
+ const store = useReadCacheStore()
107
+ usePushInvalidation(sdk, store)
108
+ return null
109
+ }
110
+ ```
111
+
112
+ Requires `workspaceId` on the SDK config (or same-origin workspace cookie). See [@ossy/sdk README](../sdk/README.md#push-invalidation-adr-0008).
113
+
81
114
  ## Cache key conventions
82
115
 
83
116
  | Action + payload | Cache key |
84
117
  |---|---|
85
- | `resources/list` + `{ location }` | `location:${normalizeLocation(location)}` |
86
- | `resources/search` + query | `search:${stableSerialize(query)}` |
87
- | `resources/get` + `{ id }` | `resource:${id}` |
118
+ | `@ossy/resources/actions/list` + `{ location }` | `location:${normalizeLocation(location)}` |
119
+ | `@ossy/resources/actions/search` + query | `search:${stableSerialize(query)}` |
120
+ | `@ossy/resources/actions/get` + `{ resourceId \| id }` | `resource:${resourceId}` |
121
+ | `@ossy/booking/actions/list` | `action:@ossy/booking/actions/list` |
122
+ | `@ossy/platform/actions/list-task-runs` + `{ workspaceId }` | `projection:@ossy/platform/data/task-run-list:${workspaceId}` |
88
123
  | default | `action:${actionId}` or `action:${actionId}:${stableSerialize(payload)}` |
89
124
 
90
- Import action POJOs from feature packages (`@ossy/resources`, `@ossy/workspaces`, …). Action ids use slash notation (`booking/create`).
125
+ Import action POJOs from feature packages (`@ossy/resources`, `@ossy/workspaces`, …). Action ids use canonical `@ossy/{package}/actions/...` notation.
91
126
 
92
127
  ## Dependencies
93
128
 
94
- Peer dependencies: `@ossy/sdk`, `react`, `react-dom`.
129
+ Peer dependencies: `@ossy/sdk`, `@ossy/fold`, `react`, `react-dom`.
95
130
 
96
131
  No Ramda.
@@ -1 +1 @@
1
- var r=function(){return r=Object.assign||function(r){for(var t,e=1,n=arguments.length;e<n;e++)for(var o in t=arguments[e])Object.prototype.hasOwnProperty.call(t,o)&&(r[o]=t[o]);return r},r.apply(this,arguments)};"function"==typeof SuppressedError&&SuppressedError;export{r as __assign};
1
+ var t=function(){return t=Object.assign||function(t){for(var e,n=1,r=arguments.length;n<r;n++)for(var o in e=arguments[n])Object.prototype.hasOwnProperty.call(e,o)&&(t[o]=e[o]);return t},t.apply(this,arguments)};function e(t,e,n,r){return new(n||(n=Promise))(function(o,a){function u(t){try{i(r.next(t))}catch(t){a(t)}}function c(t){try{i(r.throw(t))}catch(t){a(t)}}function i(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n(function(t){t(e)})).then(u,c)}i((r=r.apply(t,e||[])).next())})}function n(t,e){var n,r,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},u=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return u.next=c(0),u.throw=c(1),u.return=c(2),"function"==typeof Symbol&&(u[Symbol.iterator]=function(){return this}),u;function c(c){return function(i){return function(c){if(n)throw new TypeError("Generator is already executing.");for(;u&&(u=0,c[0]&&(a=0)),a;)try{if(n=1,r&&(o=2&c[0]?r.return:c[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,c[1])).done)return o;switch(r=0,o&&(c=[2&c[0],o.value]),c[0]){case 0:case 1:o=c;break;case 4:return a.label++,{value:c[1],done:!1};case 5:a.label++,r=c[1],c=[0];continue;case 7:c=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==c[0]&&2!==c[0])){a=0;continue}if(3===c[0]&&(!o||c[1]>o[0]&&c[1]<o[3])){a.label=c[1];break}if(6===c[0]&&a.label<o[1]){a.label=o[1],o=c;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(c);break}o[2]&&a.ops.pop(),a.trys.pop();continue}c=e.call(t,a)}catch(t){c=[6,t],r=0}finally{n=o=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}([c,i])}}}"function"==typeof SuppressedError&&SuppressedError;export{t as __assign,e as __awaiter,n as __generator};
@@ -1 +1 @@
1
- import r,{createContext as e,useMemo as t}from"react";import{createCache as o}from"./Cache.js";import{createMapCache as m}from"./cacheUtils.js";import{ReactSdk as c}from"./react-sdk.js";var n=e({}),a=o(m()),i=function(e){var o=e.sdk,m=e.children,i=t(function(){return c.from(o)},[o]);return r.createElement(a,null,r.createElement(n.Provider,{value:{sdk:i}},m))};export{n as Context,i as WorkspaceProvider};
1
+ import r,{createContext as e,useMemo as t}from"react";import{createCache as n,useReadCacheStore as o}from"./Cache.js";import{createMapCache as a}from"./cacheUtils.js";import{ReactSdk as m}from"./react-sdk.js";import{usePushInvalidation as i}from"./usePushInvalidation.js";var c=e({}),s=n(a());function l(r){var e=r.sdk,t=o();return i(e,t),null}var u=function(e){var n=e.sdk,o=e.children,a=t(function(){return m.from(n)},[n]);return r.createElement(s,null,r.createElement(c.Provider,{value:{sdk:a}},r.createElement(l,{sdk:a}),o))};export{c as Context,u as WorkspaceProvider};
@@ -1 +1 @@
1
- var i={NotInitialized:"NotInitialized",Loading:"Loading",Success:"Success",Error:"Error"};export{i as AsyncStatus};
1
+ var i={NotInitialized:"NotInitialized",Loading:"Loading",Submitting:"Submitting",Success:"Success",Error:"Error"};export{i as AsyncStatus};
@@ -1 +1 @@
1
- var t=function(){return{status:"NotInitialized",data:void 0,error:null}};export{t as emptyEntry};
1
+ import{AsyncStatus as t}from"./asyncStatus.js";var r=Object.freeze({status:t.NotInitialized,data:void 0,error:null}),e=function(){return r};export{r as NOT_INITIALIZED_ENTRY,e as emptyEntry};
@@ -1 +1 @@
1
- import{resolveActionId as o}from"./action.js";import{normalizeLocation as r}from"./normalizeLocation.js";import{stableSerialize as t}from"./stableSerialize.js";function c(c,n){var i=o(c);if("resources/list"===i){var a=null==n?void 0:n.location;return"location:".concat(r("string"==typeof a?a:void 0))}if("resources/search"===i)return"search:".concat(t(null!=n?n:{}));if("resources/get"===i){var e=null==n?void 0:n.id;return"resource:".concat(null!=e?e:"")}var s=t(n);return s?"action:".concat(i,":").concat(s):"action:".concat(i)}export{c as cacheKey};
1
+ import{resolveActionId as o}from"./action.js";import{normalizeLocation as t}from"./normalizeLocation.js";import{stableSerialize as n}from"./stableSerialize.js";function r(o,t){return"projection:".concat(o,":").concat(t)}function s(s,i){var c,a=o(s);if("@ossy/resources/actions/list"===a){var e=null==i?void 0:i.location;return"location:".concat(t("string"==typeof e?e:void 0))}if("@ossy/resources/actions/search"===a)return"search:".concat(n(null!=i?i:{}));if("@ossy/resources/actions/get"===a){var l=null!==(c=null==i?void 0:i.resourceId)&&void 0!==c?c:null==i?void 0:i.id;return"resource:".concat(null!=l?l:"")}if("@ossy/booking/actions/list"===a)return"action:@ossy/booking/actions/list";if("@ossy/platform/actions/list-task-runs"===a){var u=null==i?void 0:i.workspaceId;return r("@ossy/platform/data/task-run-list",String(null!=u?u:""))}var f=n(i);return f?"action:".concat(a,":").concat(f):"action:".concat(a)}export{s as cacheKey,r as projectionKey};
@@ -0,0 +1 @@
1
+ import{__assign as r}from"../../../node_modules/tslib/tslib.es6.js";import{fold as s}from"@ossy/fold";import{AsyncStatus as e}from"./asyncStatus.js";import{cacheKey as t}from"./cacheKey.js";function o(o,n){var u,a,i,c=t({id:"@ossy/resources/actions/get"},{resourceId:n.resourceId}),d=o.getEntry(c);if(d.status!==e.Success||null==d.data)return null;var l=d,f=s(d.data,r(r({},n),{id:n.resourceId,version:null!==(u=n.version)&&void 0!==u?u:(null!==(a=d.data.version)&&void 0!==a?a:0)+1,created:null!==(i=n.created)&&void 0!==i?i:Date.now()}));return o.setEntry(c,{status:e.Success,data:f,error:null}),l}function n(r,s,e){if(e){var o=t({id:"@ossy/resources/actions/get"},{resourceId:s});r.setEntry(o,e)}}export{o as applyOptimisticResource,n as rollbackOptimisticResource};
@@ -1 +1 @@
1
- export{AsyncStatus}from"./asyncStatus.js";export{normalizeLocation}from"./normalizeLocation.js";export{ReactSdk}from"./react-sdk.js";export{resolveActionId,toActionRef}from"./action.js";export{Context,WorkspaceProvider}from"./WorkspaceProvider.js";export{useSdk}from"./useSdk.js";export{useRead}from"./useRead.js";export{cacheKey}from"./cacheKey.js";export{stableSerialize}from"./stableSerialize.js";
1
+ export{AsyncStatus}from"./asyncStatus.js";export{normalizeLocation}from"./normalizeLocation.js";export{ReactSdk}from"./react-sdk.js";export{resolveActionId,toActionRef}from"./action.js";export{Context,WorkspaceProvider}from"./WorkspaceProvider.js";export{useSdk}from"./useSdk.js";export{useRead}from"./useRead.js";export{usePushInvalidation}from"./usePushInvalidation.js";export{applyOptimisticResource,rollbackOptimisticResource}from"./optimisticResource.js";export{cacheKey,projectionKey}from"./cacheKey.js";export{stableSerialize}from"./stableSerialize.js";export{useReadCacheStore}from"./Cache.js";
@@ -1 +1 @@
1
- import{SDK as e}from"@ossy/sdk";import{toActionRef as t}from"./action.js";export{resolveActionId}from"./action.js";var o=function(){function o(e){this.sdk=e}return Object.defineProperty(o.prototype,"workspaceId",{get:function(){return this.sdk.workspaceId},enumerable:!1,configurable:!0}),Object.defineProperty(o.prototype,"authorization",{get:function(){return this.sdk.authorization},enumerable:!1,configurable:!0}),o.prototype.updateConfig=function(e){return this.sdk.updateConfig(e),this},o.prototype.invoke=function(e,o){return this.sdk.invoke(t(e),o)},Object.defineProperty(o.prototype,"resources",{get:function(){return this.sdk.resources},enumerable:!1,configurable:!0}),o.from=function(e){return e instanceof o?e:new o(e)},o.of=function(t){return new o(e.of(t))},o}();export{o as ReactSdk};
1
+ import{SDK as e}from"@ossy/sdk";import{toActionRef as t}from"./action.js";export{resolveActionId}from"./action.js";var r=function(){function r(e){this.sdk=e}return Object.defineProperty(r.prototype,"workspaceId",{get:function(){return this.sdk.workspaceId},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"authorization",{get:function(){return this.sdk.authorization},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"baseUrl",{get:function(){return this.sdk.baseUrl},enumerable:!1,configurable:!0}),r.prototype.subscribePush=function(e){return this.sdk.subscribePush(e)},r.prototype.updateConfig=function(e){return this.sdk.updateConfig(e),this},r.prototype.invoke=function(e,r){return this.sdk.invoke(t(e),r)},Object.defineProperty(r.prototype,"resources",{get:function(){return this.sdk.resources},enumerable:!1,configurable:!0}),r.from=function(e){return e instanceof r?e:new r(e)},r.of=function(t){return new r(e.of(t))},r}();export{r as ReactSdk};
@@ -0,0 +1 @@
1
+ import{useEffect as i}from"react";function n(n,o){i(function(){if((null==n?void 0:n.workspaceId)&&"function"==typeof n.subscribePush)return n.subscribePush({onMessage:function(i){var n;(null===(n=null==i?void 0:i.invalidate)||void 0===n?void 0:n.length)&&o.invalidate(i.invalidate)}})},[n,o])}export{n as usePushInvalidation};
@@ -1 +1 @@
1
- import{useContext as t,useMemo as r,useCallback as n,useSyncExternalStore as o,useEffect as e}from"react";import{toActionRef as s}from"./action.js";import{AsyncStatus as a}from"./asyncStatus.js";import{cacheKey as u}from"./cacheKey.js";import{useReadCacheStore as c}from"./Cache.js";import{Context as i}from"./WorkspaceProvider.js";function f(f,m,d){var p=t(i).sdk,v=c(),h=r(function(){return u(f,m)},[f,m]),j=!1!==(null==d?void 0:d.enabled),k=n(function(){return v.getEntry(h)},[v,h]),l=o(v.subscribe,k,k);e(function(){j&&l.status!==a.Loading&&l.status!==a.Success&&l.status!==a.Error&&v.fetch(h,function(){return p.invoke(s(f),m)}).catch(function(){})},[j,h,l.status,v,p,f,m]);var b=n(function(){return v.invalidate(h),v.fetch(h,function(){return p.invoke(s(f),m)})},[v,h,p,f,m]);return{status:l.status,data:l.data,error:l.error,refetch:b}}export{u as cacheKey,f as useRead};
1
+ import{useContext as t,useMemo as r,useCallback as o,useSyncExternalStore as n,useEffect as e}from"react";import{toActionRef as s}from"./action.js";import{AsyncStatus as a}from"./asyncStatus.js";import{cacheKey as i}from"./cacheKey.js";import{useReadCacheStore as u}from"./Cache.js";import{Context as c}from"./WorkspaceProvider.js";import{stableSerialize as f}from"./stableSerialize.js";function m(m,p,d){var l=t(c).sdk,v=u(),h=f(null!=p?p:{}),j=r(function(){return i(m,p)},[m,h]),b=!1!==(null==d?void 0:d.enabled),k=o(function(){return v.getEntry(j)},[v,j]),y=n(v.subscribe,k,k);e(function(){b&&y.status!==a.Loading&&y.status!==a.Success&&y.status!==a.Error&&v.fetch(j,function(){return l.invoke(s(m),p)}).catch(function(){})},[b,j,y.status,v,l,m,h]);var S=o(function(){return v.invalidate(j),v.fetch(j,function(){return l.invoke(s(m),p)})},[v,j,l,m,h]);return{status:y.status,data:y.data,error:y.error,refetch:S}}export{i as cacheKey,m as useRead};
@@ -1 +1 @@
1
- import{useContext as r,useCallback as e,useMemo as o}from"react";import{cacheKey as i}from"./cacheKey.js";import{Context as t}from"./WorkspaceProvider.js";import{useReadCacheStore as n}from"./Cache.js";import{useRead as a}from"./useRead.js";function c(){var c=r(t).sdk,m=n(),f=e(function(r){m.invalidate(r)},[m]),s=e(function(r,e){return c.invoke(r,e)},[c]);return o(function(){return{invoke:s,read:a,invalidate:f,cacheKey:i,sdk:c}},[s,f,c])}export{c as useSdk};
1
+ import{__awaiter as e,__generator as r}from"../../../node_modules/tslib/tslib.es6.js";import{useContext as t,useRef as o,useCallback as i,useMemo as n}from"react";import{cacheKey as s}from"./cacheKey.js";import{Context as c}from"./WorkspaceProvider.js";import{useReadCacheStore as u}from"./Cache.js";import{useRead as a}from"./useRead.js";import{rollbackOptimisticResource as d,applyOptimisticResource as m}from"./optimisticResource.js";function l(){var l=this,f=t(c).sdk,p=u(),v=o(new Map),h=i(function(e){p.invalidate(e)},[p]),k=i(function(e,r){return f.invoke(e,r)},[f]),j=i(function(t,o,i){return e(l,void 0,void 0,function(){var e,n;return r(this,function(r){switch(r.label){case 0:e=null,(null==i?void 0:i.resourceId)&&(e=m(p,i))&&v.current.set(i.resourceId,e),r.label=1;case 1:return r.trys.push([1,3,,4]),[4,f.invoke(t,o)];case 2:return[2,r.sent()];case 3:throw n=r.sent(),(null==i?void 0:i.resourceId)&&(d(p,i.resourceId,e),v.current.delete(i.resourceId)),n;case 4:return[2]}})})},[f,p]);return n(function(){return{invoke:k,invokeOptimistic:j,read:a,invalidate:h,cacheKey:s,sdk:f}},[k,j,h,f])}export{l as useSdk};
@@ -1,9 +1,10 @@
1
- import { SDK, SDKConfig } from '@ossy/sdk';
1
+ import { SDK, SDKConfig, PushMessage } from '@ossy/sdk';
2
2
  import React, { PropsWithChildren } from 'react';
3
3
 
4
4
  declare const AsyncStatus: {
5
5
  readonly NotInitialized: "NotInitialized";
6
6
  readonly Loading: "Loading";
7
+ readonly Submitting: "Submitting";
7
8
  readonly Success: "Success";
8
9
  readonly Error: "Error";
9
10
  };
@@ -29,6 +30,8 @@ declare class ReactSdk {
29
30
  constructor(sdk: SDK);
30
31
  get workspaceId(): string | undefined;
31
32
  get authorization(): string | undefined;
33
+ get baseUrl(): string;
34
+ subscribePush(handlers: Parameters<SDK['subscribePush']>[0]): () => void;
32
35
  updateConfig(config: SDKConfig): this;
33
36
  invoke<TPayload extends Record<string, unknown> = Record<string, unknown>, TResult = unknown>(action: ActionRef | string, payload?: TPayload): Promise<TResult>;
34
37
  /** @deprecated Use `invoke(action, payload)`. Kept for transitional resource uploads. */
@@ -66,6 +69,7 @@ declare const WorkspaceProvider: ({ sdk, children, }: PropsWithChildren<{
66
69
  sdk: SDK | ReactSdk;
67
70
  }>) => React.JSX.Element;
68
71
 
72
+ declare function projectionKey(projectionId: string, scopeId: string): string;
69
73
  declare function cacheKey(action: ActionRef | string, payload?: Record<string, unknown>): string;
70
74
 
71
75
  interface ReadOptions {
@@ -80,8 +84,41 @@ interface ReadResult<T = unknown> {
80
84
  }
81
85
  declare function useRead<T = unknown>(action: ActionRef | string, payload?: Record<string, unknown>, options?: ReadOptions): ReadResult<T>;
82
86
 
87
+ interface CacheEntry<T = unknown> {
88
+ status: AsyncStatusType;
89
+ data?: T;
90
+ error?: Error | null;
91
+ }
92
+
93
+ type Listener = () => void;
94
+ interface ReadCacheStore {
95
+ getEntry: (key: string) => CacheEntry;
96
+ setEntry: (key: string, entry: CacheEntry | ((prev: CacheEntry) => CacheEntry)) => void;
97
+ invalidate: (key: string | string[]) => void;
98
+ subscribe: (listener: Listener) => () => void;
99
+ fetch: <T>(key: string, fetcher: () => Promise<T>) => Promise<T>;
100
+ }
101
+ declare const useReadCacheStore: () => ReadCacheStore;
102
+
103
+ interface OptimisticResourceEvent {
104
+ type: string;
105
+ resourceId: string;
106
+ event: string;
107
+ version?: number;
108
+ payload?: Record<string, unknown>;
109
+ created?: number;
110
+ createdBy?: string;
111
+ }
112
+ /**
113
+ * Optimistically fold a pending resource event into the read cache (ADR 0008 §9).
114
+ * Returns rollback entry when applied.
115
+ */
116
+ declare function applyOptimisticResource(store: ReadCacheStore, event: OptimisticResourceEvent): CacheEntry | null;
117
+ declare function rollbackOptimisticResource(store: ReadCacheStore, resourceId: string, rollback: CacheEntry | null): void;
118
+
83
119
  interface SdkApi {
84
120
  invoke: ReactSdk['invoke'];
121
+ invokeOptimistic: (action: Parameters<ReactSdk['invoke']>[0], payload?: Record<string, unknown>, optimistic?: OptimisticResourceEvent) => ReturnType<ReactSdk['invoke']>;
85
122
  read: typeof useRead;
86
123
  invalidate: (key: string | string[]) => void;
87
124
  cacheKey: typeof cacheKey;
@@ -89,8 +126,18 @@ interface SdkApi {
89
126
  }
90
127
  declare function useSdk(): SdkApi;
91
128
 
129
+ /**
130
+ * ADR 0008 §9 — subscribe to SSE push and invalidate read-cache keys.
131
+ */
132
+ declare function usePushInvalidation(sdk: {
133
+ subscribePush?: (handlers: {
134
+ onMessage: (message: PushMessage) => void;
135
+ }) => () => void;
136
+ workspaceId?: string;
137
+ }, store: ReadCacheStore): void;
138
+
92
139
  /** Deterministic JSON for cache keys — sorted object keys, no Ramda. */
93
140
  declare function stableSerialize(value: unknown): string;
94
141
 
95
- export { AsyncStatus, Context, ReactSdk, WorkspaceProvider, cacheKey, normalizeLocation, resolveActionId, stableSerialize, toActionRef, useRead, useSdk };
96
- export type { ActionRef, AsyncStatusType, Config, ReadOptions, ReadResult, SdkApi };
142
+ export { AsyncStatus, Context, ReactSdk, WorkspaceProvider, applyOptimisticResource, cacheKey, normalizeLocation, projectionKey, resolveActionId, rollbackOptimisticResource, stableSerialize, toActionRef, usePushInvalidation, useRead, useReadCacheStore, useSdk };
143
+ export type { ActionRef, AsyncStatusType, Config, OptimisticResourceEvent, ReadOptions, ReadResult, SdkApi };
@@ -1,6 +1,7 @@
1
1
  export declare const AsyncStatus: {
2
2
  readonly NotInitialized: "NotInitialized";
3
3
  readonly Loading: "Loading";
4
+ readonly Submitting: "Submitting";
4
5
  readonly Success: "Success";
5
6
  readonly Error: "Error";
6
7
  };
@@ -4,4 +4,6 @@ export interface CacheEntry<T = unknown> {
4
4
  data?: T;
5
5
  error?: Error | null;
6
6
  }
7
+ /** Stable fallback for useSyncExternalStore — must not allocate per getSnapshot call. */
8
+ export declare const NOT_INITIALIZED_ENTRY: CacheEntry;
7
9
  export declare const emptyEntry: () => CacheEntry;
@@ -1,2 +1,3 @@
1
1
  import { type ActionRef } from './action';
2
+ export declare function projectionKey(projectionId: string, scopeId: string): string;
2
3
  export declare function cacheKey(action: ActionRef | string, payload?: Record<string, unknown>): string;
@@ -0,0 +1,17 @@
1
+ import type { ReadCacheStore } from './Cache';
2
+ import type { CacheEntry } from './cacheEntry';
3
+ export interface OptimisticResourceEvent {
4
+ type: string;
5
+ resourceId: string;
6
+ event: string;
7
+ version?: number;
8
+ payload?: Record<string, unknown>;
9
+ created?: number;
10
+ createdBy?: string;
11
+ }
12
+ /**
13
+ * Optimistically fold a pending resource event into the read cache (ADR 0008 §9).
14
+ * Returns rollback entry when applied.
15
+ */
16
+ export declare function applyOptimisticResource(store: ReadCacheStore, event: OptimisticResourceEvent): CacheEntry | null;
17
+ export declare function rollbackOptimisticResource(store: ReadCacheStore, resourceId: string, rollback: CacheEntry | null): void;
@@ -5,5 +5,8 @@ export * from './action';
5
5
  export * from './WorkspaceProvider';
6
6
  export * from './useSdk';
7
7
  export * from './useRead';
8
+ export * from './usePushInvalidation';
9
+ export * from './optimisticResource';
8
10
  export * from './cacheKey';
9
11
  export * from './stableSerialize';
12
+ export { useReadCacheStore } from './Cache';
@@ -8,6 +8,8 @@ export declare class ReactSdk {
8
8
  constructor(sdk: SDK);
9
9
  get workspaceId(): string | undefined;
10
10
  get authorization(): string | undefined;
11
+ get baseUrl(): string;
12
+ subscribePush(handlers: Parameters<SDK['subscribePush']>[0]): () => void;
11
13
  updateConfig(config: SDKConfig): this;
12
14
  invoke<TPayload extends Record<string, unknown> = Record<string, unknown>, TResult = unknown>(action: ActionRef | string, payload?: TPayload): Promise<TResult>;
13
15
  /** @deprecated Use `invoke(action, payload)`. Kept for transitional resource uploads. */
@@ -0,0 +1,11 @@
1
+ import type { PushMessage } from '@ossy/sdk';
2
+ import type { ReadCacheStore } from './Cache';
3
+ /**
4
+ * ADR 0008 §9 — subscribe to SSE push and invalidate read-cache keys.
5
+ */
6
+ export declare function usePushInvalidation(sdk: {
7
+ subscribePush?: (handlers: {
8
+ onMessage: (message: PushMessage) => void;
9
+ }) => () => void;
10
+ workspaceId?: string;
11
+ }, store: ReadCacheStore): void;
@@ -1,8 +1,10 @@
1
1
  import { cacheKey } from './cacheKey';
2
2
  import { useRead } from './useRead';
3
3
  import type { ReactSdk } from './react-sdk';
4
+ import { type OptimisticResourceEvent } from './optimisticResource';
4
5
  export interface SdkApi {
5
6
  invoke: ReactSdk['invoke'];
7
+ invokeOptimistic: (action: Parameters<ReactSdk['invoke']>[0], payload?: Record<string, unknown>, optimistic?: OptimisticResourceEvent) => ReturnType<ReactSdk['invoke']>;
6
8
  read: typeof useRead;
7
9
  invalidate: (key: string | string[]) => void;
8
10
  cacheKey: typeof cacheKey;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/sdk-react",
3
3
  "description": "Software Development Kit React",
4
- "version": "1.40.2",
4
+ "version": "1.40.3",
5
5
  "url": "git://github.com/ossy-se/packages/sdk-react",
6
6
  "source": "src/public.index.ts",
7
7
  "main": "./build/packages/sdk-react/src/public.index.js",
@@ -40,6 +40,7 @@
40
40
  "@babel/preset-typescript": "^7.26.0"
41
41
  },
42
42
  "peerDependencies": {
43
+ "@ossy/fold": ">=1.0.0 <2.0.0",
43
44
  "@ossy/sdk": ">=1.0.0 <2.0.0",
44
45
  "react": ">=19.0.0 <20.0.0",
45
46
  "react-dom": ">=19.0.0 <20.0.0"
@@ -52,5 +53,5 @@
52
53
  "/build",
53
54
  "README.md"
54
55
  ],
55
- "gitHead": "2b8745d57fee8b6c08787e2755b4df489291a5cd"
56
+ "gitHead": "a0d89185a17f8de8ce328c3a648c108ff1d61d8f"
56
57
  }