@delali/narsil-react-native 0.0.0-stage → 0.3.1-next.146

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 (186) hide show
  1. package/LICENSE +201 -0
  2. package/NarsilReactNative.podspec +57 -0
  3. package/README.md +46 -2
  4. package/android/CMakeLists.txt +48 -0
  5. package/android/build.gradle +151 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/AndroidEnvironment.cpp +44 -0
  9. package/android/src/main/cpp/AndroidEnvironment.hpp +11 -0
  10. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  11. package/android/src/main/java/com/margelo/nitro/narsil/NarsilAndroidEnvironment.kt +29 -0
  12. package/android/src/main/java/com/margelo/nitro/narsil/NarsilReactNativePackage.kt +22 -0
  13. package/app.plugin.js +25 -0
  14. package/cpp/AttachedField.cpp +234 -0
  15. package/cpp/AttachedField.hpp +61 -0
  16. package/cpp/Compression.cpp +113 -0
  17. package/cpp/Compression.hpp +16 -0
  18. package/cpp/GraphHeldAlone.cpp +29 -0
  19. package/cpp/GraphHeldAlone.hpp +25 -0
  20. package/cpp/HostMemory.cpp +123 -0
  21. package/cpp/HostMemory.hpp +53 -0
  22. package/cpp/HybridNarsilCoreImpl.cpp +390 -0
  23. package/cpp/HybridNarsilCoreImpl.hpp +68 -0
  24. package/cpp/HybridNarsilFilesImpl.cpp +245 -0
  25. package/cpp/HybridNarsilFilesImpl.hpp +34 -0
  26. package/cpp/HybridNarsilHostImpl.cpp +126 -0
  27. package/cpp/HybridNarsilHostImpl.hpp +42 -0
  28. package/cpp/NativeAtomics.cpp +110 -0
  29. package/cpp/NativeAtomics.hpp +11 -0
  30. package/cpp/ProcessInfo.cpp +83 -0
  31. package/cpp/ProcessInfo.hpp +11 -0
  32. package/cpp/ThreadPriority.cpp +77 -0
  33. package/cpp/ThreadPriority.hpp +8 -0
  34. package/cpp/WorkletRuntimeDispatcher.cpp +61 -0
  35. package/cpp/WorkletRuntimeDispatcher.hpp +9 -0
  36. package/cpp/core/include/narsil_core.h +208 -0
  37. package/cpp/core/src/connect.c +213 -0
  38. package/cpp/core/src/distance.c +103 -0
  39. package/cpp/core/src/files.c +99 -0
  40. package/cpp/core/src/heap.c +84 -0
  41. package/cpp/core/src/internal.h +284 -0
  42. package/cpp/core/src/kernels_code.c +209 -0
  43. package/cpp/core/src/kernels_float.c +197 -0
  44. package/cpp/core/src/kernels_pair.c +133 -0
  45. package/cpp/core/src/last_links.c +145 -0
  46. package/cpp/core/src/links.c +195 -0
  47. package/cpp/core/src/locks.c +168 -0
  48. package/cpp/core/src/nearest_links.c +147 -0
  49. package/cpp/core/src/place.c +215 -0
  50. package/cpp/core/src/quantize.c +226 -0
  51. package/cpp/core/src/records.c +97 -0
  52. package/cpp/core/src/remove.c +205 -0
  53. package/cpp/core/src/vectors.c +139 -0
  54. package/cpp/core/src/walk.c +231 -0
  55. package/cpp/core/src/workspace.c +167 -0
  56. package/ios/PrivacyInfo.xcprivacy +23 -0
  57. package/lib/client.d.ts +19 -0
  58. package/lib/client.js +41 -0
  59. package/lib/constants.d.ts +9 -0
  60. package/lib/constants.js +9 -0
  61. package/lib/engine/byte-files.d.ts +5 -0
  62. package/lib/engine/byte-files.js +97 -0
  63. package/lib/engine/core.d.ts +3 -0
  64. package/lib/engine/core.js +113 -0
  65. package/lib/engine/files.d.ts +3 -0
  66. package/lib/engine/files.js +188 -0
  67. package/lib/engine/host.d.ts +13 -0
  68. package/lib/engine/host.js +102 -0
  69. package/lib/engine/install.d.ts +4 -0
  70. package/lib/engine/install.js +29 -0
  71. package/lib/engine/native-error.d.ts +1 -0
  72. package/lib/engine/native-error.js +12 -0
  73. package/lib/engine/stand-ins/atomics.d.ts +5 -0
  74. package/lib/engine/stand-ins/atomics.js +236 -0
  75. package/lib/engine/stand-ins/crypto.d.ts +2 -0
  76. package/lib/engine/stand-ins/crypto.js +65 -0
  77. package/lib/engine/stand-ins/process.d.ts +2 -0
  78. package/lib/engine/stand-ins/process.js +22 -0
  79. package/lib/engine/stand-ins/streams.d.ts +1 -0
  80. package/lib/engine/stand-ins/streams.js +31 -0
  81. package/lib/engine/stand-ins/structured-clone.d.ts +1 -0
  82. package/lib/engine/stand-ins/structured-clone.js +121 -0
  83. package/lib/engine/streamed.d.ts +6 -0
  84. package/lib/engine/streamed.js +29 -0
  85. package/lib/engine/transfer.d.ts +3 -0
  86. package/lib/engine/transfer.js +77 -0
  87. package/lib/errors.d.ts +25 -0
  88. package/lib/errors.js +59 -0
  89. package/lib/index.d.ts +3 -0
  90. package/lib/index.js +2 -0
  91. package/lib/methods.d.ts +5 -0
  92. package/lib/methods.js +47 -0
  93. package/lib/native-objects.d.ts +2 -0
  94. package/lib/native-objects.js +24 -0
  95. package/lib/specs/core.nitro.d.ts +79 -0
  96. package/lib/specs/core.nitro.js +1 -0
  97. package/lib/specs/files.nitro.d.ts +36 -0
  98. package/lib/specs/files.nitro.js +1 -0
  99. package/lib/specs/host.nitro.d.ts +24 -0
  100. package/lib/specs/host.nitro.js +1 -0
  101. package/lib/transferred.d.ts +15 -0
  102. package/lib/transferred.js +6 -0
  103. package/lib/types.d.ts +136 -0
  104. package/lib/types.js +1 -0
  105. package/lib/worklets/app-turns.d.ts +1 -0
  106. package/lib/worklets/app-turns.js +44 -0
  107. package/lib/worklets/client.d.ts +30 -0
  108. package/lib/worklets/client.js +144 -0
  109. package/lib/worklets/index.d.ts +4 -0
  110. package/lib/worklets/index.js +2 -0
  111. package/lib/worklets/instances.d.ts +15 -0
  112. package/lib/worklets/instances.js +194 -0
  113. package/lib/worklets/json-part.d.ts +1 -0
  114. package/lib/worklets/json-part.js +42 -0
  115. package/lib/worklets/runtime.d.ts +25 -0
  116. package/lib/worklets/runtime.js +80 -0
  117. package/lib/worklets/transfer-queue.d.ts +7 -0
  118. package/lib/worklets/transfer-queue.js +79 -0
  119. package/lib/worklets/types.d.ts +26 -0
  120. package/lib/worklets/types.js +1 -0
  121. package/nitro.json +32 -0
  122. package/nitrogen/generated/.gitattributes +1 -0
  123. package/nitrogen/generated/android/NarsilReactNative+autolinking.cmake +85 -0
  124. package/nitrogen/generated/android/NarsilReactNative+autolinking.gradle +27 -0
  125. package/nitrogen/generated/android/NarsilReactNativeOnLoad.cpp +69 -0
  126. package/nitrogen/generated/android/NarsilReactNativeOnLoad.hpp +34 -0
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/narsil/NarsilReactNativeOnLoad.kt +35 -0
  128. package/nitrogen/generated/ios/NarsilReactNative+autolinking.rb +62 -0
  129. package/nitrogen/generated/ios/NarsilReactNative-Swift-Cxx-Bridge.cpp +17 -0
  130. package/nitrogen/generated/ios/NarsilReactNative-Swift-Cxx-Bridge.hpp +27 -0
  131. package/nitrogen/generated/ios/NarsilReactNative-Swift-Cxx-Umbrella.hpp +38 -0
  132. package/nitrogen/generated/ios/NarsilReactNativeAutolinking.mm +55 -0
  133. package/nitrogen/generated/ios/NarsilReactNativeAutolinking.swift +16 -0
  134. package/nitrogen/generated/shared/c++/DirectoryEntry.hpp +91 -0
  135. package/nitrogen/generated/shared/c++/FileStatus.hpp +103 -0
  136. package/nitrogen/generated/shared/c++/GraphMemory.hpp +120 -0
  137. package/nitrogen/generated/shared/c++/HybridNarsilAttachedGraphSpec.cpp +21 -0
  138. package/nitrogen/generated/shared/c++/HybridNarsilAttachedGraphSpec.hpp +62 -0
  139. package/nitrogen/generated/shared/c++/HybridNarsilAttachedStoreSpec.cpp +21 -0
  140. package/nitrogen/generated/shared/c++/HybridNarsilAttachedStoreSpec.hpp +62 -0
  141. package/nitrogen/generated/shared/c++/HybridNarsilCoreSpec.cpp +35 -0
  142. package/nitrogen/generated/shared/c++/HybridNarsilCoreSpec.hpp +100 -0
  143. package/nitrogen/generated/shared/c++/HybridNarsilFilesSpec.cpp +37 -0
  144. package/nitrogen/generated/shared/c++/HybridNarsilFilesSpec.hpp +86 -0
  145. package/nitrogen/generated/shared/c++/HybridNarsilHostSpec.cpp +34 -0
  146. package/nitrogen/generated/shared/c++/HybridNarsilHostSpec.hpp +78 -0
  147. package/nitrogen/generated/shared/c++/MemoryView.hpp +91 -0
  148. package/nitrogen/generated/shared/c++/NativeConnectOutcome.hpp +87 -0
  149. package/nitrogen/generated/shared/c++/NativeScores.hpp +87 -0
  150. package/nitrogen/generated/shared/c++/NativeSearchHits.hpp +91 -0
  151. package/nitrogen/generated/shared/c++/ProcessMemory.hpp +87 -0
  152. package/nitrogen/generated/shared/c++/StoreMemory.hpp +142 -0
  153. package/package.json +87 -3
  154. package/react-native.config.js +8 -0
  155. package/src/client.ts +44 -0
  156. package/src/constants.ts +9 -0
  157. package/src/engine/byte-files.ts +99 -0
  158. package/src/engine/core.ts +162 -0
  159. package/src/engine/files.ts +186 -0
  160. package/src/engine/host.ts +111 -0
  161. package/src/engine/install.ts +31 -0
  162. package/src/engine/native-error.ts +10 -0
  163. package/src/engine/stand-ins/atomics.ts +288 -0
  164. package/src/engine/stand-ins/crypto.ts +81 -0
  165. package/src/engine/stand-ins/process.ts +30 -0
  166. package/src/engine/stand-ins/streams.ts +45 -0
  167. package/src/engine/stand-ins/structured-clone.ts +118 -0
  168. package/src/engine/streamed.ts +40 -0
  169. package/src/engine/transfer.ts +72 -0
  170. package/src/errors.ts +73 -0
  171. package/src/index.ts +3 -0
  172. package/src/methods.ts +52 -0
  173. package/src/native-objects.ts +30 -0
  174. package/src/specs/core.nitro.ts +140 -0
  175. package/src/specs/files.nitro.ts +42 -0
  176. package/src/specs/host.nitro.ts +30 -0
  177. package/src/transferred.ts +18 -0
  178. package/src/types.ts +164 -0
  179. package/src/worklets/app-turns.ts +48 -0
  180. package/src/worklets/client.ts +165 -0
  181. package/src/worklets/index.ts +4 -0
  182. package/src/worklets/instances.ts +231 -0
  183. package/src/worklets/json-part.ts +38 -0
  184. package/src/worklets/runtime.ts +190 -0
  185. package/src/worklets/transfer-queue.ts +98 -0
  186. package/src/worklets/types.ts +37 -0
