@oxy.so/services 13.7.0 → 13.9.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 (76) 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/index.js +10 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/ui/components/OxyDeviceSignIn.js +192 -0
  10. package/lib/commonjs/ui/components/OxyDeviceSignIn.js.map +1 -0
  11. package/lib/commonjs/ui/context/OxyContext.js +4 -0
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js +1 -0
  14. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js.map +1 -1
  15. package/lib/commonjs/ui/index.js +7 -0
  16. package/lib/commonjs/ui/index.js.map +1 -1
  17. package/lib/commonjs/ui/utils/nativeAssetFiles.js +11 -0
  18. package/lib/commonjs/ui/utils/nativeAssetFiles.js.map +1 -0
  19. package/lib/commonjs/ui/utils/nativeAssetFiles.macos.js +11 -0
  20. package/lib/commonjs/ui/utils/nativeAssetFiles.macos.js.map +1 -0
  21. package/lib/commonjs/ui/utils/nativeAssetFiles.native.js +202 -0
  22. package/lib/commonjs/ui/utils/nativeAssetFiles.native.js.map +1 -0
  23. package/lib/module/index.js +3 -0
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/ui/components/OxyDeviceSignIn.js +188 -0
  26. package/lib/module/ui/components/OxyDeviceSignIn.js.map +1 -0
  27. package/lib/module/ui/context/OxyContext.js +5 -1
  28. package/lib/module/ui/context/OxyContext.js.map +1 -1
  29. package/lib/module/ui/hooks/accountDialogSnapshot.js +1 -0
  30. package/lib/module/ui/hooks/accountDialogSnapshot.js.map +1 -1
  31. package/lib/module/ui/index.js +1 -0
  32. package/lib/module/ui/index.js.map +1 -1
  33. package/lib/module/ui/utils/nativeAssetFiles.js +7 -0
  34. package/lib/module/ui/utils/nativeAssetFiles.js.map +1 -0
  35. package/lib/module/ui/utils/nativeAssetFiles.macos.js +7 -0
  36. package/lib/module/ui/utils/nativeAssetFiles.macos.js.map +1 -0
  37. package/lib/module/ui/utils/nativeAssetFiles.native.js +197 -0
  38. package/lib/module/ui/utils/nativeAssetFiles.native.js.map +1 -0
  39. package/lib/typescript/commonjs/index.d.ts +2 -0
  40. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/ui/components/OxyDeviceSignIn.d.ts +29 -0
  42. package/lib/typescript/commonjs/ui/components/OxyDeviceSignIn.d.ts.map +1 -0
  43. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/ui/index.d.ts +2 -0
  46. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.d.ts +4 -0
  48. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.d.ts.map +1 -0
  49. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.macos.d.ts +4 -0
  50. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.macos.d.ts.map +1 -0
  51. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.native.d.ts +4 -0
  52. package/lib/typescript/commonjs/ui/utils/nativeAssetFiles.native.d.ts.map +1 -0
  53. package/lib/typescript/module/index.d.ts +2 -0
  54. package/lib/typescript/module/index.d.ts.map +1 -1
  55. package/lib/typescript/module/ui/components/OxyDeviceSignIn.d.ts +29 -0
  56. package/lib/typescript/module/ui/components/OxyDeviceSignIn.d.ts.map +1 -0
  57. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  58. package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
  59. package/lib/typescript/module/ui/index.d.ts +2 -0
  60. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  61. package/lib/typescript/module/ui/utils/nativeAssetFiles.d.ts +4 -0
  62. package/lib/typescript/module/ui/utils/nativeAssetFiles.d.ts.map +1 -0
  63. package/lib/typescript/module/ui/utils/nativeAssetFiles.macos.d.ts +4 -0
  64. package/lib/typescript/module/ui/utils/nativeAssetFiles.macos.d.ts.map +1 -0
  65. package/lib/typescript/module/ui/utils/nativeAssetFiles.native.d.ts +4 -0
  66. package/lib/typescript/module/ui/utils/nativeAssetFiles.native.d.ts.map +1 -0
  67. package/package.json +5 -4
  68. package/src/index.ts +4 -0
  69. package/src/ui/components/OxyDeviceSignIn.tsx +172 -0
  70. package/src/ui/context/OxyContext.tsx +13 -1
  71. package/src/ui/hooks/accountDialogSnapshot.ts +1 -0
  72. package/src/ui/index.ts +2 -0
  73. package/src/ui/utils/__tests__/nativeAssetFiles.test.ts +270 -0
  74. package/src/ui/utils/nativeAssetFiles.macos.ts +6 -0
  75. package/src/ui/utils/nativeAssetFiles.native.ts +212 -0
  76. package/src/ui/utils/nativeAssetFiles.ts +6 -0
