@webority/mobile-core 0.16.6 → 0.17.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 CHANGED
@@ -40,7 +40,7 @@ import { createApiBaseQuery, createAppApi, setupApiListeners } from '@webority/m
40
40
  Metro resolves requires statically, so anything reachable from the main barrel is bundled for
41
41
  every consumer. Keeping these off it is what makes their peers genuinely optional.
42
42
 
43
- `/query` is the RTK Query layer; it needs `@reduxjs/toolkit` (2.6 or later) and `react-redux`,
43
+ `/query` is the RTK Query layer; it needs `@reduxjs/toolkit` (2.12 or later) and `react-redux`,
44
44
  both optional peers. One `createApi` per app, built with `createAppApi` and the base query over
45
45
  the library's clients, so refresh, retry, the offline check and the `extractApiError` failure
46
46
  shape stay in one place. Never a `fetchBaseQuery`.
@@ -98,6 +98,35 @@ keeps push on calls `enablePush` at each start. `setBadgeCount(count)` sets the
98
98
  an unread count. It returns a function that removes the handler. A notification arriving in the
99
99
  background does not reach it; its tap still goes to `onOpen`.
100
100
 
101
+ ### Offline copy
102
+
103
+ `createPersistedQueries` keeps the answers of chosen endpoints on the phone, so a screen opened
104
+ with no connection shows the last data with the time it was loaded. Listed endpoints are saved to
105
+ `Storage` when they load; `restore` puts them back once at start.
106
+
107
+ 1. Pass `extractRehydrationInfo` to `createAppApi`.
108
+ 2. Add `middleware` after the api's middleware.
109
+ 3. Call `restore(store.dispatch)` once after the store is created.
110
+ 4. Call `clear()` on sign-out, so the next person on the phone never sees the last person's data.
111
+
112
+ ```ts
113
+ // services/api/offlineQueries.ts
114
+ export const offlineQueries = createPersistedQueries({
115
+ storageKey: 'offline.queries',
116
+ endpoints: ['getProjects']
117
+ });
118
+
119
+ // services/api/apiSlice.ts: add extractRehydrationInfo: offlineQueries.extractRehydrationInfo
120
+ // store.ts: reducer: { [apiSlice.reducerPath]: apiSlice.reducer }
121
+ // middleware: (getDefault) => getDefault().concat(apiSlice.middleware, offlineQueries.middleware)
122
+ offlineQueries.restore(store.dispatch); // once, after the store exists
123
+ // sign-out
124
+ await offlineQueries.clear();
125
+ ```
126
+
127
+ A restored answer shows at once without a fetch, so set `refetchOnMountOrArgChange: true` on each
128
+ kept endpoint: a screen that mounts then asks the server again and refreshes the restored data.
129
+
101
130
  ### Notification buttons
102
131
 
