@oxy.so/services 13.7.0 → 13.8.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.
Files changed (42) hide show
  1. package/android/build.gradle +1 -0
  2. package/android/src/main/java/so/oxy/assets/OxyAssetFilesModule.kt +49 -0
  3. package/android/src/main/java/so/oxy/assets/StreamSha256.kt +23 -0
  4. package/android/src/test/java/so/oxy/assets/StreamSha256Test.kt +45 -0
  5. package/expo-module.config.json +3 -2
  6. package/ios/OxyAssetFilesModule.swift +72 -0
  7. package/lib/commonjs/ui/context/OxyContext.js +4 -0
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/utils/nativeAssetFiles.js +11 -0
  10. package/lib/commonjs/ui/utils/nativeAssetFiles.js.map +1 -0
  11. package/lib/commonjs/ui/utils/nativeAssetFiles.macos.js +11 -0
  12. package/lib/commonjs/ui/utils/nativeAssetFiles.macos.js.map +1 -0
  13. package/lib/commonjs/ui/utils/nativeAssetFiles.native.js +202 -0
  14. package/lib/commonjs/ui/utils/nativeAssetFiles.native.js.map +1 -0
  15. package/lib/module/ui/context/OxyContext.js +5 -1
  16. package/lib/module/ui/context/OxyContext.js.map +1 -1
  17. package/lib/module/ui/utils/nativeAssetFiles.js +7 -0
  18. package/lib/module/ui/utils/nativeAssetFiles.js.map +1 -0
  19. package/lib/module/ui/utils/nativeAssetFiles.macos.js +7 -0
  20. package/lib/module/ui/utils/nativeAssetFiles.macos.js.map +1 -0
  21. package/lib/module/ui/utils/nativeAssetFiles.native.js +197 -0
  22. package/lib/module/ui/utils/nativeAssetFiles.native.js.map +1 -0
  23. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.d.ts +4 -0
  25. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.d.ts.map +1 -0
  26. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.macos.d.ts +4 -0
  27. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.macos.d.ts.map +1 -0
  28. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.native.d.ts +4 -0
  29. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.native.d.ts.map +1 -0
  30. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  31. package/lib/typescript/module/ui/utils/nativeAssetFiles.d.ts +4 -0
  32. package/lib/typescript/module/ui/utils/nativeAssetFiles.d.ts.map +1 -0
  33. package/lib/typescript/module/ui/utils/nativeAssetFiles.macos.d.ts +4 -0
  34. package/lib/typescript/module/ui/utils/nativeAssetFiles.macos.d.ts.map +1 -0
  35. package/lib/typescript/module/ui/utils/nativeAssetFiles.native.d.ts +4 -0
  36. package/lib/typescript/module/ui/utils/nativeAssetFiles.native.d.ts.map +1 -0
  37. package/package.json +5 -4
  38. package/src/ui/context/OxyContext.tsx +13 -1
  39. package/src/ui/utils/__tests__/nativeAssetFiles.test.ts +270 -0
  40. package/src/ui/utils/nativeAssetFiles.macos.ts +6 -0
  41. package/src/ui/utils/nativeAssetFiles.native.ts +212 -0
  42. package/src/ui/utils/nativeAssetFiles.ts +6 -0
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+
3
+ // File.slice() reads the WHOLE file on Expo; RN Blob does the same on Java's
4
+ // heap. FileHandle reads and native file uploads keep both heaps bounded.
5
+ const COPY_CHUNK_BYTES = 1024 * 1024;
6
+ let partsDirectory;
7
+ let nextPart = 0;
8
+ let nextHash = 0;
9
+ // FileHandle methods are synchronous: awaiting them alone only yields a
10
+ // microtask. Let rendering, native events and abort timers run between chunks.
11
+ function yieldToEventLoop() {
12
+ return new Promise(resolve => setTimeout(resolve, 0));
13
+ }
14
+ function abortError() {
15
+ const error = new Error('Upload aborted');
16
+ error.name = 'AbortError';
17
+ return error;
18
+ }
19
+ function checkAbort(signal) {
20
+ if (signal?.aborted) throw abortError();
21
+ }
22
+ async function temporaryParts() {
23
+ partsDirectory ??= import('expo-file-system').then(({
24
+ Directory,
25
+ Paths
26
+ }) => {
27
+ const directory = new Directory(Paths.cache, 'oxy-upload-parts');
28
+ // These are foreground transfers. A new JS runtime owns no previous parts;
29
+ // remove leftovers from a killed process before creating any new part.
30
+ if (directory.exists) directory.delete();
31
+ directory.create({
32
+ intermediates: true
33
+ });
34
+ return directory;
35
+ }).catch(error => {
36
+ partsDirectory = undefined;
37
+ throw error;
38
+ });
39
+ return partsDirectory;
40
+ }
41
+ async function copyRange(file, target, start, end, signal) {
42
+ const reader = file.open();
43
+ try {
44
+ reader.offset = start;
45
+ const writer = target.open();
46
+ try {
47
+ for (let offset = start; offset < end;) {
48
+ checkAbort(signal);
49
+ const bytes = await reader.readBytes(Math.min(COPY_CHUNK_BYTES, end - offset));
50
+ if (!bytes.length) throw new Error('File changed while preparing upload');
51
+ await writer.writeBytes(bytes);
52
+ offset += bytes.length;
53
+ await yieldToEventLoop();
54
+ }
55
+ } finally {
56
+ writer.close();
57
+ }
58
+ } finally {
59
+ reader.close();
60
+ }
61
+ }
62
+ async function putRange(file, request) {
63
+ const [{
64
+ File
65
+ }, legacy, directory] = await Promise.all([import('expo-file-system'), import('expo-file-system/legacy'), temporaryParts()]);
66
+ checkAbort(request.signal);
67
+ const part = new File(directory, `part-${++nextPart}`);
68
+ part.create();
69
+ try {
70
+ await copyRange(file, part, request.start, request.end, request.signal);
71
+ checkAbort(request.signal);
72
+ const task = legacy.createUploadTask(request.url, part.uri, {
73
+ httpMethod: 'PUT',
74
+ uploadType: legacy.FileSystemUploadType.BINARY_CONTENT,
75
+ sessionType: legacy.FileSystemSessionType.FOREGROUND,
76
+ headers: {}
77
+ }, ({
78
+ totalBytesSent
79
+ }) => request.onProgress(totalBytesSent));
80
+ let cancel;
81
+ let stop = () => {};
82
+ const interrupted = new Promise((_resolve, reject) => {
83
+ stop = () => {
84
+ cancel ??= task.cancelAsync();
85
+ void cancel.catch(() => undefined);
86
+ reject(abortError());
87
+ };
88
+ });
89
+ const timer = setTimeout(() => {
90
+ stop();
91
+ }, request.timeoutMs);
92
+ request.signal?.addEventListener('abort', stop, {
93
+ once: true
94
+ });
95
+ try {
96
+ checkAbort(request.signal);
97
+ const result = await Promise.race([task.uploadAsync(), interrupted]);
98
+ if (!result) throw abortError();
99
+ return result.status >= 200 && result.status < 300 ? {
100
+ ok: true
101
+ } : {
102
+ ok: false,
103
+ status: result.status
104
+ };
105
+ } catch {
106
+ if (request.signal?.aborted) throw abortError();
107
+ // The core session owner applies its bounded retry/backoff policy.
108
+ return {
109
+ ok: false,
110
+ status: 0
111
+ };
112
+ } finally {
113
+ clearTimeout(timer);
114
+ request.signal?.removeEventListener('abort', stop);
115
+ if (cancel) await cancel.catch(() => undefined);
116
+ }
117
+ } finally {
118
+ if (part.exists) part.delete();
119
+ }
120
+ }
121
+ async function openNativeFile(uri) {
122
+ const {
123
+ File
124
+ } = await import('expo-file-system');
125
+ let nativeHasher = null;
126
+ try {
127
+ const {
128
+ requireOptionalNativeModule
129
+ } = await import('expo-modules-core');
130
+ nativeHasher = requireOptionalNativeModule('OxyAssetFiles');
131
+ } catch {
132
+ // Hosts without the optional Expo module bridge retain bounded file access.
133
+ }
134
+ const file = new File(uri);
135
+ if (!file.exists) throw new Error('Native upload file is unavailable');
136
+ const size = file.size;
137
+ return {
138
+ size,
139
+ // Older native binaries can still use bounded JS reads after an OTA. New
140
+ // binaries hash entirely on a native worker, without sending bytes to JS.
141
+ hash: nativeHasher ? async (onProgress, signal) => {
142
+ checkAbort(signal);
143
+ if (file.size !== size) throw new Error('File changed while hashing');
144
+ const id = `upload-hash-${++nextHash}`;
145
+ const listener = nativeHasher.addListener('hashProgress', event => {
146
+ if (event.id === id && !signal?.aborted) onProgress?.(event.loaded);
147
+ });
148
+ const stop = () => nativeHasher.cancelHash(id);
149
+ try {
150
+ nativeHasher.createHash(id);
151
+ signal?.addEventListener('abort', stop, {
152
+ once: true
153
+ });
154
+ if (signal?.aborted) stop();
155
+ const digest = await nativeHasher.hashAsync(id, uri, size);
156
+ checkAbort(signal);
157
+ return digest;
158
+ } catch (error) {
159
+ checkAbort(signal);
160
+ throw error;
161
+ } finally {
162
+ signal?.removeEventListener('abort', stop);
163
+ listener.remove();
164
+ }
165
+ } : undefined,
166
+ async readRange(start, end) {
167
+ if (file.size !== size) throw new Error('File changed while uploading');
168
+ if (start < 0 || end > size || end < start) throw new Error('Invalid upload file range');
169
+ const reader = file.open();
170
+ try {
171
+ reader.offset = start;
172
+ const output = new Uint8Array(end - start);
173
+ let read = 0;
174
+ while (read < output.length) {
175
+ const bytes = await reader.readBytes(Math.min(COPY_CHUNK_BYTES, output.length - read));
176
+ if (!bytes.length) throw new Error('File changed while hashing');
177
+ output.set(bytes, read);
178
+ read += bytes.length;
179
+ await yieldToEventLoop();
180
+ }
181
+ return output;
182
+ } finally {
183
+ reader.close();
184
+ }
185
+ },
186
+ putPart: request => {
187
+ if (file.size !== size) throw new Error('File changed while uploading');
188
+ return putRange(file, request);
189
+ }
190
+ };
191
+ }
192
+
193
+ /** One provider-owned adapter; no native filesystem module loads before an upload. */
194
+ export function installNativeAssetFiles(oxy) {
195
+ return oxy.assets.installFileAdapter(openNativeFile);
196
+ }
197
+ //# sourceMappingURL=nativeAssetFiles.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["COPY_CHUNK_BYTES","partsDirectory","nextPart","nextHash","yieldToEventLoop","Promise","resolve","setTimeout","abortError","error","Error","name","checkAbort","signal","aborted","temporaryParts","then","Directory","Paths","directory","cache","exists","delete","create","intermediates","catch","undefined","copyRange","file","target","start","end","reader","open","offset","writer","bytes","readBytes","Math","min","length","writeBytes","close","putRange","request","File","legacy","all","part","task","createUploadTask","url","uri","httpMethod","uploadType","FileSystemUploadType","BINARY_CONTENT","sessionType","FileSystemSessionType","FOREGROUND","headers","totalBytesSent","onProgress","cancel","stop","interrupted","_resolve","reject","cancelAsync","timer","timeoutMs","addEventListener","once","result","race","uploadAsync","status","ok","clearTimeout","removeEventListener","openNativeFile","nativeHasher","requireOptionalNativeModule","size","hash","id","listener","addListener","event","loaded","cancelHash","createHash","digest","hashAsync","remove","readRange","output","Uint8Array","read","set","putPart","installNativeAssetFiles","oxy","assets","installFileAdapter"],"sourceRoot":"../../../../src","sources":["ui/utils/nativeAssetFiles.native.ts"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"OxyContext.d.ts","sourceRoot":"","sources":["../../../../../src/ui/context/OxyContext.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAqG/B,OAAO,KAAK,EAAmB,uBAAuB,EAAe,MAAM,mBAAmB,CAAC;AAQ/F,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAOhG,OAAO,EACL,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAovDhE,CAAC"}
1
+ {"version":3,"file":"OxyContext.d.ts","sourceRoot":"","sources":["../../../../../src/ui/context/OxyContext.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA6G/B,OAAO,KAAK,EAAmB,uBAAuB,EAAe,MAAM,mBAAmB,CAAC;AAQ/F,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAOhG,OAAO,EACL,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAuvDhE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+ /** Web/Node use their existing Blob transport. Metro selects the native adapter. */
3
+ export declare function installNativeAssetFiles(_oxy: OxyServices): undefined;
4
+ //# sourceMappingURL=nativeAssetFiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeAssetFiles.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/nativeAssetFiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,CAEpE"}
@@ -0,0 +1,4 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+ /** Web/Node use their existing Blob transport. Metro selects the native adapter. */
3
+ export declare function installNativeAssetFiles(_oxy: OxyServices): undefined;
4
+ //# sourceMappingURL=nativeAssetFiles.macos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeAssetFiles.macos.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/nativeAssetFiles.macos.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,CAEpE"}
@@ -0,0 +1,4 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+ /** One provider-owned adapter; no native filesystem module loads before an upload. */
3
+ export declare function installNativeAssetFiles(oxy: OxyServices): () => void;
4
+ //# sourceMappingURL=nativeAssetFiles.native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeAssetFiles.native.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/nativeAssetFiles.native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyC,WAAW,EAAE,MAAM,cAAc,CAAC;AAgNvF,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,IAAI,CAEpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"OxyContext.d.ts","sourceRoot":"","sources":["../../../../../src/ui/context/OxyContext.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAqG/B,OAAO,KAAK,EAAmB,uBAAuB,EAAe,MAAM,sBAAmB,CAAC;AAQ/F,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,sBAAmB,CAAC;AAOhG,OAAO,EACL,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,uBAAuB,GACxB,MAAM,wBAAqB,CAAC;AAE7B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAovDhE,CAAC"}
1
+ {"version":3,"file":"OxyContext.d.ts","sourceRoot":"","sources":["../../../../../src/ui/context/OxyContext.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA6G/B,OAAO,KAAK,EAAmB,uBAAuB,EAAe,MAAM,sBAAmB,CAAC;AAQ/F,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,sBAAmB,CAAC;AAOhG,OAAO,EACL,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,uBAAuB,GACxB,MAAM,wBAAqB,CAAC;AAE7B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAuvDhE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+ /** Web/Node use their existing Blob transport. Metro selects the native adapter. */
3
+ export declare function installNativeAssetFiles(_oxy: OxyServices): undefined;
4
+ //# sourceMappingURL=nativeAssetFiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeAssetFiles.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/nativeAssetFiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,CAEpE"}
@@ -0,0 +1,4 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+ /** Web/Node use their existing Blob transport. Metro selects the native adapter. */
3
+ export declare function installNativeAssetFiles(_oxy: OxyServices): undefined;
4
+ //# sourceMappingURL=nativeAssetFiles.macos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeAssetFiles.macos.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/nativeAssetFiles.macos.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,CAEpE"}
@@ -0,0 +1,4 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+ /** One provider-owned adapter; no native filesystem module loads before an upload. */
3
+ export declare function installNativeAssetFiles(oxy: OxyServices): () => void;
4
+ //# sourceMappingURL=nativeAssetFiles.native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeAssetFiles.native.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/nativeAssetFiles.native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyC,WAAW,EAAE,MAAM,cAAc,CAAC;AAgNvF,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,IAAI,CAEpE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/services",
3
- "version": "13.7.0",
3
+ "version": "13.8.0",
4
4
  "description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -107,6 +107,7 @@
107
107
  "android/build.gradle",
108
108
  "android/src",
109
109
  "ios/OxyIdentityModule.swift",
110
+ "ios/OxyAssetFilesModule.swift",
110
111
  "ios/OxyIdentity.podspec",
111
112
  "plugins",
112
113
  "expo-module.config.json"
@@ -160,7 +161,7 @@
160
161
  "@commitlint/config-conventional": "^21.2.0",
161
162
  "@expo/app-integrity": "~58.0.2",
162
163
  "@expo/vector-icons": "^15.0.3",
163
- "@oxy.so/core": "7.6.0",
164
+ "@oxy.so/core": "7.7.0",
164
165
  "@react-native-async-storage/async-storage": "^2.0.0",
165
166
  "@react-native-community/netinfo": "^12.0.1",
166
167
  "@react-native/babel-preset": "^0.88.0",
@@ -210,13 +211,13 @@
210
211
  "expo-video": "~58.0.12"
211
212
  },
212
213
  "//@oxy.so/bloom": "Bloom 11.1.0 adds the shared grouped sections, usage card, subscription plans, frame appearance and overlay navigation APIs used by this SDK. First-party Bloom peers remain open above this tested minimum.",
213
- "//@oxy.so/core": "Core 7.6.0 forwards guest locale and first-touch campaign metadata through signup; 7.3.0 supplies `users.lookupMany`, with which the account members screen fails (and retries) a profile read whose chunk failed instead of caching those members as handle-less; 7.2.0 supplies `assets.cachedUrl`, with which `useFileDownloadUrl` answers an already-resolved URL in its first render; 6.16.0 supplies `requireServerVerdict` and `onHolderRejected`, with which the cold boot checks a session it painted from the cache; 6.13.0 supplies the `accounts.settings.*` strings (picture, categories) the account settings speak in every language; 6.12.0 supplies `fileManagement.contentLost` and the `lost` file status a lost file is listed with; 6.11.0 supplies the `follow.errors.*` strings the follow error codes are worded with; 6.4.0 the `follow.*` strings the follow buttons read for every verb and language; 6.1.0 the `oxy.auth.email.{startChange,change,remove}` and `oxy.auth.password.remove` the sign-in & security panels call. Keep one Core instance per app; raise this floor with the root catalogue when new runtime APIs are required.",
214
+ "//@oxy.so/core": "Core 7.7.0 supplies the bounded native asset file adapter and phase progress installed by OxyProvider; 7.6.0 forwards guest locale and first-touch campaign metadata through signup; 7.3.0 supplies `users.lookupMany`, with which the account members screen fails (and retries) a profile read whose chunk failed instead of caching those members as handle-less; 7.2.0 supplies `assets.cachedUrl`, with which `useFileDownloadUrl` answers an already-resolved URL in its first render; 6.16.0 supplies `requireServerVerdict` and `onHolderRejected`, with which the cold boot checks a session it painted from the cache; 6.13.0 supplies the `accounts.settings.*` strings (picture, categories) the account settings speak in every language; 6.12.0 supplies `fileManagement.contentLost` and the `lost` file status a lost file is listed with; 6.11.0 supplies the `follow.errors.*` strings the follow error codes are worded with; 6.4.0 the `follow.*` strings the follow buttons read for every verb and language; 6.1.0 the `oxy.auth.email.{startChange,change,remove}` and `oxy.auth.password.remove` the sign-in & security panels call. Keep one Core instance per app; raise this floor with the root catalogue when new runtime APIs are required.",
214
215
  "//react-native": "React Native 0.81 to 0.88, React 19.1+ and Expo SDK 54 to 58. The floors are the Expo SDK 54 stack (react-native 0.81.6, react 19.1.4, react-native-web 0.21.4, expo 54.0.38 and the module versions its bundledNativeModules.json pins), which compat/expo-54 installs as an external consumer and checks on every CI run (`bun check.mjs`): every peer here admits that stack, services' published src typechecks against its types, and <OxyProvider> mounts and settles its cold boot on iOS (React Native 0.81), web (react-native-web 0.21) and react-native-macos 0.81 with the native modules macOS lacks absent. Astro, Oxy's browser, runs react-native-macos 0.81 (0.83 is its latest release), which is why the floor is not 0.85. The SDK 56 floors these replaced were the versions current when the ranges were first written, not measurements.",
215
216
  "peerDependencies": {
216
217
  "@expo/app-integrity": ">=0.1.10 <59.0.0",
217
218
  "@expo/vector-icons": "^15.0.3",
218
219
  "@oxy.so/bloom": ">=11.1.0",
219
- "@oxy.so/core": "^7.6.0",
220
+ "@oxy.so/core": "^7.7.0",
220
221
  "@react-native-async-storage/async-storage": "^2.0.0",
221
222
  "@react-native-community/netinfo": ">=11.4.1 <13.0.0",
222
223
  "@tanstack/query-async-storage-persister": "^5.101",
@@ -1,3 +1,4 @@
1
+ import { installNativeAssetFiles } from '../utils/nativeAssetFiles';
1
2
  import {
2
3
  startNativeOAuthSignIn,
3
4
  type StartNativeOAuthSignInOptions,
@@ -9,7 +10,15 @@ import {
9
10
  setIsolatedOAuthSession,
10
11
  } from './isolatedOAuthSession';
11
12
  import type React from 'react';
12
- import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
13
+ import {
14
+ useCallback,
15
+ useEffect,
16
+ useLayoutEffect,
17
+ useMemo,
18
+ useRef,
19
+ useState,
20
+ useSyncExternalStore,
21
+ } from 'react';
13
22
  import { AppState, Linking, Platform } from 'react-native';
14
23
  import { OxyServices } from '@oxy.so/core';
15
24
  import type { LoginSessionResult } from '@oxy.so/contracts';
@@ -182,6 +191,9 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
182
191
  }
183
192
  }
184
193
  const oxyServices = oxyServicesRef.current;
194
+ // Child passive effects may upload immediately on mount. Configure transport
195
+ // during commit, before those effects, without mutating the client in render.
196
+ useLayoutEffect(() => installNativeAssetFiles(oxyServices), [oxyServices]);
185
197
 
186
198
  // The device-first persisted auth-state store (per-origin device credential on
187
199
  // web; SecureStore session blob on native). Built ONCE per provider mount.
@@ -0,0 +1,270 @@
1
+ import type { AssetFileAdapter, OxyServices } from '@oxy.so/core';
2
+ import { installNativeAssetFiles } from '../nativeAssetFiles.native';
3
+
4
+ const originalUri = 'file:///capture.mp4';
5
+ const mockFiles = new Map<string, { size: number; written: number }>();
6
+ const mockReads: number[] = [];
7
+ const mockDeleted: string[] = [];
8
+ let mockNativeHasher: unknown = null;
9
+ jest.mock('expo-modules-core', () => ({ requireOptionalNativeModule: () => mockNativeHasher }));
10
+ let mockUpload: jest.Mock;
11
+ let mockCancel: jest.Mock;
12
+ let mockRequest: { url: string; uri: string; options: unknown } | undefined;
13
+
14
+ jest.mock('expo-file-system', () => {
15
+ const uriOf = (value: string | { uri: string }) =>
16
+ typeof value === 'string' ? value : value.uri;
17
+ class Directory {
18
+ uri: string;
19
+ constructor(base: string, name: string) {
20
+ this.uri = `${base}/${name}`;
21
+ }
22
+ get exists() {
23
+ return false;
24
+ }
25
+ create() {}
26
+ delete() {}
27
+ }
28
+ class File {
29
+ uri: string;
30
+ constructor(base: string | { uri: string }, name?: string) {
31
+ this.uri = name ? `${uriOf(base)}/${name}` : uriOf(base);
32
+ }
33
+ get exists() {
34
+ return mockFiles.has(this.uri);
35
+ }
36
+ get size() {
37
+ return mockFiles.get(this.uri)?.size ?? 0;
38
+ }
39
+ create() {
40
+ mockFiles.set(this.uri, { size: 0, written: 0 });
41
+ }
42
+ delete() {
43
+ mockDeleted.push(this.uri);
44
+ mockFiles.delete(this.uri);
45
+ }
46
+ open() {
47
+ const uri = this.uri;
48
+ let offset = 0;
49
+ let closed = false;
50
+ return {
51
+ get offset() {
52
+ return offset;
53
+ },
54
+ set offset(next: number) {
55
+ offset = next;
56
+ },
57
+ readBytes(length: number) {
58
+ if (closed) throw new Error('Closed');
59
+ mockReads.push(length);
60
+ const size = mockFiles.get(uri)?.size ?? 0;
61
+ const bytes = new Uint8Array(Math.max(0, Math.min(length, size - offset))).fill(7);
62
+ offset += bytes.length;
63
+ return bytes;
64
+ },
65
+ writeBytes(bytes: Uint8Array) {
66
+ if (closed) throw new Error('Closed');
67
+ const file = mockFiles.get(uri);
68
+ if (!file) throw new Error('Missing target');
69
+ file.written += bytes.length;
70
+ file.size += bytes.length;
71
+ },
72
+ close() {
73
+ closed = true;
74
+ },
75
+ };
76
+ }
77
+ }
78
+ return { File, Directory, Paths: { cache: 'file:///cache' } };
79
+ });
80
+ jest.mock('expo-file-system/legacy', () => ({
81
+ FileSystemUploadType: { BINARY_CONTENT: 0 },
82
+ FileSystemSessionType: { FOREGROUND: 0 },
83
+ createUploadTask: (
84
+ url: string,
85
+ uri: string,
86
+ options: unknown,
87
+ progress: (event: { totalBytesSent: number }) => void,
88
+ ) => {
89
+ mockRequest = { url, uri, options };
90
+ return { uploadAsync: () => mockUpload(uri, progress), cancelAsync: () => mockCancel() };
91
+ },
92
+ }));
93
+
94
+ function adapter(): AssetFileAdapter {
95
+ let installed: AssetFileAdapter | undefined;
96
+ const oxy = {
97
+ assets: {
98
+ installFileAdapter: (value: AssetFileAdapter) => {
99
+ installed = value;
100
+ return () => {};
101
+ },
102
+ },
103
+ } as unknown as OxyServices;
104
+ installNativeAssetFiles(oxy);
105
+ if (!installed) throw new Error('Adapter was not installed');
106
+ return installed;
107
+ }
108
+
109
+ beforeEach(() => {
110
+ mockNativeHasher = null;
111
+ mockFiles.clear();
112
+ mockFiles.set(originalUri, { size: 370 * 1024 * 1024, written: 0 });
113
+ mockReads.length = 0;
114
+ mockDeleted.length = 0;
115
+ mockRequest = undefined;
116
+ mockCancel = jest.fn(async () => undefined);
117
+ mockUpload = jest.fn(
118
+ async (uri: string, progress: (event: { totalBytesSent: number }) => void) => {
119
+ progress({ totalBytesSent: mockFiles.get(uri)?.size ?? 0 });
120
+ return { status: 200 };
121
+ },
122
+ );
123
+ });
124
+
125
+ test('reads bounded ranges and streams only a temporary part of a larger-than-heap file', async () => {
126
+ const source = await adapter()(originalUri);
127
+ expect(source.size).toBe(370 * 1024 * 1024);
128
+ const bytes = await source.readRange(16 * 1024 * 1024, 24 * 1024 * 1024);
129
+ expect(bytes.length).toBe(8 * 1024 * 1024);
130
+ expect(bytes[bytes.length - 1]).toBe(7);
131
+ const onProgress = jest.fn();
132
+ await expect(
133
+ source.putPart({
134
+ url: 'https://storage.example/part?signed=1',
135
+ start: 32 * 1024 * 1024,
136
+ end: 40 * 1024 * 1024,
137
+ timeoutMs: 1000,
138
+ onProgress,
139
+ }),
140
+ ).resolves.toEqual({ ok: true });
141
+ expect(Math.max(...mockReads)).toBe(1024 * 1024);
142
+ expect(mockReads.reduce((sum, size) => sum + size, 0)).toBe(16 * 1024 * 1024);
143
+ expect(mockRequest).toMatchObject({
144
+ url: 'https://storage.example/part?signed=1',
145
+ options: { httpMethod: 'PUT', uploadType: 0, sessionType: 0, headers: {} },
146
+ });
147
+ expect(onProgress).toHaveBeenCalledWith(8 * 1024 * 1024);
148
+ expect(mockFiles.has(originalUri)).toBe(true);
149
+ expect(mockFiles.size).toBe(1);
150
+ expect(mockDeleted).toHaveLength(1);
151
+ });
152
+
153
+ test('abort cancels the native transfer and deletes its part while preserving the capture', async () => {
154
+ const source = await adapter()(originalUri);
155
+ const controller = new AbortController();
156
+ mockUpload.mockImplementation(() => new Promise(() => {}));
157
+ const pending = source.putPart({
158
+ url: 'https://storage.example/part',
159
+ start: 0,
160
+ end: 2 * 1024 * 1024,
161
+ timeoutMs: 1000,
162
+ signal: controller.signal,
163
+ onProgress: jest.fn(),
164
+ });
165
+ while (!mockRequest) await new Promise((resolve) => setTimeout(resolve, 0));
166
+ controller.abort();
167
+ await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
168
+ expect(mockCancel).toHaveBeenCalledTimes(1);
169
+ expect(mockFiles.size).toBe(1);
170
+ expect(mockFiles.has(originalUri)).toBe(true);
171
+ });
172
+
173
+ test('a stalled native PUT times out and cleans its temporary part', async () => {
174
+ const source = await adapter()(originalUri);
175
+ mockUpload.mockImplementation(() => new Promise(() => {}));
176
+ await expect(
177
+ source.putPart({
178
+ url: 'https://storage.example/part',
179
+ start: 0,
180
+ end: 1024,
181
+ timeoutMs: 5,
182
+ onProgress: jest.fn(),
183
+ }),
184
+ ).resolves.toEqual({ ok: false, status: 0 });
185
+ expect(mockCancel).toHaveBeenCalledTimes(1);
186
+ expect(mockFiles.size).toBe(1);
187
+ });
188
+
189
+ test('hash reads let scheduled UI work run before the complete range is read', async () => {
190
+ const source = await adapter()(originalUri);
191
+ let readsAtTick = -1;
192
+ const tick = new Promise<void>((resolve) =>
193
+ setTimeout(() => {
194
+ readsAtTick = mockReads.length;
195
+ resolve();
196
+ }, 0),
197
+ );
198
+ await source.readRange(0, 8 * 1024 * 1024);
199
+ await tick;
200
+ expect(readsAtTick).toBeGreaterThan(0);
201
+ expect(readsAtTick).toBeLessThan(8);
202
+ });
203
+
204
+ test('an abort timer interrupts part preparation before copying the complete range', async () => {
205
+ const source = await adapter()(originalUri);
206
+ const controller = new AbortController();
207
+ setTimeout(() => controller.abort(), 0);
208
+ await expect(
209
+ source.putPart({
210
+ url: 'https://storage.example/part',
211
+ start: 0,
212
+ end: 8 * 1024 * 1024,
213
+ timeoutMs: 1000,
214
+ signal: controller.signal,
215
+ onProgress: jest.fn(),
216
+ }),
217
+ ).rejects.toMatchObject({ name: 'AbortError' });
218
+ expect(mockReads.length).toBeLessThan(8);
219
+ expect(mockUpload).not.toHaveBeenCalled();
220
+ expect(mockFiles.size).toBe(1);
221
+ expect(mockFiles.has(originalUri)).toBe(true);
222
+ });
223
+
224
+ test('uses the native streaming hash without reading bytes into JavaScript', async () => {
225
+ let progress: (event: { id: string; loaded: number }) => void;
226
+ const remove = jest.fn();
227
+ const digest = 'ab'.repeat(32);
228
+ mockNativeHasher = {
229
+ createHash: jest.fn(),
230
+ cancelHash: jest.fn(),
231
+ addListener: jest.fn((_event, callback) => {
232
+ progress = callback;
233
+ return { remove };
234
+ }),
235
+ hashAsync: jest.fn(async (id, _uri, size) => {
236
+ progress({ id: 'another-upload', loaded: 1 });
237
+ progress({ id, loaded: size });
238
+ return digest;
239
+ }),
240
+ };
241
+ const source = await adapter()(originalUri);
242
+ const onProgress = jest.fn();
243
+ await expect(source.hash?.(onProgress)).resolves.toBe(digest);
244
+ expect(mockReads).toHaveLength(0);
245
+ expect(onProgress).toHaveBeenCalledTimes(1);
246
+ expect(onProgress).toHaveBeenCalledWith(source.size);
247
+ expect(remove).toHaveBeenCalledTimes(1);
248
+ });
249
+
250
+ test('native hash cancellation waits for worker cleanup and removes its listener', async () => {
251
+ const controller = new AbortController();
252
+ const remove = jest.fn();
253
+ let finish: (value: string) => void;
254
+ const cancelHash = jest.fn(() => finish('ab'.repeat(32)));
255
+ mockNativeHasher = {
256
+ createHash: jest.fn(),
257
+ cancelHash,
258
+ addListener: () => ({ remove }),
259
+ hashAsync: () =>
260
+ new Promise<string>((resolve) => {
261
+ finish = resolve;
262
+ }),
263
+ };
264
+ const source = await adapter()(originalUri);
265
+ const pending = source.hash?.(undefined, controller.signal);
266
+ controller.abort();
267
+ await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
268
+ expect(cancelHash).toHaveBeenCalledTimes(1);
269
+ expect(remove).toHaveBeenCalledTimes(1);
270
+ });
@@ -0,0 +1,6 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+
3
+ /** Web/Node use their existing Blob transport. Metro selects the native adapter. */
4
+ export function installNativeAssetFiles(_oxy: OxyServices): undefined {
5
+ return undefined;
6
+ }