@delali/narsil-react-native 0.0.0-stage → 0.3.1-next.151
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/LICENSE +201 -0
- package/NarsilReactNative.podspec +57 -0
- package/README.md +46 -2
- package/android/CMakeLists.txt +48 -0
- package/android/build.gradle +151 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/AndroidEnvironment.cpp +44 -0
- package/android/src/main/cpp/AndroidEnvironment.hpp +11 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/narsil/NarsilAndroidEnvironment.kt +29 -0
- package/android/src/main/java/com/margelo/nitro/narsil/NarsilReactNativePackage.kt +22 -0
- package/app.plugin.js +25 -0
- package/cpp/AttachedField.cpp +234 -0
- package/cpp/AttachedField.hpp +61 -0
- package/cpp/Compression.cpp +113 -0
- package/cpp/Compression.hpp +16 -0
- package/cpp/GraphHeldAlone.cpp +29 -0
- package/cpp/GraphHeldAlone.hpp +25 -0
- package/cpp/HostMemory.cpp +123 -0
- package/cpp/HostMemory.hpp +53 -0
- package/cpp/HybridNarsilCoreImpl.cpp +390 -0
- package/cpp/HybridNarsilCoreImpl.hpp +68 -0
- package/cpp/HybridNarsilFilesImpl.cpp +245 -0
- package/cpp/HybridNarsilFilesImpl.hpp +34 -0
- package/cpp/HybridNarsilHostImpl.cpp +126 -0
- package/cpp/HybridNarsilHostImpl.hpp +42 -0
- package/cpp/NativeAtomics.cpp +110 -0
- package/cpp/NativeAtomics.hpp +11 -0
- package/cpp/ProcessInfo.cpp +83 -0
- package/cpp/ProcessInfo.hpp +11 -0
- package/cpp/ThreadPriority.cpp +77 -0
- package/cpp/ThreadPriority.hpp +8 -0
- package/cpp/WorkletRuntimeDispatcher.cpp +61 -0
- package/cpp/WorkletRuntimeDispatcher.hpp +9 -0
- package/cpp/core/include/narsil_core.h +208 -0
- package/cpp/core/src/connect.c +213 -0
- package/cpp/core/src/distance.c +103 -0
- package/cpp/core/src/files.c +99 -0
- package/cpp/core/src/heap.c +84 -0
- package/cpp/core/src/internal.h +284 -0
- package/cpp/core/src/kernels_code.c +209 -0
- package/cpp/core/src/kernels_float.c +197 -0
- package/cpp/core/src/kernels_pair.c +133 -0
- package/cpp/core/src/last_links.c +145 -0
- package/cpp/core/src/links.c +195 -0
- package/cpp/core/src/locks.c +168 -0
- package/cpp/core/src/nearest_links.c +147 -0
- package/cpp/core/src/place.c +215 -0
- package/cpp/core/src/quantize.c +226 -0
- package/cpp/core/src/records.c +97 -0
- package/cpp/core/src/remove.c +205 -0
- package/cpp/core/src/vectors.c +139 -0
- package/cpp/core/src/walk.c +231 -0
- package/cpp/core/src/workspace.c +167 -0
- package/ios/PrivacyInfo.xcprivacy +23 -0
- package/lib/client.d.ts +19 -0
- package/lib/client.js +41 -0
- package/lib/constants.d.ts +9 -0
- package/lib/constants.js +9 -0
- package/lib/engine/byte-files.d.ts +5 -0
- package/lib/engine/byte-files.js +97 -0
- package/lib/engine/core.d.ts +3 -0
- package/lib/engine/core.js +113 -0
- package/lib/engine/files.d.ts +3 -0
- package/lib/engine/files.js +188 -0
- package/lib/engine/host.d.ts +13 -0
- package/lib/engine/host.js +102 -0
- package/lib/engine/install.d.ts +4 -0
- package/lib/engine/install.js +29 -0
- package/lib/engine/native-error.d.ts +1 -0
- package/lib/engine/native-error.js +12 -0
- package/lib/engine/stand-ins/atomics.d.ts +5 -0
- package/lib/engine/stand-ins/atomics.js +236 -0
- package/lib/engine/stand-ins/crypto.d.ts +2 -0
- package/lib/engine/stand-ins/crypto.js +65 -0
- package/lib/engine/stand-ins/process.d.ts +2 -0
- package/lib/engine/stand-ins/process.js +22 -0
- package/lib/engine/stand-ins/streams.d.ts +1 -0
- package/lib/engine/stand-ins/streams.js +31 -0
- package/lib/engine/stand-ins/structured-clone.d.ts +1 -0
- package/lib/engine/stand-ins/structured-clone.js +121 -0
- package/lib/engine/streamed.d.ts +6 -0
- package/lib/engine/streamed.js +29 -0
- package/lib/engine/transfer.d.ts +3 -0
- package/lib/engine/transfer.js +77 -0
- package/lib/errors.d.ts +25 -0
- package/lib/errors.js +59 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +2 -0
- package/lib/methods.d.ts +5 -0
- package/lib/methods.js +47 -0
- package/lib/native-objects.d.ts +2 -0
- package/lib/native-objects.js +24 -0
- package/lib/specs/core.nitro.d.ts +79 -0
- package/lib/specs/core.nitro.js +1 -0
- package/lib/specs/files.nitro.d.ts +36 -0
- package/lib/specs/files.nitro.js +1 -0
- package/lib/specs/host.nitro.d.ts +24 -0
- package/lib/specs/host.nitro.js +1 -0
- package/lib/transferred.d.ts +15 -0
- package/lib/transferred.js +6 -0
- package/lib/types.d.ts +136 -0
- package/lib/types.js +1 -0
- package/lib/worklets/app-turns.d.ts +1 -0
- package/lib/worklets/app-turns.js +44 -0
- package/lib/worklets/client.d.ts +30 -0
- package/lib/worklets/client.js +144 -0
- package/lib/worklets/index.d.ts +4 -0
- package/lib/worklets/index.js +2 -0
- package/lib/worklets/instances.d.ts +15 -0
- package/lib/worklets/instances.js +194 -0
- package/lib/worklets/json-part.d.ts +1 -0
- package/lib/worklets/json-part.js +42 -0
- package/lib/worklets/runtime.d.ts +25 -0
- package/lib/worklets/runtime.js +80 -0
- package/lib/worklets/transfer-queue.d.ts +7 -0
- package/lib/worklets/transfer-queue.js +79 -0
- package/lib/worklets/types.d.ts +26 -0
- package/lib/worklets/types.js +1 -0
- package/nitro.json +32 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NarsilReactNative+autolinking.cmake +85 -0
- package/nitrogen/generated/android/NarsilReactNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NarsilReactNativeOnLoad.cpp +69 -0
- package/nitrogen/generated/android/NarsilReactNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/narsil/NarsilReactNativeOnLoad.kt +35 -0
- package/nitrogen/generated/ios/NarsilReactNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NarsilReactNative-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/NarsilReactNative-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/NarsilReactNative-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/NarsilReactNativeAutolinking.mm +55 -0
- package/nitrogen/generated/ios/NarsilReactNativeAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/DirectoryEntry.hpp +91 -0
- package/nitrogen/generated/shared/c++/FileStatus.hpp +103 -0
- package/nitrogen/generated/shared/c++/GraphMemory.hpp +120 -0
- package/nitrogen/generated/shared/c++/HybridNarsilAttachedGraphSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridNarsilAttachedGraphSpec.hpp +62 -0
- package/nitrogen/generated/shared/c++/HybridNarsilAttachedStoreSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridNarsilAttachedStoreSpec.hpp +62 -0
- package/nitrogen/generated/shared/c++/HybridNarsilCoreSpec.cpp +35 -0
- package/nitrogen/generated/shared/c++/HybridNarsilCoreSpec.hpp +100 -0
- package/nitrogen/generated/shared/c++/HybridNarsilFilesSpec.cpp +37 -0
- package/nitrogen/generated/shared/c++/HybridNarsilFilesSpec.hpp +86 -0
- package/nitrogen/generated/shared/c++/HybridNarsilHostSpec.cpp +34 -0
- package/nitrogen/generated/shared/c++/HybridNarsilHostSpec.hpp +78 -0
- package/nitrogen/generated/shared/c++/MemoryView.hpp +91 -0
- package/nitrogen/generated/shared/c++/NativeConnectOutcome.hpp +87 -0
- package/nitrogen/generated/shared/c++/NativeScores.hpp +87 -0
- package/nitrogen/generated/shared/c++/NativeSearchHits.hpp +91 -0
- package/nitrogen/generated/shared/c++/ProcessMemory.hpp +87 -0
- package/nitrogen/generated/shared/c++/StoreMemory.hpp +142 -0
- package/package.json +87 -3
- package/react-native.config.js +8 -0
- package/src/client.ts +44 -0
- package/src/constants.ts +9 -0
- package/src/engine/byte-files.ts +99 -0
- package/src/engine/core.ts +162 -0
- package/src/engine/files.ts +186 -0
- package/src/engine/host.ts +111 -0
- package/src/engine/install.ts +31 -0
- package/src/engine/native-error.ts +10 -0
- package/src/engine/stand-ins/atomics.ts +288 -0
- package/src/engine/stand-ins/crypto.ts +81 -0
- package/src/engine/stand-ins/process.ts +30 -0
- package/src/engine/stand-ins/streams.ts +45 -0
- package/src/engine/stand-ins/structured-clone.ts +118 -0
- package/src/engine/streamed.ts +40 -0
- package/src/engine/transfer.ts +72 -0
- package/src/errors.ts +73 -0
- package/src/index.ts +3 -0
- package/src/methods.ts +52 -0
- package/src/native-objects.ts +30 -0
- package/src/specs/core.nitro.ts +140 -0
- package/src/specs/files.nitro.ts +42 -0
- package/src/specs/host.nitro.ts +30 -0
- package/src/transferred.ts +18 -0
- package/src/types.ts +164 -0
- package/src/worklets/app-turns.ts +48 -0
- package/src/worklets/client.ts +165 -0
- package/src/worklets/index.ts +4 -0
- package/src/worklets/instances.ts +231 -0
- package/src/worklets/json-part.ts +38 -0
- package/src/worklets/runtime.ts +190 -0
- package/src/worklets/transfer-queue.ts +98 -0
- 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,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
|
+
}
|