103
132
  A push names a category; the category's buttons are registered on the phone. Register them at
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"APP_API_DEFAULTS",{enumerable:!0,get:function(){return _createAppApi.APP_API_DEFAULTS}}),Object.defineProperty(exports,"LIST_TAG_ID",{enumerable:!0,get:function(){return _tags.LIST_TAG_ID}}),Object.defineProperty(exports,"buildListQuery",{enumerable:!0,get:function(){return _paging.buildListQuery}}),Object.defineProperty(exports,"createApiBaseQuery",{enumerable:!0,get:function(){return _apiBaseQuery.createApiBaseQuery}}),Object.defineProperty(exports,"createAppApi",{enumerable:!0,get:function(){return _createAppApi.createAppApi}}),Object.defineProperty(exports,"getNextPageParam",{enumerable:!0,get:function(){return _paging.getNextPageParam}}),Object.defineProperty(exports,"getPreviousPageParam",{enumerable:!0,get:function(){return _paging.getPreviousPageParam}}),Object.defineProperty(exports,"hasNextPage",{enumerable:!0,get:function(){return _paging.hasNextPage}}),Object.defineProperty(exports,"hasPreviousPage",{enumerable:!0,get:function(){return _paging.hasPreviousPage}}),Object.defineProperty(exports,"itemTag",{enumerable:!0,get:function(){return _tags.itemTag}}),Object.defineProperty(exports,"listTag",{enumerable:!0,get:function(){return _tags.listTag}}),Object.defineProperty(exports,"providesList",{enumerable:!0,get:function(){return _tags.providesList}}),Object.defineProperty(exports,"providesPagedList",{enumerable:!0,get:function(){return _tags.providesPagedList}}),Object.defineProperty(exports,"setupApiListeners",{enumerable:!0,get:function(){return _listeners.setupApiListeners}});var _apiBaseQuery=require("./apiBaseQuery.js"),_createAppApi=require("./createAppApi.js"),_listeners=require("./listeners.js"),_paging=require("./paging.js"),_tags=require("./tags.js");
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"APP_API_DEFAULTS",{enumerable:!0,get:function(){return _createAppApi.APP_API_DEFAULTS}}),Object.defineProperty(exports,"LIST_TAG_ID",{enumerable:!0,get:function(){return _tags.LIST_TAG_ID}}),Object.defineProperty(exports,"PERSISTED_QUERIES_RESTORED",{enumerable:!0,get:function(){return _persistedQueries.PERSISTED_QUERIES_RESTORED}}),Object.defineProperty(exports,"buildListQuery",{enumerable:!0,get:function(){return _paging.buildListQuery}}),Object.defineProperty(exports,"createApiBaseQuery",{enumerable:!0,get:function(){return _apiBaseQuery.createApiBaseQuery}}),Object.defineProperty(exports,"createAppApi",{enumerable:!0,get:function(){return _createAppApi.createAppApi}}),Object.defineProperty(exports,"createPersistedQueries",{enumerable:!0,get:function(){return _persistedQueries.createPersistedQueries}}),Object.defineProperty(exports,"getNextPageParam",{enumerable:!0,get:function(){return _paging.getNextPageParam}}),Object.defineProperty(exports,"getPreviousPageParam",{enumerable:!0,get:function(){return _paging.getPreviousPageParam}}),Object.defineProperty(exports,"hasNextPage",{enumerable:!0,get:function(){return _paging.hasNextPage}}),Object.defineProperty(exports,"hasPreviousPage",{enumerable:!0,get:function(){return _paging.hasPreviousPage}}),Object.defineProperty(exports,"itemTag",{enumerable:!0,get:function(){return _tags.itemTag}}),Object.defineProperty(exports,"listTag",{enumerable:!0,get:function(){return _tags.listTag}}),Object.defineProperty(exports,"providesList",{enumerable:!0,get:function(){return _tags.providesList}}),Object.defineProperty(exports,"providesPagedList",{enumerable:!0,get:function(){return _tags.providesPagedList}}),Object.defineProperty(exports,"setupApiListeners",{enumerable:!0,get:function(){return _listeners.setupApiListeners}});var _apiBaseQuery=require("./apiBaseQuery.js"),_createAppApi=require("./createAppApi.js"),_listeners=require("./listeners.js"),_paging=require("./paging.js"),_persistedQueries=require("./persistedQueries.js"),_tags=require("./tags.js");
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.createPersistedQueries=exports.PERSISTED_QUERIES_RESTORED=void 0;var _index=require("../logger/index.js"),_index2=require("../storage/index.js");const PERSISTED_QUERIES_RESTORED=exports.PERSISTED_QUERIES_RESTORED="@webority/mobile-core/query/persistedQueriesRestored",SAVED_VERSION=1,FULFILLED_SUFFIX="/executeQuery/fulfilled",fulfilledEndpointName=e=>{if("object"!=typeof e||null===e)return;const{type:r,meta:t}=e;if("string"!=typeof r||!r.endsWith(FULFILLED_SUFFIX))return;const s=t?.arg?.endpointName;return"string"==typeof s?s:void 0},isSavedQueries=e=>{if("object"!=typeof e||null===e)return!1;const{version:r,queries:t}=e;return 1===r&&"object"==typeof t&&null!==t&&!Array.isArray(t)},createPersistedQueries=e=>{const{storageKey:r,endpoints:t,reducerPath:s="api"}=e,i=new Set(t);let n=Promise.resolve(),o=0;const enqueue=e=>(n=n.then(e).catch(e=>_index.Logger.warn("[persistedQueries] storage step failed",e)),n);return{extractRehydrationInfo:(e,r)=>{if(e.type!==PERSISTED_QUERIES_RESTORED||r.reducerPath!==s)return;const{queries:t}=e.payload;return{queries:t,mutations:{},provided:{},subscriptions:{},config:{}}},middleware:e=>t=>n=>{const a=t(n),d=fulfilledEndpointName(n);if(void 0!==d&&i.has(d)){const t=e.getState(),n=t[s]?.queries??{},a={version:1,queries:Object.fromEntries(Object.entries(n).filter(([,e])=>void 0!==e&&i.has(e.endpointName)&&"fulfilled"===e.status))},d=o;enqueue(async()=>{if(d!==o)return;await _index2.Storage.setJson(r,a)||_index.Logger.warn("[persistedQueries] save failed")})}return a},restore:async e=>{const t=await _index2.Storage.getJson(r);isSavedQueries(t)&&e({type:PERSISTED_QUERIES_RESTORED,payload:{queries:t.queries}})},clear:()=>(o+=1,enqueue(async()=>{await _index2.Storage.removeItem(r)||_index.Logger.warn("[persistedQueries] clear failed; the saved list is still on the phone")}))}};exports.createPersistedQueries=createPersistedQueries;
@@ -1 +1 @@
1
- "use strict";export{createApiBaseQuery}from"./apiBaseQuery.js";export{APP_API_DEFAULTS,createAppApi}from"./createAppApi.js";export{setupApiListeners}from"./listeners.js";export{buildListQuery,getNextPageParam,getPreviousPageParam,hasNextPage,hasPreviousPage}from"./paging.js";export{itemTag,LIST_TAG_ID,listTag,providesList,providesPagedList}from"./tags.js";
1
+ "use strict";export{createApiBaseQuery}from"./apiBaseQuery.js";export{APP_API_DEFAULTS,createAppApi}from"./createAppApi.js";export{setupApiListeners}from"./listeners.js";export{buildListQuery,getNextPageParam,getPreviousPageParam,hasNextPage,hasPreviousPage}from"./paging.js";export{createPersistedQueries,PERSISTED_QUERIES_RESTORED}from"./persistedQueries.js";export{itemTag,LIST_TAG_ID,listTag,providesList,providesPagedList}from"./tags.js";
@@ -0,0 +1 @@
1
+ "use strict";import{Logger}from"../logger/index.js";import{Storage}from"../storage/index.js";export const PERSISTED_QUERIES_RESTORED="@webority/mobile-core/query/persistedQueriesRestored";const SAVED_VERSION=1,FULFILLED_SUFFIX="/executeQuery/fulfilled",fulfilledEndpointName=e=>{if("object"!=typeof e||null===e)return;const{type:t,meta:r}=e;if("string"!=typeof t||!t.endsWith(FULFILLED_SUFFIX))return;const s=r?.arg?.endpointName;return"string"==typeof s?s:void 0},isSavedQueries=e=>{if("object"!=typeof e||null===e)return!1;const{version:t,queries:r}=e;return 1===t&&"object"==typeof r&&null!==r&&!Array.isArray(r)};export const createPersistedQueries=e=>{const{storageKey:t,endpoints:r,reducerPath:s="api"}=e,i=new Set(r);let o=Promise.resolve(),n=0;const enqueue=e=>(o=o.then(e).catch(e=>Logger.warn("[persistedQueries] storage step failed",e)),o);return{extractRehydrationInfo:(e,t)=>{if(e.type!==PERSISTED_QUERIES_RESTORED||t.reducerPath!==s)return;const{queries:r}=e.payload;return{queries:r,mutations:{},provided:{},subscriptions:{},config:{}}},middleware:e=>r=>o=>{const a=r(o),u=fulfilledEndpointName(o);if(void 0!==u&&i.has(u)){const r=e.getState(),o=r[s]?.queries??{},a={version:1,queries:Object.fromEntries(Object.entries(o).filter(([,e])=>void 0!==e&&i.has(e.endpointName)&&"fulfilled"===e.status))},u=n;enqueue(async()=>{if(u!==n)return;await Storage.setJson(t,a)||Logger.warn("[persistedQueries] save failed")})}return a},restore:async e=>{const r=await Storage.getJson(t);isSavedQueries(r)&&e({type:PERSISTED_QUERIES_RESTORED,payload:{queries:r.queries}})},clear:()=>(n+=1,enqueue(async()=>{await Storage.removeItem(t)||Logger.warn("[persistedQueries] clear failed; the saved list is still on the phone")}))}};
@@ -4,6 +4,8 @@ export { APP_API_DEFAULTS, createAppApi } from './createAppApi';
4
4
  export { setupApiListeners } from './listeners';