package/src/types.ts ADDED
@@ -0,0 +1,164 @@
1
+ import type {
2
+ AnalysisChange,
3
+ AnyDocument,
4
+ BatchResult,
5
+ BM25Params,
6
+ DocumentImportResult,
7
+ DurabilityConfig,
8
+ IndexConfig,
9
+ IndexInfo,
10
+ IndexStats,
11
+ InsertOptions,
12
+ ListParams,
13
+ ListResult,
14
+ MemoryStats,
15
+ NarsilConfig,
16
+ NarsilEventMap,
17
+ PartitionConfig,
18
+ PartitionStatsResult,
19
+ PreflightResult,
20
+ QueryParams,
21
+ QueryResult,
22
+ SuggestParams,
23
+ SuggestResult,
24
+ SynonymRule,
25
+ VectorMaintenanceResult,
26
+ WriteOptions,
27
+ } from '@delali/narsil'
28
+
29
+ /**
30
+ * The engine settings, which are the settings of `createNarsil` in
31
+ * `@delali/narsil`, function settings such as `embeddingAdapters`, `plugins`,
32
+ * and `idGenerator` included.
33
+ *
34
+ * `durability` defaults to a write-ahead log in a `narsil` directory inside
35
+ * the app's private storage, and `null` keeps the index in memory alone. A
36
+ * relative `directory` resolves against that private storage.
37
+ *
38
+ * @public
39
+ */
40
+ export type PhoneNarsilConfig = Omit<NarsilConfig, 'durability'> & { durability?: DurabilityConfig | null }
41
+
42
+ /**
43
+ * Receives one engine event on the app's JS thread. An `Error` inside the
44
+ * payload arrives as an `Error` carrying the original `name`, `message`,
45
+ * `code`, and `details`.
46
+ *
47
+ * @public
48
+ */
49
+ export type PhoneEventHandler<K extends keyof NarsilEventMap> = (payload: NarsilEventMap[K]) => void
50
+
51
+ /**
52
+ * A Narsil engine on a phone. Every method resolves with the engine's answer,
53
+ * so a method that the engine answers synchronously, such as `listIndexes`,
54
+ * also resolves through a promise here. Each method otherwise behaves as the
55
+ * method of the same name on the `Narsil` interface of `@delali/narsil`.
56
+ *
57
+ * A failure rejects with an `Error` whose `name` is `NarsilError` for an
58
+ * engine failure, and whose `code` and `details` match the engine's error.
59
+ * {@link isNarsilError} recognises it.
60
+ *
61
+ * @public
62
+ */
63
+ export interface PhoneNarsil {
64
+ /** Creates an index from a schema and its settings. */
65
+ createIndex(name: string, config: IndexConfig): Promise<void>
66
+ /** Removes an index and the files that it wrote. */
67
+ dropIndex(name: string): Promise<void>
68
+ /** Lists every index that this engine holds. */
69
+ listIndexes(): Promise<IndexInfo[]>
70
+ /** Returns one index's document count, memory estimate, and schema. */
71
+ getStats(indexName: string): Promise<IndexStats>
72
+ /** Returns the figures of each open partition of one index. */
73
+ getPartitionStats(indexName: string): Promise<PartitionStatsResult[]>
74
+ /** Fetches one stored document by id, or `undefined` when the index lacks it. */
75
+ get(indexName: string, docId: string): Promise<AnyDocument | undefined>
76
+ /** Fetches many stored documents by id, keyed by id. */
77
+ getMultiple(indexName: string, docIds: string[]): Promise<Map<string, AnyDocument>>
78
+ /** Reports whether an index holds a document. */
79
+ has(indexName: string, docId: string): Promise<boolean>
80
+ /** Counts the documents of one index. */
81
+ countDocuments(indexName: string): Promise<number>
82
+ /** Reads one page of stored documents in document-id order. */
83
+ listDocuments<T = AnyDocument>(indexName: string, params?: ListParams): Promise<ListResult<T>>
84
+ /** Searches one index by keyword, by vector, or by both. */
85
+ query<T = AnyDocument>(indexName: string, params: QueryParams): Promise<QueryResult<T>>
86
+ /** Counts what a query would match without building a single hit. */
87
+ preflight(indexName: string, params: QueryParams): Promise<PreflightResult>
88
+ /** Returns the indexed terms that complete a prefix. */
89
+ suggest(indexName: string, params: SuggestParams): Promise<SuggestResult>
90
+ /** Rebuilds an index's terms from the documents that it already stores. */
91
+ rebuildAnalysis(indexName: string): Promise<void>
92
+ /** Returns a snapshot of one index as the bytes of one `.nrsl` file. */
93
+ snapshot(indexName: string): Promise<Uint8Array>
94
+ /**
95
+ * Writes a snapshot of one index into a file and resolves with its size in
96
+ * bytes. A relative path resolves against the app's private storage, and
97
+ * the file replaces any file at that path only once every byte is on disk.
98
+ */
99
+ snapshotToFile(indexName: string, path: string): Promise<number>
100
+ /** Replaces one index with the snapshot in these bytes. */
101
+ restore(indexName: string, data: Uint8Array): Promise<void>
102
+ /** Replaces one index with the snapshot in a file, which the engine reads in chunks. */
103
+ restoreFromFile(indexName: string, path: string): Promise<void>
104
+ /** Returns every document of one index as the bytes of a document export. */
105
+ exportDocuments(indexName: string): Promise<Uint8Array>
106
+ /** Writes a document export of one index into a file and resolves with its size in bytes. */
107
+ exportDocumentsToFile(indexName: string, path: string): Promise<number>
108
+ /** Inserts the documents of a document export held in these bytes. */
109
+ importDocumentExport(indexName: string, data: Uint8Array): Promise<DocumentImportResult>
110
+ /** Inserts the documents of a document export held in a file. */
111
+ importDocumentExportFromFile(indexName: string, path: string): Promise<DocumentImportResult>
112
+ /** Writes an index's current state to the durability directory, so a restart replays less of the log. */
113
+ checkpoint(indexName: string): Promise<void>
114
+ /** Removes every document from an index and keeps the index, its schema, and its settings. */
115
+ clear(indexName: string): Promise<void>
116
+ /** Spreads an index's documents across a different number of partitions. */
117
+ rebalance(indexName: string, targetPartitionCount: number): Promise<void>
118
+ /** Changes how an index grows into new partitions. */
119
+ updatePartitionConfig(indexName: string, config: Partial<PartitionConfig>): Promise<void>
120
+ /** Reports what this engine and the app's process use. */
121
+ getMemoryStats(): Promise<MemoryStats>
122
+ /** Rebuilds a vector field's graph without the tombstones that removed documents left behind. */
123
+ compactVectors(indexName: string, fieldName?: string): Promise<void>
124
+ /** Rebuilds a vector field's graph from scratch, which recovers recall. */
125
+ optimizeVectors(indexName: string, fieldName?: string): Promise<void>
126
+ /** Reports the state of each vector field in an index, and what its maintenance would cost. */
127
+ vectorMaintenanceStatus(indexName: string): Promise<VectorMaintenanceResult[]>
128
+ /** Opens a closed index. */
129
+ open(indexName: string): Promise<void>
130
+ /** Closes an index. */
131
+ close(indexName: string): Promise<void>
132
+ /** Inserts one document and resolves with the id that it is stored under. */
133
+ insert(indexName: string, document: AnyDocument, docId?: string, options?: InsertOptions): Promise<string>
134
+ /** Inserts many documents in one request. */
135
+ insertBatch(indexName: string, documents: AnyDocument[], options?: InsertOptions): Promise<BatchResult>
136
+ /** Removes one document. */
137
+ remove(indexName: string, docId: string, options?: WriteOptions): Promise<void>
138
+ /** Removes many documents in one request. */
139
+ removeBatch(indexName: string, docIds: string[], options?: WriteOptions): Promise<BatchResult>
140
+ /** Replaces one stored document. */
141
+ update(indexName: string, docId: string, document: AnyDocument, options?: WriteOptions): Promise<void>
142
+ /** Replaces many stored documents in one request. */
143
+ updateBatch(
144
+ indexName: string,
145
+ updates: Array<{ docId: string; document: AnyDocument }>,
146
+ options?: WriteOptions,
147
+ ): Promise<BatchResult>
148
+ /** Resolves once every worker copy of an index has applied every earlier write. */
149
+ waitForWrites(indexName: string): Promise<void>
150
+ /** Replaces the synonym rules of one index. */
151
+ updateSynonyms(indexName: string, synonyms: SynonymRule[]): Promise<void>
152
+ /** Returns the synonym rules of one index. */
153
+ getSynonyms(indexName: string): Promise<SynonymRule[]>
154
+ /** Changes how one index analyses text, and re-analyses its documents. */
155
+ changeAnalysis(indexName: string, change: AnalysisChange): Promise<void>
156
+ /** Changes the BM25 parameters of one index. */
157
+ updateBM25(indexName: string, bm25: BM25Params): Promise<void>
158
+ /** Calls a handler on the app's JS thread for each engine event of one kind. */
159
+ on<K extends keyof NarsilEventMap>(event: K, handler: PhoneEventHandler<K>): void
160
+ /** Stops calling a handler that {@link PhoneNarsil.on} registered. */
161
+ off<K extends keyof NarsilEventMap>(event: K, handler: PhoneEventHandler<K>): void
162
+ /** Shuts the engine down, after which every other method rejects with `INSTANCE_SHUT_DOWN`. */
163
+ shutdown(): Promise<void>
164
+ }
@@ -0,0 +1,48 @@
1
+ import { APP_TIMER_WAIT_BEFORE_RUN_LOOP_HOP_MS } from '../constants'
2
+
3
+ type Hop = (run: () => void) => void
4
+
5
+ function hopThroughRuntimeScheduler(): Hop | null {
6
+ const runtimeScheduler: unknown = Reflect.get(globalThis, 'nativeRuntimeScheduler')
7
+ if (typeof runtimeScheduler !== 'object' || runtimeScheduler === null) return null
8
+ const schedule: unknown = Reflect.get(runtimeScheduler, 'unstable_scheduleCallback')
9
+ const priority: unknown = Reflect.get(runtimeScheduler, 'unstable_NormalPriority')
10
+ if (typeof schedule !== 'function' || typeof priority !== 'number') return null
11
+ let appTimerArmedAt: number | null = null
12
+ let runsAfterAppTimer: Array<() => void> = []
13
+
14
+ function appTimerFired(): void {
15
+ appTimerArmedAt = null
16
+ const runs = runsAfterAppTimer
17
+ runsAfterAppTimer = []
18
+ for (const run of runs) run()
19
+ }
20
+
21
+ return run => {
22
+ const now = performance.now()
23
+ if (appTimerArmedAt === null) {
24
+ appTimerArmedAt = now
25
+ setTimeout(appTimerFired, 0)
26
+ } else if (now - appTimerArmedAt >= APP_TIMER_WAIT_BEFORE_RUN_LOOP_HOP_MS) {
27
+ runsAfterAppTimer.push(run)
28
+ return
29
+ }
30
+ Reflect.apply(schedule, runtimeScheduler, [
31
+ priority,
32
+ () => {
33
+ run()
34
+ },
35
+ ])
36
+ }
37
+ }
38
+
39
+ function hopThroughTimer(run: () => void): void {
40
+ setTimeout(run, 0)
41
+ }
42
+
43
+ let hop: Hop | null = null
44
+
45
+ export function afterAppTurn(run: () => void): void {
46
+ hop ??= hopThroughRuntimeScheduler() ?? hopThroughTimer
47
+ hop(run)
48
+ }
@@ -0,0 +1,165 @@
1
+ import type { NarsilEventMap } from '@delali/narsil'
2
+ import { isWorkletFunction, type WorkletRuntime } from 'react-native-worklets'
3
+ import { rebuiltError, rebuiltValue } from '../errors'
4
+ import { type EngineMethod, FORWARDED_METHODS, STREAMED_METHODS } from '../methods'
5
+ import { loadNativeObjects } from '../native-objects'
6
+ import type { CallOutcome } from '../transferred'
7
+ import type { PhoneNarsil } from '../types'
8
+ import { jsonOfPart } from './json-part'
9
+ import {
10
+ boxNativeObjects,
11
+ sendCall,
12
+ sendCallPart,
13
+ sendCallPartText,
14
+ sendDropCallParts,
15
+ sendListen,
16
+ sendOpen,
17
+ startEngineRuntime,
18
+ } from './runtime'
19
+ import { enqueueCall } from './transfer-queue'
20
+ import type { EngineSetup, WorkletNarsilConfig } from './types'
21
+
22
+ type EventName = keyof NarsilEventMap
23
+ type AnyHandler = (payload: never) => void
24
+
25
+ interface PendingCall {
26
+ resolve(value: unknown): void
27
+ reject(error: unknown): void
28
+ }
29
+
30
+ const pendingCalls = new Map<number, PendingCall>()
31
+ const handlersByInstance = new Map<number, Map<EventName, Set<AnyHandler>>>()
32
+ let nextCallId = 1
33
+
34
+ function settleCall(callId: number, outcome: CallOutcome): void {
35
+ const pending = pendingCalls.get(callId)
36
+ if (pending === undefined) return
37
+ pendingCalls.delete(callId)
38
+ if (outcome.ok) pending.resolve(outcome.value)
39
+ else pending.reject(rebuiltError(outcome.error))
40
+ }
41
+
42
+ function deliverEvent(instanceId: number, event: string, payload: unknown): void {
43
+ const handlers = handlersByInstance.get(instanceId)?.get(event as EventName)
44
+ if (handlers === undefined) return
45
+ const rebuilt = rebuiltValue(payload) as never
46
+ for (const handler of [...handlers]) {
47
+ try {
48
+ handler(rebuilt)
49
+ } catch (error) {
50
+ setTimeout(() => {
51
+ throw error
52
+ }, 0)
53
+ }
54
+ }
55
+ }
56
+
57
+ function request(send: (callId: number) => void): Promise<unknown> {
58
+ return new Promise((resolve, reject) => {
59
+ const callId = nextCallId++
60
+ pendingCalls.set(callId, { resolve, reject })
61
+ try {
62
+ send(callId)
63
+ } catch (error) {
64
+ pendingCalls.delete(callId)
65
+ reject(error)
66
+ }
67
+ })
68
+ }
69
+
70
+ function phoneNarsil(runtime: WorkletRuntime, instanceId: number): PhoneNarsil {
71
+ const handlers = new Map<EventName, Set<AnyHandler>>()
72
+ handlersByInstance.set(instanceId, handlers)
73
+
74
+ function call(method: EngineMethod, args: unknown[]): Promise<unknown> {
75
+ return new Promise((resolve, reject) => {
76
+ const callId = nextCallId++
77
+ pendingCalls.set(callId, { resolve, reject })
78
+ enqueueCall(args, {
79
+ sendPart: (argIndex, items) => {
80
+ const text = jsonOfPart(items)
81
+ if (text === null) sendCallPart(runtime, callId, argIndex, items)
82
+ else sendCallPartText(runtime, callId, argIndex, text)
83
+ },
84
+ sendCall: (values, partedArgs) => sendCall(runtime, callId, settleCall, instanceId, method, values, partedArgs),
85
+ dropParts: () => sendDropCallParts(runtime, callId),
86
+ fail: error => {
87
+ pendingCalls.delete(callId)
88
+ reject(error)
89
+ },
90
+ })
91
+ })
92
+ }
93
+
94
+ const client: Record<string, unknown> = {}
95
+ for (const method of [...FORWARDED_METHODS, ...STREAMED_METHODS]) {
96
+ client[method] = (...args: unknown[]) => call(method, args)
97
+ }
98
+
99
+ client.on = (event: EventName, handler: AnyHandler) => {
100
+ let registered = handlers.get(event)
101
+ if (registered === undefined) {
102
+ registered = new Set()
103
+ handlers.set(event, registered)
104
+ }
105
+ const first = registered.size === 0
106
+ registered.add(handler)
107
+ if (first) sendListen(runtime, instanceId, event, deliverEvent, true)
108
+ }
109
+
110
+ client.off = (event: EventName, handler: AnyHandler) => {
111
+ const registered = handlers.get(event)
112
+ if (registered === undefined || !registered.delete(handler)) return
113
+ if (registered.size > 0) return
114
+ handlers.delete(event)
115
+ sendListen(runtime, instanceId, event, deliverEvent, false)
116
+ }
117
+
118
+ client.shutdown = async () => {
119
+ try {
120
+ await call('shutdown', [])
121
+ } finally {
122
+ handlers.clear()
123
+ handlersByInstance.delete(instanceId)
124
+ }
125
+ }
126
+
127
+ return client as unknown as PhoneNarsil
128
+ }
129
+
130
+ /**
131
+ * Starts a Narsil engine on its own Worklets runtime and resolves with a
132
+ * client whose methods send each request to that runtime, so the engine's
133
+ * JavaScript work leaves the app's JS thread free. Every engine that this
134
+ * function starts shares one runtime, and each engine keeps its own indexes.
135
+ * The app needs `react-native-worklets` with Bundle Mode turned on. The
136
+ * client copies each batch that an insert sends across to the engine's
137
+ * runtime before the engine indexes it, as JSON text when the batch holds
138
+ * plain data. On a physical iPhone and a physical Android phone, inserts took
139
+ * 6% to 57% longer than on the app's own JS runtime, depending on the number
140
+ * of documents and on durability, and an import from a file, which sends only
141
+ * the file's path across, took up to 11% longer. In return, the longest pause
142
+ * that a 10 ms timer on the app's JS thread saw during an insert of 50,000
143
+ * documents fell from 111 ms to 30 ms on the Android phone and from 144 ms to
144
+ * 74 ms on the iPhone.
145
+ *
146
+ * @param config - The engine settings that cross to the engine's runtime as
147
+ * data. `durability` defaults to a write-ahead log in the `narsil` directory
148
+ * of the app's private storage.
149
+ * @param setup - A worklet that returns the settings that hold functions,
150
+ * such as embedding adapters, plugins, or an id generator.
151
+ * @returns The client, once the engine has started on its runtime.
152
+ * @throws A `NarsilError` with `NATIVE_DEPENDENCY_MISSING` when the app's
153
+ * build lacks the native module, as in Expo Go.
154
+ *
155
+ * @public
156
+ */
157
+ export async function createNarsil(config: WorkletNarsilConfig = {}, setup?: EngineSetup): Promise<PhoneNarsil> {
158
+ if (setup !== undefined && !isWorkletFunction(setup)) {
159
+ throw new TypeError("The setup function must be a worklet, so start its body with the 'worklet' directive")
160
+ }
161
+ const natives = await loadNativeObjects()
162
+ const runtime = startEngineRuntime(await boxNativeObjects(natives))
163
+ const instanceId = await request(callId => sendOpen(runtime, callId, settleCall, config, setup))
164
+ return phoneNarsil(runtime, instanceId as number)
165
+ }
@@ -0,0 +1,4 @@
1
+ export { isNarsilError, type PhoneNarsilError } from '../errors'
2
+ export type { PhoneEventHandler, PhoneNarsil } from '../types'
3
+ export { createNarsil } from './client'
4
+ export type { EngineSetup, WorkletNarsilConfig } from './types'
@@ -0,0 +1,231 @@
1
+ import {
2
+ createNarsil,
3
+ ErrorCodes,
4
+ type Narsil,
5
+ type NarsilConfig,
6
+ NarsilError,
7
+ type NarsilEventMap,
8
+ } from '@delali/narsil'
9
+ import { scheduleOnRN } from 'react-native-worklets'
10
+ import { installedHost } from '../engine/install'
11
+ import { callStreamed, withPhoneDurability } from '../engine/streamed'
12
+ import { transferable, transferredError } from '../engine/transfer'
13
+ import { type EngineMethod, FORWARDED_METHODS, STREAMED_METHODS, type StreamedMethod } from '../methods'
14
+ import type { CallOutcome } from '../transferred'
15
+ import type { EngineSetup, WorkletNarsilConfig } from './types'
16
+
17
+ type Settle = (callId: number, outcome: CallOutcome) => void
18
+ type DeliverEvent = (instanceId: number, event: EventName, payload: unknown) => void
19
+ type EventName = keyof NarsilEventMap
20
+
21
+ interface OpenInstance {
22
+ narsil: Narsil
23
+ listeners: Map<EventName, (payload: unknown) => void>
24
+ }
25
+
26
+ interface EngineStart {
27
+ failed: boolean
28
+ error: unknown
29
+ }
30
+
31
+ const FORWARDED = new Set<string>(FORWARDED_METHODS)
32
+ const STREAMED = new Set<string>(STREAMED_METHODS)
33
+ const UNDELIVERABLE_OUTCOME: CallOutcome = {
34
+ ok: false,
35
+ error: {
36
+ transferredError: true,
37
+ name: 'Error',
38
+ message: "The engine's runtime could not send this call's outcome back to the app",
39
+ code: undefined,
40
+ details: undefined,
41
+ },
42
+ }
43
+ const instances = new Map<number, OpenInstance>()
44
+ const stagedParts = new Map<number, Map<number, unknown[]>>()
45
+ const failedCalls = new Map<number, unknown>()
46
+ const engineStart: EngineStart = { failed: false, error: undefined }
47
+ let nextInstanceId = 1
48
+
49
+ function instanceOf(instanceId: number): OpenInstance {
50
+ const instance = instances.get(instanceId)
51
+ if (instance === undefined) {
52
+ throw new NarsilError(ErrorCodes.INSTANCE_SHUT_DOWN, 'This Narsil instance has been shut down')
53
+ }
54
+ return instance
55
+ }
56
+
57
+ function failureOf(error: unknown): CallOutcome {
58
+ return { ok: false, error: transferredError(error) }
59
+ }
60
+
61
+ function deliver(settle: Settle, callId: number, outcome: () => CallOutcome): void {
62
+ try {
63
+ scheduleOnRN(settle, callId, outcome())
64
+ return
65
+ } catch (error) {
66
+ try {
67
+ scheduleOnRN(settle, callId, failureOf(error))
68
+ return
69
+ } catch {}
70
+ }
71
+ try {
72
+ scheduleOnRN(settle, callId, UNDELIVERABLE_OUTCOME)
73
+ } catch {}
74
+ }
75
+
76
+ function report(settle: Settle, callId: number, work: () => Promise<unknown>): void {
77
+ let pending: Promise<unknown>
78
+ try {
79
+ pending = work()
80
+ } catch (error) {
81
+ deliver(settle, callId, () => failureOf(error))
82
+ return
83
+ }
84
+ pending.then(
85
+ value => deliver(settle, callId, () => ({ ok: true, value: transferable(value) })),
86
+ error => deliver(settle, callId, () => failureOf(error)),
87
+ )
88
+ }
89
+
90
+ export function recordEngineStartFailure(error: unknown): void {
91
+ engineStart.failed = true
92
+ engineStart.error = error
93
+ }
94
+
95
+ async function openInstance(config: WorkletNarsilConfig, setup: EngineSetup | undefined): Promise<number> {
96
+ if (engineStart.failed) throw engineStart.error
97
+ const fromSetup = setup === undefined ? {} : await setup()
98
+ const merged: NarsilConfig = { ...withPhoneDurability(config, fromSetup.durability !== undefined), ...fromSetup }
99
+ const narsil = await createNarsil(merged)
100
+ const instanceId = nextInstanceId++
101
+ instances.set(instanceId, { narsil, listeners: new Map() })
102
+ return instanceId
103
+ }
104
+
105
+ async function shutdown(instanceId: number): Promise<void> {
106
+ const instance = instances.get(instanceId)
107
+ if (instance === undefined) return
108
+ instances.delete(instanceId)
109
+ for (const [event, listener] of instance.listeners) instance.narsil.off(event, listener)
110
+ instance.listeners.clear()
111
+ await instance.narsil.shutdown()
112
+ }
113
+
114
+ function dispatch(instanceId: number, method: EngineMethod, args: readonly unknown[]): Promise<unknown> {
115
+ if (method === 'shutdown') return shutdown(instanceId)
116
+ const { narsil } = instanceOf(instanceId)
117
+ if (FORWARDED.has(method)) {
118
+ const target = narsil[method as keyof Narsil] as (...values: unknown[]) => unknown
119
+ return Promise.resolve(target.apply(narsil, [...args]))
120
+ }
121
+ if (!STREAMED.has(method)) throw new TypeError(`The Narsil engine has no method called ${String(method)}`)
122
+ return callStreamed(narsil, installedHost(), method as StreamedMethod, args)
123
+ }
124
+
125
+ export function openOnEngine(
126
+ callId: number,
127
+ settle: Settle,
128
+ config: WorkletNarsilConfig,
129
+ setup: EngineSetup | undefined,
130
+ ): void {
131
+ report(settle, callId, () => openInstance(config, setup))
132
+ }
133
+
134
+ function failCall(callId: number, error: unknown): void {
135
+ stagedParts.delete(callId)
136
+ failedCalls.set(callId, error)
137
+ }
138
+
139
+ function stageItems(callId: number, argIndex: number, items: unknown): void {
140
+ if (!Array.isArray(items)) throw new TypeError('A part of a call reached the engine without its items')
141
+ let parts = stagedParts.get(callId)
142
+ if (parts === undefined) {
143
+ parts = new Map()
144
+ stagedParts.set(callId, parts)
145
+ }
146
+ let staged = parts.get(argIndex)
147
+ if (staged === undefined) {
148
+ staged = []
149
+ parts.set(argIndex, staged)
150
+ }
151
+ for (const item of items) staged.push(item)
152
+ }
153
+
154
+ export function stageCallPart(callId: number, argIndex: number, items: readonly unknown[]): void {
155
+ if (failedCalls.has(callId)) return
156
+ try {
157
+ stageItems(callId, argIndex, items)
158
+ } catch (error) {
159
+ failCall(callId, error)
160
+ }
161
+ }
162
+
163
+ export function stageCallPartText(callId: number, argIndex: number, text: string): void {
164
+ if (failedCalls.has(callId)) return
165
+ try {
166
+ stageItems(callId, argIndex, JSON.parse(text))
167
+ } catch (error) {
168
+ failCall(callId, error)
169
+ }
170
+ }
171
+
172
+ export function dropCallParts(callId: number): void {
173
+ stagedParts.delete(callId)
174
+ failedCalls.delete(callId)
175
+ }
176
+
177
+ function withStagedParts(callId: number, args: readonly unknown[], partedArgs: readonly number[]): unknown[] {
178
+ const parts = stagedParts.get(callId)
179
+ const failure = failedCalls.get(callId)
180
+ stagedParts.delete(callId)
181
+ if (failedCalls.delete(callId)) throw failure
182
+ const values = [...args]
183
+ for (const argIndex of partedArgs) values[argIndex] = parts?.get(argIndex) ?? []
184
+ return values
185
+ }
186
+
187
+ export function callOnEngine(
188
+ callId: number,
189
+ settle: Settle,
190
+ instanceId: number,
191
+ method: EngineMethod,
192
+ args: readonly unknown[],
193
+ partedArgs: readonly number[],
194
+ ): void {
195
+ report(settle, callId, () =>
196
+ dispatch(instanceId, method, partedArgs.length === 0 ? args : withStagedParts(callId, args, partedArgs)),
197
+ )
198
+ }
199
+
200
+ function forwardEvent(forward: DeliverEvent, instanceId: number, event: EventName, payload: unknown): void {
201
+ try {
202
+ scheduleOnRN(forward, instanceId, event, transferable(payload))
203
+ } catch {}
204
+ }
205
+
206
+ function changeListener(instanceId: number, event: EventName, forward: DeliverEvent, listening: boolean): void {
207
+ const instance = instances.get(instanceId)
208
+ if (instance === undefined) return
209
+ const existing = instance.listeners.get(event)
210
+ if (!listening) {
211
+ if (existing === undefined) return
212
+ instance.listeners.delete(event)
213
+ instance.narsil.off(event, existing)
214
+ return
215
+ }
216
+ if (existing !== undefined) return
217
+ const listener = (payload: unknown) => forwardEvent(forward, instanceId, event, payload)
218
+ instance.listeners.set(event, listener)
219
+ try {
220
+ instance.narsil.on(event, listener)
221
+ } catch (error) {
222
+ instance.listeners.delete(event)
223
+ throw error
224
+ }
225
+ }
226
+
227
+ export function listenOnEngine(instanceId: number, event: EventName, forward: DeliverEvent, listening: boolean): void {
228
+ try {
229
+ changeListener(instanceId, event, forward, listening)
230
+ } catch {}
231
+ }
@@ -0,0 +1,38 @@
1
+ import { WORKLET_JSON_PART_MAX_DEPTH } from '../constants'
2
+
3
+ function encodesExactly(value: unknown, depth: number): boolean {
4
+ switch (typeof value) {
5
+ case 'string':
6
+ case 'boolean':
7
+ return true
8
+ case 'number':
9
+ return Number.isFinite(value) && !Object.is(value, -0)
10
+ case 'object':
11
+ break
12
+ default:
13
+ return false
14
+ }
15
+ if (value === null) return true
16
+ if (depth >= WORKLET_JSON_PART_MAX_DEPTH) return false
17
+ const prototype = Object.getPrototypeOf(value)
18
+ if (prototype === Array.prototype) {
19
+ const items = value as readonly unknown[]
20
+ for (let index = 0; index < items.length; index++) {
21
+ if (!(index in items) || !encodesExactly(items[index], depth + 1)) return false
22
+ }
23
+ return true
24
+ }
25
+ if (prototype !== Object.prototype && prototype !== null) return false
26
+ const record = value as Record<string, unknown>
27
+ for (const key of Object.keys(record)) {
28
+ if (!encodesExactly(record[key], depth + 1)) return false
29
+ }
30
+ return true
31
+ }
32
+
33
+ export function jsonOfPart(items: readonly unknown[]): string | null {
34
+ for (const item of items) {
35
+ if (!encodesExactly(item, 1)) return null
36
+ }
37
+ return JSON.stringify(items)
38
+ }