@expo-kit/cloudkit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -2
- package/app.plugin.js +1 -0
- package/build/codec.d.ts +6 -0
- package/build/codec.d.ts.map +1 -0
- package/build/codec.js +34 -0
- package/build/codec.js.map +1 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +9 -0
- package/build/index.js.map +1 -0
- package/build/native.d.ts +86 -0
- package/build/native.d.ts.map +1 -0
- package/build/native.js +7 -0
- package/build/native.js.map +1 -0
- package/build/schema.d.ts +98 -0
- package/build/schema.d.ts.map +1 -0
- package/build/schema.js +83 -0
- package/build/schema.js.map +1 -0
- package/build/sync.d.ts +78 -0
- package/build/sync.d.ts.map +1 -0
- package/build/sync.js +236 -0
- package/build/sync.js.map +1 -0
- package/build/use-cloud-sync.d.ts +5 -0
- package/build/use-cloud-sync.d.ts.map +1 -0
- package/build/use-cloud-sync.js +6 -0
- package/build/use-cloud-sync.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/CloudSyncEngineObject.swift +62 -0
- package/ios/ExpoKitCloudKit.podspec +28 -0
- package/ios/ExpoKitCloudKitModule.swift +50 -0
- package/ios/LocalStore.swift +351 -0
- package/ios/PushSubscriber.swift +14 -0
- package/ios/RecordMapper.swift +237 -0
- package/ios/Records.swift +60 -0
- package/ios/SchemaModel.swift +68 -0
- package/ios/SyncCore.swift +586 -0
- package/package.json +61 -4
- package/plugin/build/index.d.ts +7 -0
- package/plugin/build/index.js +68 -0
- package/src/codec.ts +36 -0
- package/src/index.ts +26 -0
- package/src/native.ts +83 -0
- package/src/schema.ts +169 -0
- package/src/sync.ts +322 -0
- package/src/use-cloud-sync.ts +9 -0
package/README.md
CHANGED
|
@@ -1,3 +1,146 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @expo-kit/cloudkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed iCloud sync for Expo apps. Your app keeps its own database; this package syncs its records between the user's devices through their private iCloud, using Apple's `CKSyncEngine` in Swift. Part of expo-kit, small packages for Expo apps. Not made by Expo or Apple.
|
|
4
|
+
|
|
5
|
+
- Works with any data: you describe your records once in TypeScript, and every call is typed from that.
|
|
6
|
+
- Native Swift on JSI (Expo Modules API), no old bridge.
|
|
7
|
+
- Offline-safe: changes wait on the device (in the module's own SQLite file) and upload later, also after the app was killed.
|
|
8
|
+
- A fresh install downloads first, so it never overwrites what's already in iCloud.
|
|
9
|
+
- Files (assets) keep their file type.
|
|
10
|
+
|
|
11
|
+
iOS 17+ only. Needs a development build (not Expo Go).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx expo install @expo-kit/cloudkit expo-build-properties
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`app.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"expo": {
|
|
24
|
+
"ios": { "bundleIdentifier": "com.example.app" },
|
|
25
|
+
"plugins": [
|
|
26
|
+
["@expo-kit/cloudkit", { "containers": ["iCloud.com.example.app"] }],
|
|
27
|
+
["expo-build-properties", { "ios": { "enableSceneSupport": true } }]
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- The plugin adds the iCloud entitlements, background pushes, and sets iOS 17 as the minimum.
|
|
34
|
+
- `enableSceneSupport` is needed for Expo SDK 57 apps built with Xcode 27 (the app crashes at launch without it). SDK 58+ doesn't need it.
|
|
35
|
+
- The first signed build registers the container on your Apple team (Xcode automatic signing does this).
|
|
36
|
+
|
|
37
|
+
## 1. Describe the data
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { defineCloudSchema, field, type CloudRecord } from '@expo-kit/cloudkit';
|
|
41
|
+
|
|
42
|
+
export const schema = defineCloudSchema({
|
|
43
|
+
Venue: { name: field.string() },
|
|
44
|
+
Shift: {
|
|
45
|
+
date: field.date(),
|
|
46
|
+
tips: field.double(),
|
|
47
|
+
hours: field.int(),
|
|
48
|
+
paid: field.bool(),
|
|
49
|
+
note: field.string().optional(),
|
|
50
|
+
tags: field.stringList(),
|
|
51
|
+
venue: field.reference('Venue'),
|
|
52
|
+
photo: field.asset().optional(),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type Shift = CloudRecord<typeof schema, 'Shift'>;
|
|
57
|
+
// { id: string; date: Date; tips: number; hours: number; paid: boolean; tags: string[]; venue: string;
|
|
58
|
+
// note?: string | null; photo?: string | null }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Field kinds: `string`, `int`, `double`, `bool`, `date`, `stringList`, `intList`, `doubleList`, `reference(type)` (the linked record's `id`), `asset` (a local file path). Add `.optional()` for a field that can be empty.
|
|
62
|
+
|
|
63
|
+
Every record has a string `id`, unique within its type (ASCII). After release, only **add** types and fields; never rename or remove one.
|
|
64
|
+
|
|
65
|
+
## 2. Connect your database
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createCloudSync } from '@expo-kit/cloudkit';
|
|
69
|
+
|
|
70
|
+
export const sync = createCloudSync({
|
|
71
|
+
container: 'iCloud.com.example.app',
|
|
72
|
+
schema,
|
|
73
|
+
apply: {
|
|
74
|
+
// Changes from other devices. Insert-or-update, and delete.
|
|
75
|
+
Shift: { save: (shifts) => db.upsertShifts(shifts), delete: (ids) => db.deleteShifts(ids) },
|
|
76
|
+
Venue: { save: (venues) => db.upsertVenues(venues), delete: (ids) => db.deleteVenues(ids) },
|
|
77
|
+
},
|
|
78
|
+
conflict: 'newest', // or 'server' | 'device'
|
|
79
|
+
onAccountChange: ({ change }) => {
|
|
80
|
+
// 'signOut' | 'switchAccounts' | 'cloudDataDeleted': sync state was reset.
|
|
81
|
+
// Keep or clear local data; to upload it to the new account, call importAll.
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Once, when the app's database is ready:
|
|
86
|
+
await sync.start();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`apply` handlers may throw (e.g. a foreign key isn't there yet): that group is retried after the others, and nothing is lost.
|
|
90
|
+
|
|
91
|
+
## 3. Tell it about your own writes
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await db.insertShift(shift);
|
|
95
|
+
sync.save('Shift', shift); // typed: a wrong field or type doesn't compile
|
|
96
|
+
|
|
97
|
+
await db.deleteVenue(id);
|
|
98
|
+
sync.delete('Venue', id);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
First time sync is turned on for existing data: `sync.importAll('Shift', allShifts)`.
|
|
102
|
+
|
|
103
|
+
## Status
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
const { supported, account, syncing, initialSyncDone, pendingUploads, pendingChanges, lastSyncedAt, error } = useCloudSync(sync);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`sync.syncNow()` syncs right away (e.g. pull to refresh). It also syncs when the app comes back to the front, and when another device pushes a change.
|
|
110
|
+
|
|
111
|
+
## Conflicts
|
|
112
|
+
|
|
113
|
+
- `'newest'` (default): the later edit wins. A delete from another device wins over a local edit.
|
|
114
|
+
- `'server'`: iCloud's version wins.
|
|
115
|
+
- `'device'`: this device's version wins, also over a delete.
|
|
116
|
+
|
|
117
|
+
Fields your app doesn't know (e.g. added by a newer app version on another device) are never removed from iCloud.
|
|
118
|
+
|
|
119
|
+
## Release checklist
|
|
120
|
+
|
|
121
|
+
- In the [CloudKit Console](https://icloud.developer.apple.com), deploy the schema to production before the App Store release. In development, record types and fields are created automatically on first save.
|
|
122
|
+
- Reserved field names: `id`, `ekModifiedAt`, anything starting with `ek_`, and CloudKit's own (`recordName`, `createdAt`, …).
|
|
123
|
+
|
|
124
|
+
## What's tested
|
|
125
|
+
|
|
126
|
+
Tested on a real iPhone (iOS 27, Xcode 27, Expo SDK 57):
|
|
127
|
+
|
|
128
|
+
- Upload, and download on a fresh install (103 records + links between them)
|
|
129
|
+
- Records arriving in any order (a note before its folder)
|
|
130
|
+
- Files: upload and download, file type kept
|
|
131
|
+
|
|
132
|
+
Tested with automated tests only:
|
|
133
|
+
|
|
134
|
+
- Waiting changes survive an app kill (the module's SQLite store)
|
|
135
|
+
- Value checks and conversions for every field kind
|
|
136
|
+
- Typed API: wrong records don't compile
|
|
137
|
+
|
|
138
|
+
Built but not tested yet:
|
|
139
|
+
|
|
140
|
+
- Two devices editing the same record (conflict rules `newest`, `server`, `device`)
|
|
141
|
+
- Push: a change on one device showing up on another without opening the app
|
|
142
|
+
- Signing out of iCloud, switching accounts, deleting the app's iCloud data
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./plugin/build');
|
package/build/codec.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CloudSchema } from './schema';
|
|
2
|
+
/** App record → plain fields for the native side (dates as milliseconds, no `id`, no `undefined`). */
|
|
3
|
+
export declare function encodeFields(schema: CloudSchema, type: string, record: Record<string, unknown>): Record<string, unknown>;
|
|
4
|
+
/** Native fields → app record (milliseconds back to `Date`, empty optionals as `null`). */
|
|
5
|
+
export declare function decodeRecord(schema: CloudSchema, type: string, id: string, fields: Record<string, unknown>): Record<string, unknown> | null;
|
|
6
|
+
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,sGAAsG;AACtG,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASxH;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAC1B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAehC"}
|
package/build/codec.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** App record → plain fields for the native side (dates as milliseconds, no `id`, no `undefined`). */
|
|
2
|
+
export function encodeFields(schema, type, record) {
|
|
3
|
+
const fields = schema.types[type];
|
|
4
|
+
if (!fields)
|
|
5
|
+
throw new Error(`"${type}" isn't a record type in the schema.`);
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const [key, value] of Object.entries(record)) {
|
|
8
|
+
if (key === 'id' || value === undefined)
|
|
9
|
+
continue;
|
|
10
|
+
result[key] = value instanceof Date ? value.getTime() : value;
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
/** Native fields → app record (milliseconds back to `Date`, empty optionals as `null`). */
|
|
15
|
+
export function decodeRecord(schema, type, id, fields) {
|
|
16
|
+
const spec = schema.types[type];
|
|
17
|
+
if (!spec)
|
|
18
|
+
return null;
|
|
19
|
+
const record = { id };
|
|
20
|
+
for (const [name, field] of Object.entries(spec)) {
|
|
21
|
+
const value = fields[name];
|
|
22
|
+
if (value === undefined || value === null) {
|
|
23
|
+
record[name] = null;
|
|
24
|
+
}
|
|
25
|
+
else if (field.kind === 'date' && typeof value === 'number') {
|
|
26
|
+
record[name] = new Date(value);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
record[name] = value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return record;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.js","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAEA,sGAAsG;AACtG,MAAM,UAAU,YAAY,CAAC,MAAmB,EAAE,IAAY,EAAE,MAA+B;IAC7F,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,sCAAsC,CAAC,CAAC;IAC7E,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IAChE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,YAAY,CAC1B,MAAmB,EACnB,IAAY,EACZ,EAAU,EACV,MAA+B;IAE/B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,MAAM,GAA4B,EAAE,EAAE,EAAE,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC1C,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QACtB,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { defineCloudSchema, field, type CloudRecord, type CloudSchema, type Field, type FieldKind, type RecordTypeName, } from './schema';
|
|
2
|
+
export { createCloudSync, type ApplyHandlers, type CloudSync, type CloudSyncOptions, type CloudSyncStatus, type ConflictRule, } from './sync';
|
|
3
|
+
export { useCloudSync } from './use-cloud-sync';
|
|
4
|
+
export type { AccountChangeKind, AccountStatus } from './native';
|
|
5
|
+
/** `true` when the native module is linked (a development or release build on iOS). */
|
|
6
|
+
export declare function isCloudKitSupported(): boolean;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,KAAK,EACL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,cAAc,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEjE,uFAAuF;AACvF,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { nativeModule } from './native';
|
|
2
|
+
export { defineCloudSchema, field, } from './schema';
|
|
3
|
+
export { createCloudSync, } from './sync';
|
|
4
|
+
export { useCloudSync } from './use-cloud-sync';
|
|
5
|
+
/** `true` when the native module is linked (a development or release build on iOS). */
|
|
6
|
+
export function isCloudKitSupported() {
|
|
7
|
+
return nativeModule?.isSupported() ?? false;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,EACL,iBAAiB,EACjB,KAAK,GAMN,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,eAAe,GAMhB,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,uFAAuF;AACvF,MAAM,UAAU,mBAAmB;IACjC,OAAO,YAAY,EAAE,WAAW,EAAE,IAAI,KAAK,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { SharedObject, type NativeModule } from 'expo';
|
|
2
|
+
import type { SchemaSpec } from './schema';
|
|
3
|
+
export type AccountStatus = 'available' | 'noAccount' | 'restricted' | 'temporarilyUnavailable' | 'couldNotDetermine' | 'unknown';
|
|
4
|
+
export type NativeStatus = {
|
|
5
|
+
account: AccountStatus;
|
|
6
|
+
syncing: boolean;
|
|
7
|
+
initialSyncDone: boolean;
|
|
8
|
+
pendingUploads: number;
|
|
9
|
+
pendingChanges: number;
|
|
10
|
+
/** Milliseconds since 1970. */
|
|
11
|
+
lastSyncedAt: number | null;
|
|
12
|
+
error: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type NativeChange = {
|
|
15
|
+
sequence: number;
|
|
16
|
+
type: string;
|
|
17
|
+
id: string;
|
|
18
|
+
operation: 'save' | 'delete';
|
|
19
|
+
fields: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
export type AccountChangeKind = 'signIn' | 'signOut' | 'switchAccounts' | 'cloudDataDeleted' | 'unknown';
|
|
22
|
+
export type NativeEvents = {
|
|
23
|
+
status: (status: NativeStatus) => void;
|
|
24
|
+
changes: () => void;
|
|
25
|
+
account: (change: {
|
|
26
|
+
change: AccountChangeKind;
|
|
27
|
+
account: AccountStatus;
|
|
28
|
+
}) => void;
|
|
29
|
+
error: (error: {
|
|
30
|
+
message: string;
|
|
31
|
+
context: string;
|
|
32
|
+
}) => void;
|
|
33
|
+
};
|
|
34
|
+
export type EngineOptions = {
|
|
35
|
+
containerId: string;
|
|
36
|
+
zoneName: string;
|
|
37
|
+
conflict: 'newest' | 'server' | 'device';
|
|
38
|
+
schema: SchemaSpec;
|
|
39
|
+
};
|
|
40
|
+
/** The native engine. Everything that touches iCloud runs in Swift. */
|
|
41
|
+
export interface NativeEngine {
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
syncNow(): Promise<void>;
|
|
44
|
+
stop(): Promise<void>;
|
|
45
|
+
save(input: {
|
|
46
|
+
type: string;
|
|
47
|
+
id: string;
|
|
48
|
+
fields: Record<string, unknown>;
|
|
49
|
+
}): void;
|
|
50
|
+
delete(input: {
|
|
51
|
+
type: string;
|
|
52
|
+
id: string;
|
|
53
|
+
}): void;
|
|
54
|
+
pendingChanges(limit: number): Promise<NativeChange[]>;
|
|
55
|
+
ack(sequences: number[]): Promise<void>;
|
|
56
|
+
status(): NativeStatus;
|
|
57
|
+
addListener<E extends keyof NativeEvents>(event: E, listener: NativeEvents[E]): {
|
|
58
|
+
remove(): void;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
declare class CloudSyncEngine extends SharedObject<NativeEvents> implements NativeEngine {
|
|
62
|
+
constructor(options: EngineOptions);
|
|
63
|
+
start(): Promise<void>;
|
|
64
|
+
syncNow(): Promise<void>;
|
|
65
|
+
stop(): Promise<void>;
|
|
66
|
+
save(input: {
|
|
67
|
+
type: string;
|
|
68
|
+
id: string;
|
|
69
|
+
fields: Record<string, unknown>;
|
|
70
|
+
}): void;
|
|
71
|
+
delete(input: {
|
|
72
|
+
type: string;
|
|
73
|
+
id: string;
|
|
74
|
+
}): void;
|
|
75
|
+
pendingChanges(limit: number): Promise<NativeChange[]>;
|
|
76
|
+
ack(sequences: number[]): Promise<void>;
|
|
77
|
+
status(): NativeStatus;
|
|
78
|
+
}
|
|
79
|
+
declare class ExpoKitCloudKitModule extends NativeModule {
|
|
80
|
+
isSupported(): boolean;
|
|
81
|
+
CloudSyncEngine: typeof CloudSyncEngine;
|
|
82
|
+
}
|
|
83
|
+
export declare const nativeModule: ExpoKitCloudKitModule | null;
|
|
84
|
+
export declare function createNativeEngine(options: EngineOptions): NativeEngine | null;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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"}
|
package/build/native.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { requireOptionalNativeModule } from 'expo';
|
|
2
|
+
// Null in Expo Go, on Android and on web, where the native module isn't linked.
|
|
3
|
+
export const nativeModule = requireOptionalNativeModule('ExpoKitCloudKit');
|
|
4
|
+
export function createNativeEngine(options) {
|
|
5
|
+
return nativeModule ? new nativeModule.CloudSyncEngine(options) : null;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAmC,MAAM,MAAM,CAAC;AA6EpF,gFAAgF;AAChF,MAAM,CAAC,MAAM,YAAY,GAAG,2BAA2B,CAAwB,iBAAiB,CAAC,CAAC;AAElG,MAAM,UAAU,kBAAkB,CAAC,OAAsB;IACvD,OAAO,YAAY,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export type FieldKind = 'string' | 'int' | 'double' | 'bool' | 'date' | 'stringList' | 'intList' | 'doubleList' | 'reference' | 'asset';
|
|
2
|
+
type ValueOfKind = {
|
|
3
|
+
string: string;
|
|
4
|
+
int: number;
|
|
5
|
+
double: number;
|
|
6
|
+
bool: boolean;
|
|
7
|
+
date: Date;
|
|
8
|
+
stringList: string[];
|
|
9
|
+
intList: number[];
|
|
10
|
+
doubleList: number[];
|
|
11
|
+
/** The ID of the linked record. */
|
|
12
|
+
reference: string;
|
|
13
|
+
/** A local file path (`file://…` or an absolute path). */
|
|
14
|
+
asset: string;
|
|
15
|
+
};
|
|
16
|
+
export type Field<K extends FieldKind = FieldKind, Optional extends boolean = boolean, Target extends string = string> = {
|
|
17
|
+
readonly kind: K;
|
|
18
|
+
readonly isOptional: Optional;
|
|
19
|
+
/** For references: the record type it links to. */
|
|
20
|
+
readonly target: K extends 'reference' ? Target : undefined;
|
|
21
|
+
};
|
|
22
|
+
/** A field that can also be left out or set to `null`. */
|
|
23
|
+
type Optionalizable<K extends FieldKind, Target extends string = string> = Field<K, false, Target> & {
|
|
24
|
+
optional(): Field<K, true, Target>;
|
|
25
|
+
};
|
|
26
|
+
/** Field kinds for `defineCloudSchema`. Add `.optional()` for a field that can be empty. */
|
|
27
|
+
export declare const field: {
|
|
28
|
+
string: () => Optionalizable<"string", string>;
|
|
29
|
+
/** Whole numbers. Stored as a 64-bit integer. */
|
|
30
|
+
int: () => Optionalizable<"int", string>;
|
|
31
|
+
double: () => Optionalizable<"double", string>;
|
|
32
|
+
bool: () => Optionalizable<"bool", string>;
|
|
33
|
+
date: () => Optionalizable<"date", string>;
|
|
34
|
+
stringList: () => Optionalizable<"stringList", string>;
|
|
35
|
+
intList: () => Optionalizable<"intList", string>;
|
|
36
|
+
doubleList: () => Optionalizable<"doubleList", string>;
|
|
37
|
+
/** A link to another record type in the same schema, by its ID. */
|
|
38
|
+
reference: <Target extends string>(target: Target) => Optionalizable<"reference", Target>;
|
|
39
|
+
/** A file stored in iCloud. The app passes and gets back a local file path. */
|
|
40
|
+
asset: () => Optionalizable<"asset", string>;
|
|
41
|
+
};
|
|
42
|
+
type AnyField = Field<FieldKind, boolean, string>;
|
|
43
|
+
type FieldsShape = Record<string, AnyField>;
|
|
44
|
+
export type SchemaShape = Record<string, FieldsShape>;
|
|
45
|
+
/** Every reference must point to a record type in the same schema. */
|
|
46
|
+
type CheckReferences<S extends SchemaShape> = {
|
|
47
|
+
[T in keyof S]: {
|
|
48
|
+
[F in keyof S[T]]: S[T][F] extends Field<'reference', boolean, infer Target> ? Target extends keyof S ? S[T][F] : Field<'reference', boolean, Extract<keyof S, string>> : S[T][F];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export type CloudSchema<S extends SchemaShape = SchemaShape> = {
|
|
52
|
+
readonly types: S;
|
|
53
|
+
};
|
|
54
|
+
/** The names of the record types in a schema, e.g. `'Shift' | 'Venue'`. */
|
|
55
|
+
export type RecordTypeName<S extends CloudSchema> = Extract<keyof S['types'], string>;
|
|
56
|
+
type FieldValue<F> = F extends Field<infer K, boolean, string> ? ValueOfKind[K] : never;
|
|
57
|
+
type RequiredKeys<Fs> = {
|
|
58
|
+
[K in keyof Fs]: Fs[K] extends Field<FieldKind, false, string> ? K : never;
|
|
59
|
+
}[keyof Fs];
|
|
60
|
+
type OptionalKeys<Fs> = {
|
|
61
|
+
[K in keyof Fs]: Fs[K] extends Field<FieldKind, false, string> ? never : K;
|
|
62
|
+
}[keyof Fs];
|
|
63
|
+
type Simplify<T> = {
|
|
64
|
+
[K in keyof T]: T[K];
|
|
65
|
+
} & {};
|
|
66
|
+
/**
|
|
67
|
+
* The TypeScript type of one record, e.g. `CloudRecord<typeof schema, 'Shift'>`.
|
|
68
|
+
* Every record has a string `id`. Optional fields can be left out or `null`.
|
|
69
|
+
*/
|
|
70
|
+
export type CloudRecord<S extends CloudSchema, T extends RecordTypeName<S>> = Simplify<{
|
|
71
|
+
id: string;
|
|
72
|
+
} & {
|
|
73
|
+
[K in RequiredKeys<S['types'][T]>]: FieldValue<S['types'][T][K]>;
|
|
74
|
+
} & {
|
|
75
|
+
[K in OptionalKeys<S['types'][T]>]?: FieldValue<S['types'][T][K]> | null;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* Describes the app's data for iCloud. One entry per record type, one field kind per field.
|
|
79
|
+
* Throws when a name isn't allowed in CloudKit or a reference points to an unknown type.
|
|
80
|
+
*
|
|
81
|
+
* Rule after release: only add fields and types. Never rename or remove one.
|
|
82
|
+
*/
|
|
83
|
+
export declare function defineCloudSchema<const S extends SchemaShape>(types: S & CheckReferences<S>): CloudSchema<S>;
|
|
84
|
+
export type SchemaSpec = {
|
|
85
|
+
types: {
|
|
86
|
+
name: string;
|
|
87
|
+
fields: {
|
|
88
|
+
name: string;
|
|
89
|
+
kind: FieldKind;
|
|
90
|
+
optional: boolean;
|
|
91
|
+
target: string | null;
|
|
92
|
+
}[];
|
|
93
|
+
}[];
|
|
94
|
+
};
|
|
95
|
+
/** The plain description sent to the native side. */
|
|
96
|
+
export declare function toSchemaSpec(schema: CloudSchema): SchemaSpec;
|
|
97
|
+
export {};
|
|
98
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/build/schema.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function make(kind, target) {
|
|
2
|
+
const base = { kind, isOptional: false, target: (target ?? undefined) };
|
|
3
|
+
return { ...base, optional: () => ({ ...base, isOptional: true }) };
|
|
4
|
+
}
|
|
5
|
+
/** Field kinds for `defineCloudSchema`. Add `.optional()` for a field that can be empty. */
|
|
6
|
+
export const field = {
|
|
7
|
+
string: () => make('string'),
|
|
8
|
+
/** Whole numbers. Stored as a 64-bit integer. */
|
|
9
|
+
int: () => make('int'),
|
|
10
|
+
double: () => make('double'),
|
|
11
|
+
bool: () => make('bool'),
|
|
12
|
+
date: () => make('date'),
|
|
13
|
+
stringList: () => make('stringList'),
|
|
14
|
+
intList: () => make('intList'),
|
|
15
|
+
doubleList: () => make('doubleList'),
|
|
16
|
+
/** A link to another record type in the same schema, by its ID. */
|
|
17
|
+
reference: (target) => make('reference', target),
|
|
18
|
+
/** A file stored in iCloud. The app passes and gets back a local file path. */
|
|
19
|
+
asset: () => make('asset'),
|
|
20
|
+
};
|
|
21
|
+
/** Names CloudKit or this package use for themselves. */
|
|
22
|
+
const RESERVED_FIELD_NAMES = new Set([
|
|
23
|
+
'id',
|
|
24
|
+
'recordName',
|
|
25
|
+
'recordID',
|
|
26
|
+
'recordType',
|
|
27
|
+
'createdAt',
|
|
28
|
+
'modifiedAt',
|
|
29
|
+
'createdBy',
|
|
30
|
+
'modifiedBy',
|
|
31
|
+
'creationDate',
|
|
32
|
+
'modificationDate',
|
|
33
|
+
'creatorUserRecordID',
|
|
34
|
+
'lastModifiedUserRecordID',
|
|
35
|
+
'recordChangeTag',
|
|
36
|
+
'ekModifiedAt',
|
|
37
|
+
]);
|
|
38
|
+
// CloudKit: starts with a letter, then letters, digits or underscores, at most 255 characters.
|
|
39
|
+
const NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,254}$/;
|
|
40
|
+
/**
|
|
41
|
+
* Describes the app's data for iCloud. One entry per record type, one field kind per field.
|
|
42
|
+
* Throws when a name isn't allowed in CloudKit or a reference points to an unknown type.
|
|
43
|
+
*
|
|
44
|
+
* Rule after release: only add fields and types. Never rename or remove one.
|
|
45
|
+
*/
|
|
46
|
+
export function defineCloudSchema(types) {
|
|
47
|
+
const typeNames = Object.keys(types);
|
|
48
|
+
if (typeNames.length === 0)
|
|
49
|
+
throw new Error('defineCloudSchema: add at least one record type.');
|
|
50
|
+
for (const typeName of typeNames) {
|
|
51
|
+
if (!NAME_PATTERN.test(typeName)) {
|
|
52
|
+
throw new Error(`defineCloudSchema: "${typeName}" isn't a valid record type name (letters, digits, _; starts with a letter).`);
|
|
53
|
+
}
|
|
54
|
+
const fields = types[typeName];
|
|
55
|
+
for (const [fieldName, spec] of Object.entries(fields)) {
|
|
56
|
+
if (!NAME_PATTERN.test(fieldName)) {
|
|
57
|
+
throw new Error(`defineCloudSchema: ${typeName}.${fieldName} isn't a valid field name (letters, digits, _; starts with a letter).`);
|
|
58
|
+
}
|
|
59
|
+
if (RESERVED_FIELD_NAMES.has(fieldName) || fieldName.startsWith('ek_')) {
|
|
60
|
+
throw new Error(`defineCloudSchema: ${typeName}.${fieldName} is a reserved name. Pick another one.`);
|
|
61
|
+
}
|
|
62
|
+
if (spec.kind === 'reference' && !typeNames.includes(spec.target)) {
|
|
63
|
+
throw new Error(`defineCloudSchema: ${typeName}.${fieldName} links to "${String(spec.target)}", which isn't in the schema.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { types: types };
|
|
68
|
+
}
|
|
69
|
+
/** The plain description sent to the native side. */
|
|
70
|
+
export function toSchemaSpec(schema) {
|
|
71
|
+
return {
|
|
72
|
+
types: Object.entries(schema.types).map(([name, fields]) => ({
|
|
73
|
+
name,
|
|
74
|
+
fields: Object.entries(fields).map(([fieldName, spec]) => ({
|
|
75
|
+
name: fieldName,
|
|
76
|
+
kind: spec.kind,
|
|
77
|
+
optional: spec.isOptional,
|
|
78
|
+
target: spec.kind === 'reference' ? spec.target : null,
|
|
79
|
+
})),
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/build/sync.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createNativeEngine, type AccountChangeKind, type AccountStatus } from './native';
|
|
2
|
+
import { type CloudRecord, type CloudSchema, type RecordTypeName } from './schema';
|
|
3
|
+
export type ConflictRule = 'newest' | 'server' | 'device';
|
|
4
|
+
type MaybePromise = void | Promise<void>;
|
|
5
|
+
/** How the app writes changes from iCloud into its own database, per record type. */
|
|
6
|
+
export type ApplyHandlers<S extends CloudSchema> = {
|
|
7
|
+
[T in RecordTypeName<S>]: {
|
|
8
|
+
/** New or changed records from another device. Insert or update them. */
|
|
9
|
+
save: (records: CloudRecord<S, T>[]) => MaybePromise;
|
|
10
|
+
/** Records deleted on another device. */
|
|
11
|
+
delete: (ids: string[]) => MaybePromise;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type CloudSyncOptions<S extends CloudSchema> = {
|
|
15
|
+
/** iCloud container ID, also listed in the config plugin, e.g. `'iCloud.com.example.app'`. */
|
|
16
|
+
container: string;
|
|
17
|
+
schema: S;
|
|
18
|
+
apply: ApplyHandlers<S>;
|
|
19
|
+
/**
|
|
20
|
+
* Who wins when the same record changed on two devices. Default `'newest'`: the later edit wins,
|
|
21
|
+
* and a delete from another device wins over a local edit. `'server'`: iCloud wins. `'device'`: this device wins.
|
|
22
|
+
*/
|
|
23
|
+
conflict?: ConflictRule;
|
|
24
|
+
/** Default `'AppData'`. Only change it to run two separate syncs in one app. */
|
|
25
|
+
zone?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Called when the iCloud account changes (signed out, switched accounts) or this app's iCloud
|
|
28
|
+
* data was deleted. Sync state is reset; the app decides what to do with its local data,
|
|
29
|
+
* e.g. keep it and call `importAll` to upload it to the new account.
|
|
30
|
+
*/
|
|
31
|
+
onAccountChange?: (event: {
|
|
32
|
+
change: AccountChangeKind;
|
|
33
|
+
account: AccountStatus;
|
|
34
|
+
}) => void;
|
|
35
|
+
/** Sync when the app comes back to the front. Default `true`. */
|
|
36
|
+
syncOnForeground?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type CloudSyncStatus = {
|
|
39
|
+
/** `false` in Expo Go, on Android and on web. Everything else is then a no-op. */
|
|
40
|
+
supported: boolean;
|
|
41
|
+
account: AccountStatus;
|
|
42
|
+
syncing: boolean;
|
|
43
|
+
/** The first download finished. Local changes upload only after that. */
|
|
44
|
+
initialSyncDone: boolean;
|
|
45
|
+
/** Local changes not yet in iCloud. */
|
|
46
|
+
pendingUploads: number;
|
|
47
|
+
/** Changes from iCloud not yet applied to the app's database. */
|
|
48
|
+
pendingChanges: number;
|
|
49
|
+
lastSyncedAt: Date | null;
|
|
50
|
+
/** The last error, if the last sync didn't finish. */
|
|
51
|
+
error: string | null;
|
|
52
|
+
};
|
|
53
|
+
export type CloudSync<S extends CloudSchema> = {
|
|
54
|
+
/** Starts syncing: downloads first, then uploads. Call once, e.g. after the app's database is ready. */
|
|
55
|
+
start(): Promise<void>;
|
|
56
|
+
/** Queues a new or changed record for upload. Call after every local insert or update. */
|
|
57
|
+
save<T extends RecordTypeName<S>>(type: T, record: CloudRecord<S, T>): void;
|
|
58
|
+
/** Queues a delete. Call after every local delete. */
|
|
59
|
+
delete<T extends RecordTypeName<S>>(type: T, id: string): void;
|
|
60
|
+
/** Queues many records at once, e.g. existing data the first time sync is turned on. */
|
|
61
|
+
importAll<T extends RecordTypeName<S>>(type: T, records: CloudRecord<S, T>[]): void;
|
|
62
|
+
/** Downloads and uploads now, e.g. pull to refresh. */
|
|
63
|
+
syncNow(): Promise<void>;
|
|
64
|
+
getStatus(): CloudSyncStatus;
|
|
65
|
+
/** Calls `listener` on every status change. Returns a function that stops it. */
|
|
66
|
+
subscribe(listener: (status: CloudSyncStatus) => void): () => void;
|
|
67
|
+
/** Stops syncing and releases the native engine. */
|
|
68
|
+
stop(): Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Creates the sync for one schema. Create it once (module level) and call `start()`.
|
|
72
|
+
* In Expo Go (no native module) every method is a safe no-op.
|
|
73
|
+
*/
|
|
74
|
+
export declare function createCloudSync<S extends CloudSchema>(options: CloudSyncOptions<S>): CloudSync<S>;
|
|
75
|
+
/** Internal: lets tests pass a fake engine. */
|
|
76
|
+
export declare function createCloudSyncWithEngine<S extends CloudSchema>(options: CloudSyncOptions<S>, makeEngine: typeof createNativeEngine): CloudSync<S>;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"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"}
|