5
5
  export type { ListFilterValue, ListQuery, PagedDataObject, PagedListRequest, SortOrder } from './paging';
6
6
  export { buildListQuery, getNextPageParam, getPreviousPageParam, hasNextPage, hasPreviousPage } from './paging';
7
+ export type { PersistedQueries, PersistedQueriesOptions } from './persistedQueries';
8
+ export { createPersistedQueries, PERSISTED_QUERIES_RESTORED } from './persistedQueries';
7
9
  export type { ApiTag, TagId } from './tags';
8
10
  export { itemTag, LIST_TAG_ID, listTag, providesList, providesPagedList } from './tags';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,26 @@
1
+ import type { Dispatch, Middleware, UnknownAction } from '@reduxjs/toolkit';
2
+ import type { CombinedState } from '@reduxjs/toolkit/query';
3
+ export declare const PERSISTED_QUERIES_RESTORED = "@webority/mobile-core/query/persistedQueriesRestored";
4
+ export interface PersistedQueriesOptions {
5
+ storageKey: string;
6
+ endpoints: readonly string[];
7
+ reducerPath?: string;
8
+ }
9
+ type RehydratedState = CombinedState<any, any, any>;
10
+ export interface PersistedQueries {
11
+ extractRehydrationInfo: (action: UnknownAction, meta: {
12
+ reducerPath: string;
13
+ }) => RehydratedState | undefined;
14
+ middleware: Middleware;
15
+ restore: (dispatch: Dispatch) => Promise<void>;
16
+ clear: () => Promise<void>;
17
+ }
18
+ /**
19
+ * Keeps the chosen RTK Query results on the phone so they show offline. Pass
20
+ * `extractRehydrationInfo` to `createAppApi`, add `middleware` after the api's
21
+ * middleware, call `restore` once after the store is created, and call `clear`
22
+ * on sign-out so the next person on the phone never sees the last person's data.
23
+ */
24
+ export declare const createPersistedQueries: (options: PersistedQueriesOptions) => PersistedQueries;
25
+ export {};
26
+ //# sourceMappingURL=persistedQueries.d.ts.map
@@ -4,6 +4,8 @@ export { APP_API_DEFAULTS, createAppApi } from './createAppApi.js';
4
4
  export { setupApiListeners } from './listeners.js';