@@ -0,0 +1,212 @@
1
+ import type { AssetFilePartRequest, AssetFileSource, OxyServices } from '@oxy.so/core';
2
+ import type { Directory, File } from 'expo-file-system';
3
+
4
+ // File.slice() reads the WHOLE file on Expo; RN Blob does the same on Java's
5
+ // heap. FileHandle reads and native file uploads keep both heaps bounded.
6
+ const COPY_CHUNK_BYTES = 1024 * 1024;
7
+ let partsDirectory: Promise<Directory> | undefined;
8
+ let nextPart = 0;
9
+ let nextHash = 0;
10
+ interface NativeFileHasher {
11
+ createHash(id: string): void;
12
+ cancelHash(id: string): void;
13
+ hashAsync(id: string, uri: string, expectedSize: number): Promise<string>;
14
+ addListener(
15
+ event: 'hashProgress',
16
+ listener: (event: { id: string; loaded: number }) => void,
17
+ ): { remove(): void };
18
+ }
19
+
20
+ // FileHandle methods are synchronous: awaiting them alone only yields a
21
+ // microtask. Let rendering, native events and abort timers run between chunks.
22
+ function yieldToEventLoop(): Promise<void> {
23
+ return new Promise((resolve) => setTimeout(resolve, 0));
24
+ }
25
+
26
+ function abortError(): Error {
27
+ const error = new Error('Upload aborted');
28
+ error.name = 'AbortError';
29
+ return error;
30
+ }
31
+
32
+ function checkAbort(signal?: AbortSignal): void {
33
+ if (signal?.aborted) throw abortError();
34
+ }
35
+
36
+ async function temporaryParts(): Promise<Directory> {
37
+ partsDirectory ??= import('expo-file-system')
38
+ .then(({ Directory, Paths }) => {
39
+ const directory = new Directory(Paths.cache, 'oxy-upload-parts');
40
+ // These are foreground transfers. A new JS runtime owns no previous parts;
41
+ // remove leftovers from a killed process before creating any new part.
42
+ if (directory.exists) directory.delete();
43
+ directory.create({ intermediates: true });
44
+ return directory;
45
+ })
46
+ .catch((error) => {
47
+ partsDirectory = undefined;
48
+ throw error;
49
+ });
50
+ return partsDirectory;
51
+ }
52
+
53
+ async function copyRange(
54
+ file: File,
55
+ target: File,
56
+ start: number,
57
+ end: number,
58
+ signal?: AbortSignal,
59
+ ): Promise<void> {
60
+ const reader = file.open();
61
+ try {
62
+ reader.offset = start;
63
+ const writer = target.open();
64
+ try {
65
+ for (let offset = start; offset < end; ) {
66
+ checkAbort(signal);
67
+ const bytes = await reader.readBytes(Math.min(COPY_CHUNK_BYTES, end - offset));
68
+ if (!bytes.length) throw new Error('File changed while preparing upload');
69
+ await writer.writeBytes(bytes);
70
+ offset += bytes.length;
71
+ await yieldToEventLoop();
72
+ }
73
+ } finally {
74
+ writer.close();
75
+ }
76
+ } finally {
77
+ reader.close();
78
+ }
79
+ }
80
+
81
+ async function putRange(
82
+ file: File,
83
+ request: AssetFilePartRequest,
84
+ ): ReturnType<AssetFileSource['putPart']> {
85
+ const [{ File }, legacy, directory] = await Promise.all([
86
+ import('expo-file-system'),
87
+ import('expo-file-system/legacy'),
88
+ temporaryParts(),
89
+ ]);
90
+ checkAbort(request.signal);
91
+ const part = new File(directory, `part-${++nextPart}`);
92
+ part.create();
93
+ try {
94
+ await copyRange(file, part, request.start, request.end, request.signal);
95
+ checkAbort(request.signal);
96
+ const task = legacy.createUploadTask(
97
+ request.url,
98
+ part.uri,
99
+ {
100
+ httpMethod: 'PUT',
101
+ uploadType: legacy.FileSystemUploadType.BINARY_CONTENT,
102
+ sessionType: legacy.FileSystemSessionType.FOREGROUND,
103
+ headers: {},
104
+ },
105
+ ({ totalBytesSent }) => request.onProgress(totalBytesSent),
106
+ );
107
+ let cancel: Promise<void> | undefined;
108
+ let stop = () => {};
109
+ const interrupted = new Promise<never>((_resolve, reject) => {
110
+ stop = () => {
111
+ cancel ??= task.cancelAsync();
112
+ void cancel.catch(() => undefined);
113
+ reject(abortError());
114
+ };
115
+ });
116
+ const timer = setTimeout(() => {
117
+ stop();
118
+ }, request.timeoutMs);
119
+ request.signal?.addEventListener('abort', stop, { once: true });
120
+ try {
121
+ checkAbort(request.signal);
122
+ const result = await Promise.race([task.uploadAsync(), interrupted]);
123
+ if (!result) throw abortError();
124
+ return result.status >= 200 && result.status < 300
125
+ ? { ok: true }
126
+ : { ok: false, status: result.status };
127
+ } catch {
128
+ if (request.signal?.aborted) throw abortError();
129
+ // The core session owner applies its bounded retry/backoff policy.
130
+ return { ok: false, status: 0 };
131
+ } finally {
132
+ clearTimeout(timer);
133
+ request.signal?.removeEventListener('abort', stop);
134
+ if (cancel) await cancel.catch(() => undefined);
135
+ }
136
+ } finally {
137
+ if (part.exists) part.delete();
138
+ }
139
+ }
140
+
141
+ async function openNativeFile(uri: string): Promise<AssetFileSource> {
142
+ const { File } = await import('expo-file-system');
143
+ let nativeHasher: NativeFileHasher | null = null;
144
+ try {
145
+ const { requireOptionalNativeModule } = await import('expo-modules-core');
146
+ nativeHasher = requireOptionalNativeModule<NativeFileHasher>('OxyAssetFiles');
147
+ } catch {
148
+ // Hosts without the optional Expo module bridge retain bounded file access.
149
+ }
150
+ const file = new File(uri);
151
+ if (!file.exists) throw new Error('Native upload file is unavailable');
152
+ const size = file.size;
153
+ return {
154
+ size,
155
+ // Older native binaries can still use bounded JS reads after an OTA. New
156
+ // binaries hash entirely on a native worker, without sending bytes to JS.
157
+ hash: nativeHasher
158
+ ? async (onProgress, signal) => {
159
+ checkAbort(signal);
160
+ if (file.size !== size) throw new Error('File changed while hashing');
161
+ const id = `upload-hash-${++nextHash}`;
162
+ const listener = nativeHasher.addListener('hashProgress', (event) => {
163
+ if (event.id === id && !signal?.aborted) onProgress?.(event.loaded);
164
+ });
165
+ const stop = () => nativeHasher.cancelHash(id);
166
+ try {
167
+ nativeHasher.createHash(id);
168
+ signal?.addEventListener('abort', stop, { once: true });
169
+ if (signal?.aborted) stop();
170
+ const digest = await nativeHasher.hashAsync(id, uri, size);
171
+ checkAbort(signal);
172
+ return digest;
173
+ } catch (error) {
174
+ checkAbort(signal);
175
+ throw error;
176
+ } finally {
177
+ signal?.removeEventListener('abort', stop);
178
+ listener.remove();
179
+ }
180
+ }
181
+ : undefined,
182
+ async readRange(start, end) {
183
+ if (file.size !== size) throw new Error('File changed while uploading');
184
+ if (start < 0 || end > size || end < start) throw new Error('Invalid upload file range');
185
+ const reader = file.open();
186
+ try {
187
+ reader.offset = start;
188
+ const output = new Uint8Array(end - start);
189
+ let read = 0;
190
+ while (read < output.length) {
191
+ const bytes = await reader.readBytes(Math.min(COPY_CHUNK_BYTES, output.length - read));
192
+ if (!bytes.length) throw new Error('File changed while hashing');
193
+ output.set(bytes, read);
194
+ read += bytes.length;
195
+ await yieldToEventLoop();
196
+ }
197
+ return output;
198
+ } finally {
199
+ reader.close();
200
+ }
201
+ },
202
+ putPart: (request) => {
203
+ if (file.size !== size) throw new Error('File changed while uploading');
204
+ return putRange(file, request);
205
+ },
206
+ };
207
+ }
208
+
209
+ /** One provider-owned adapter; no native filesystem module loads before an upload. */
210
+ export function installNativeAssetFiles(oxy: OxyServices): () => void {
211
+ return oxy.assets.installFileAdapter(openNativeFile);
212
+ }
@@ -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
+ }