5
5
  export type { ListFilterValue, ListQuery, PagedDataObject, PagedListRequest, SortOrder } from './paging.js';
6
6
  export { buildListQuery, getNextPageParam, getPreviousPageParam, hasNextPage, hasPreviousPage } from './paging.js';
7
+ export type { PersistedQueries, PersistedQueriesOptions } from './persistedQueries.js';
8
+ export { createPersistedQueries, PERSISTED_QUERIES_RESTORED } from './persistedQueries.js';
7
9
  export type { ApiTag, TagId } from './tags.js';
8
10
  export { itemTag, LIST_TAG_ID, listTag, providesList, providesPagedList } from './tags.js';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,26 @@
1
+ import type { Dispatch, Middleware, UnknownAction } from '@reduxjs/toolkit';
2
+ import type { CombinedState } from '@reduxjs/toolkit/query';
3
+ export declare const PERSISTED_QUERIES_RESTORED = "@webority/mobile-core/query/persistedQueriesRestored";
4
+ export interface PersistedQueriesOptions {
5
+ storageKey: string;
6
+ endpoints: readonly string[];
7
+ reducerPath?: string;
8
+ }
9
+ type RehydratedState = CombinedState<any, any, any>;
10
+ export interface PersistedQueries {
11
+ extractRehydrationInfo: (action: UnknownAction, meta: {
12
+ reducerPath: string;
13
+ }) => RehydratedState | undefined;
14
+ middleware: Middleware;
15
+ restore: (dispatch: Dispatch) => Promise<void>;
16
+ clear: () => Promise<void>;
17
+ }
18
+ /**
19
+ * Keeps the chosen RTK Query results on the phone so they show offline. Pass
20
+ * `extractRehydrationInfo` to `createAppApi`, add `middleware` after the api's
21
+ * middleware, call `restore` once after the store is created, and call `clear`
22
+ * on sign-out so the next person on the phone never sees the last person's data.
23
+ */
24
+ export declare const createPersistedQueries: (options: PersistedQueriesOptions) => PersistedQueries;
25
+ export {};
26
+ //# sourceMappingURL=persistedQueries.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-core",
3
- "version": "0.16.6",
3
+ "version": "0.17.0",
4
4
  "description": "Platform layer for Webority React Native apps: HTTP clients, auth/token storage, config, logging, formatters, validators, network status, permissions, storage, version checks and over-the-air updates. No UI.",
5
5
  "keywords": [
6
6
  "react-native",