@protobase/cli 0.73.0 → 0.73.1
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/dist/cli/package.js +1 -1
- package/dist/mobile/build.js +19 -10
- package/dist/mobile/capacitor-config.js +5 -3
- package/dist/mobile/ios-release.js +99 -0
- package/dist/mobile/run.js +2 -1
- package/package.json +8 -8
- package/skills/protobase/references/guides/mobile-app.md +3 -1
- package/skills/protobase/references/reference/api.md +1 -1
- package/skills/protobase/references/reference/cli.md +1 -1
- package/skills/protobase/references/reference/client.md +1 -1
package/dist/cli/package.js
CHANGED
package/dist/mobile/build.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { androidSigning } from "./android-signing.js";
|
|
2
2
|
import { runChild } from "./child-process.js";
|
|
3
|
+
import { buildIosAutomatic, iosApiKey } from "./ios-release.js";
|
|
3
4
|
import { copyRelease, releaseFile } from "./release-output.js";
|
|
4
5
|
import { prepareReleaseProject } from "./release-project.js";
|
|
5
6
|
import { prepare, summary } from "./run.js";
|
|
@@ -11,25 +12,24 @@ const gradleArgs = [
|
|
|
11
12
|
"--no-configuration-cache",
|
|
12
13
|
"-Pkotlin.compiler.execution.strategy=in-process"
|
|
13
14
|
];
|
|
14
|
-
const
|
|
15
|
+
const capBuildManual = (team, { certificate, profile }) => [
|
|
15
16
|
"build",
|
|
16
17
|
"ios",
|
|
17
18
|
"--xcode-team-id",
|
|
18
19
|
team,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
manual.profile
|
|
26
|
-
] : []
|
|
20
|
+
"--xcode-signing-style",
|
|
21
|
+
"manual",
|
|
22
|
+
"--xcode-signing-certificate",
|
|
23
|
+
certificate,
|
|
24
|
+
"--xcode-provisioning-profile",
|
|
25
|
+
profile
|
|
27
26
|
];
|
|
28
27
|
/** `protobase mobile build`: generates the native project as a release, builds it signed and copies the binary out. */
|
|
29
28
|
const mobileBuild = async (input, out, deps = {}) => {
|
|
30
29
|
const { version, outDir, ios, ...mobile } = input;
|
|
31
30
|
if (mobile.platform === "ios" && !ios) throw new Error("An iOS release is signed by your Apple Developer team: pass --team");
|
|
32
31
|
const signing = mobile.platform === "android" ? androidSigning(process.env, mobile.projectDir) : void 0;
|
|
32
|
+
const apiKey = ios && !ios.manual ? iosApiKey(process.env, mobile.projectDir) : void 0;
|
|
33
33
|
const prepared = await prepare({
|
|
34
34
|
...mobile,
|
|
35
35
|
release: true
|
|
@@ -45,7 +45,16 @@ const mobileBuild = async (input, out, deps = {}) => {
|
|
|
45
45
|
...signing.env
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
-
} else
|
|
48
|
+
} else {
|
|
49
|
+
const { team, manual } = ios;
|
|
50
|
+
if (manual) await prepared.cap(capBuildManual(team, manual));
|
|
51
|
+
else await buildIosAutomatic({
|
|
52
|
+
ws: prepared.ws,
|
|
53
|
+
env: prepared.env,
|
|
54
|
+
team,
|
|
55
|
+
key: apiKey
|
|
56
|
+
}, deps.run ?? runChild);
|
|
57
|
+
}
|
|
49
58
|
const file = await copyRelease(releaseFile(prepared.ws, mobile.platform), outDir, `${prepared.appId}-${version.version}-${version.build}`);
|
|
50
59
|
out(`release ${path.relative(mobile.projectDir, file)}\n`);
|
|
51
60
|
};
|
|
@@ -8,7 +8,8 @@ const liveUpdatePlugin = "@capawesome/capacitor-live-update";
|
|
|
8
8
|
const readyTimeout = 3e4;
|
|
9
9
|
/**
|
|
10
10
|
* The app's `capacitor.config.json`, every key it sets named here. It never sets `server.url`, the hostname or the
|
|
11
|
-
* schemes (the API's CORS list names Capacitor's default origins)
|
|
11
|
+
* schemes (the API's CORS list names Capacitor's default origins). WebView debugging is off in an iOS release by name:
|
|
12
|
+
* Capacitor's prebuilt simulator framework is a Debug build, so its default lists a Release app in Safari's Develop menu.
|
|
12
13
|
* `plugins` configures ProtobaseAuth, the keyboard plugin and, with the live updater, what its guard needs: the key
|
|
13
14
|
* releases are signed with, where they come from, the binary's fingerprint, shell version and the order of its own
|
|
14
15
|
* files, background updates off, a rolled-back bundle blocked and `readyTimeout`, within which the shell must call
|
|
@@ -19,7 +20,7 @@ const readyTimeout = 3e4;
|
|
|
19
20
|
* guards it. The file ships outside the app's web part, so the fingerprint in the user agent and ProtobaseAuth's
|
|
20
21
|
* origins are beyond page script and a live update.
|
|
21
22
|
*/
|
|
22
|
-
const capacitorConfig = ({ appId, appName, fingerprint, shellVersion, plugins, liveUpdate, allowances, apiOrigin, platform }) => ({
|
|
23
|
+
const capacitorConfig = ({ appId, appName, fingerprint, shellVersion, plugins, liveUpdate, allowances, apiOrigin, platform, release }) => ({
|
|
23
24
|
appId,
|
|
24
25
|
appName,
|
|
25
26
|
webDir: "web",
|
|
@@ -50,7 +51,8 @@ const capacitorConfig = ({ appId, appName, fingerprint, shellVersion, plugins, l
|
|
|
50
51
|
android: {
|
|
51
52
|
resolveServiceWorkerRequests: false,
|
|
52
53
|
...allowances.mixedContent && { allowMixedContent: true }
|
|
53
|
-
}
|
|
54
|
+
},
|
|
55
|
+
...release && platform === "ios" && { ios: { webContentsDebuggingEnabled: false } }
|
|
54
56
|
});
|
|
55
57
|
/** Writes the config into the workspace `dir`, removing the `.ts` and `.js` configs Capacitor would read instead. */
|
|
56
58
|
const writeCapacitorConfig = async (dir, config) => {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
4
|
+
//#region src/mobile/ios-release.ts
|
|
5
|
+
const apiKeyVariables = [
|
|
6
|
+
"PROTOBASE_IOS_API_KEY_PATH",
|
|
7
|
+
"PROTOBASE_IOS_API_KEY_ID",
|
|
8
|
+
"PROTOBASE_IOS_API_KEY_ISSUER"
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* The API key in `env`, checked before anything is installed: none of its variables or all of them, the key a file,
|
|
12
|
+
* made absolute against the project folder since xcodebuild runs in the native project. Without it Xcode signs with
|
|
13
|
+
* the account in its settings.
|
|
14
|
+
*/
|
|
15
|
+
const iosApiKey = (env, projectDir) => {
|
|
16
|
+
const missing = apiKeyVariables.filter((key) => !env[key]);
|
|
17
|
+
if (missing.length === apiKeyVariables.length) return;
|
|
18
|
+
if (missing.length > 0) throw new Error(`Signing with an App Store Connect API key takes all of ${apiKeyVariables.join(", ")}: set ${missing.join(", ")}`);
|
|
19
|
+
const file = path.resolve(projectDir, env.PROTOBASE_IOS_API_KEY_PATH);
|
|
20
|
+
if (!statSync(file, { throwIfNoEntry: false })?.isFile()) throw new Error(`PROTOBASE_IOS_API_KEY_PATH names ${file}, which is not a file`);
|
|
21
|
+
return {
|
|
22
|
+
path: file,
|
|
23
|
+
id: env.PROTOBASE_IOS_API_KEY_ID,
|
|
24
|
+
issuer: env.PROTOBASE_IOS_API_KEY_ISSUER
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const archive = "App.xcarchive";
|
|
28
|
+
const exportOptions = "archive.plist";
|
|
29
|
+
const provisioningArgs = (key) => ["-allowProvisioningUpdates", ...key ? [
|
|
30
|
+
"-authenticationKeyPath",
|
|
31
|
+
key.path,
|
|
32
|
+
"-authenticationKeyID",
|
|
33
|
+
key.id,
|
|
34
|
+
"-authenticationKeyIssuerID",
|
|
35
|
+
key.issuer
|
|
36
|
+
] : []];
|
|
37
|
+
/** xcodebuild's arguments to archive the release for `team`. */
|
|
38
|
+
const iosArchiveArgs = (team, key) => [
|
|
39
|
+
"-project",
|
|
40
|
+
"App.xcodeproj",
|
|
41
|
+
"-scheme",
|
|
42
|
+
"App",
|
|
43
|
+
"-configuration",
|
|
44
|
+
"Release",
|
|
45
|
+
"-destination",
|
|
46
|
+
"generic/platform=iOS",
|
|
47
|
+
"-archivePath",
|
|
48
|
+
archive,
|
|
49
|
+
"archive",
|
|
50
|
+
`DEVELOPMENT_TEAM=${team}`,
|
|
51
|
+
...provisioningArgs(key)
|
|
52
|
+
];
|
|
53
|
+
/** xcodebuild's arguments to export the archive as an .ipa into `output`, where `releaseFile` finds it. */
|
|
54
|
+
const iosExportArgs = (key) => [
|
|
55
|
+
"-exportArchive",
|
|
56
|
+
"-archivePath",
|
|
57
|
+
archive,
|
|
58
|
+
"-exportOptionsPlist",
|
|
59
|
+
exportOptions,
|
|
60
|
+
"-exportPath",
|
|
61
|
+
"output",
|
|
62
|
+
...provisioningArgs(key)
|
|
63
|
+
];
|
|
64
|
+
/** The export options of an App Store build `team` signs automatically; `team` is an id `--team` checked. */
|
|
65
|
+
const iosExportOptions = (team) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
66
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
67
|
+
<plist version="1.0">
|
|
68
|
+
<dict>
|
|
69
|
+
<key>method</key>
|
|
70
|
+
<string>app-store-connect</string>
|
|
71
|
+
<key>signingStyle</key>
|
|
72
|
+
<string>automatic</string>
|
|
73
|
+
<key>teamID</key>
|
|
74
|
+
<string>${team}</string>
|
|
75
|
+
</dict>
|
|
76
|
+
</plist>
|
|
77
|
+
`;
|
|
78
|
+
/**
|
|
79
|
+
* Archives and exports the iOS project in `ws` with automatic signing. xcodebuild runs here rather than through
|
|
80
|
+
* `cap build ios`, which lets Xcode create nothing while archiving and cannot pass an API key to either step.
|
|
81
|
+
*/
|
|
82
|
+
const buildIosAutomatic = async ({ ws, env, team, key }, run) => {
|
|
83
|
+
const cwd = path.join(ws, "ios", "App");
|
|
84
|
+
await run("xcodebuild", iosArchiveArgs(team, key), {
|
|
85
|
+
cwd,
|
|
86
|
+
env
|
|
87
|
+
});
|
|
88
|
+
await writeFile(path.join(cwd, exportOptions), iosExportOptions(team));
|
|
89
|
+
await run("xcodebuild", iosExportArgs(key), {
|
|
90
|
+
cwd,
|
|
91
|
+
env
|
|
92
|
+
});
|
|
93
|
+
await rm(path.join(cwd, archive), {
|
|
94
|
+
recursive: true,
|
|
95
|
+
force: true
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
export { buildIosAutomatic, iosApiKey, iosArchiveArgs, iosExportArgs, iosExportOptions };
|
package/dist/mobile/run.js
CHANGED
|
@@ -71,7 +71,8 @@ const prepare = async ({ projectDir, platform, api, extensions, live, release },
|
|
|
71
71
|
plugins,
|
|
72
72
|
allowances,
|
|
73
73
|
apiOrigin: url.origin,
|
|
74
|
-
platform
|
|
74
|
+
platform,
|
|
75
|
+
...release && { release }
|
|
75
76
|
};
|
|
76
77
|
await writeCapacitorConfig(ws, capacitorConfig({
|
|
77
78
|
...config,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.73.
|
|
3
|
+
"version": "0.73.1",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@better-auth/core": "^1.7.12",
|
|
45
45
|
"@hono/node-server": "^2.1.3",
|
|
46
|
-
"@protobase/config": "0.73.
|
|
47
|
-
"@protobase/layout": "0.73.
|
|
48
|
-
"@protobase/query": "0.73.
|
|
49
|
-
"@protobase/schema": "0.73.
|
|
50
|
-
"@protobase/server": "0.73.
|
|
51
|
-
"@protobase/ui": "0.73.
|
|
52
|
-
"@protobase/workflows": "0.73.
|
|
46
|
+
"@protobase/config": "0.73.1",
|
|
47
|
+
"@protobase/layout": "0.73.1",
|
|
48
|
+
"@protobase/query": "0.73.1",
|
|
49
|
+
"@protobase/schema": "0.73.1",
|
|
50
|
+
"@protobase/server": "0.73.1",
|
|
51
|
+
"@protobase/ui": "0.73.1",
|
|
52
|
+
"@protobase/workflows": "0.73.1",
|
|
53
53
|
"@react-email/render": "^2.1.0",
|
|
54
54
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
55
55
|
"@sentry/vite-plugin": "^11.10.0",
|
|
@@ -118,7 +118,9 @@ Use it as the upload key with Play App Signing. Set these variables in your shel
|
|
|
118
118
|
|
|
119
119
|
Only Gradle gets them, for the length of the build: npm, its install scripts and Capacitor never do. The build needs the Android SDK and JDK 21.
|
|
120
120
|
|
|
121
|
-
**iOS**, on a Mac with Xcode: `--team` is your Apple Developer team id, shown in your account's membership details. Xcode signs with the certificates and profiles of the account you added in its settings. For manual signing, pass `--certificate <name>` and `--profile <name>` too. <gap>Manual signing cannot use a certificate or profile name that holds `<`, `>` or `&` because writing the escaped export options is not implemented.</gap>
|
|
121
|
+
**iOS**, on a Mac with Xcode: `--team` is your Apple Developer team id, shown in your account's membership details. Xcode signs with the certificates and profiles of the account you added in its settings. For manual signing, pass `--certificate <name>` and `--profile <name>` too. <gap>Manual signing cannot use a certificate or profile name that holds `<`, `>` or `&` because writing the escaped export options is not implemented.</gap> Xcode creates the certificates and profiles a new app lacks.
|
|
122
|
+
|
|
123
|
+
To sign without an account in Xcode, as on a build machine, make an App Store Connect API key with the Admin role and set `PROTOBASE_IOS_API_KEY_PATH` to its `.p8` file, absolute or relative to the project folder, `PROTOBASE_IOS_API_KEY_ID` to its key id and `PROTOBASE_IOS_API_KEY_ISSUER` to its issuer id. Set all three or none; manual signing ignores them. The bundle id must exist on the team first.
|
|
122
124
|
|
|
123
125
|
## 7. Live updates
|
|
124
126
|
|
|
@@ -143,7 +143,7 @@ The queue is an `AuditQueue`, `{ publish(event): Promise<void> }`, the default e
|
|
|
143
143
|
|
|
144
144
|
## Filters
|
|
145
145
|
|
|
146
|
-
`filter` and `order_by` are AIP-160 and AIP-132 text, checked against the model (filterable and sortable fields only). Functions: `in(field, v, ...)`, `search("text")` and bare words (every word, ignoring case, anywhere inside one of the resource's search fields, so `port` finds "Portfolio"; `%`, `_` and `\` are literal; a [`digitsEnd`](data-config.md#resource) field matches only the end of its digits), `similar(field, "text")`, `regex(field, "pattern")`, `isNull(field)`, `now()` with offsets such as `now() - 7d`. A filter is capped at 10,000 characters in a query string and 200,000 in a `:search` body.
|
|
146
|
+
`filter` and `order_by` are AIP-160 and AIP-132 text, checked against the model (filterable and sortable fields only). Functions: `in(field, v, ...)`, `search("text")` and bare words (every word, ignoring case, anywhere inside one of the resource's search fields, so `port` finds "Portfolio"; `%`, `_` and `\` are literal; a [`digitsEnd`](data-config.md#resource) field matches only the end of its digits), `similar(field, "text")`, `regex(field, "pattern")`, `isNull(field)`, `now()` with offsets such as `now() - 7d`. A filter is capped at 10,000 characters in a query string and 200,000 in a `:search` body. A proxy in front of the API can refuse a long URL sooner with a `431`, counting the headers too: Protobase Cloud refused about 8,000 characters. So send a long filter to `:search`, as `client.list` does when its query string passes 4,000 characters.
|
|
147
147
|
|
|
148
148
|
## Writes
|
|
149
149
|
|
|
@@ -650,7 +650,7 @@ Builds the project in the current directory as an iOS or Android app with Capaci
|
|
|
650
650
|
- It installs `@protobase/mobile`, of the CLI's version, which pins Capacitor and its plugins, with the section's `plugins` into `.protobase/mobile/` with npm, when that changed, and on every run for a package from a local folder, given as `file:` or a path such as `../my-plugin`, which npm copies. When such a folder's dependencies are not the versions installed, it removes the workspace's `node_modules` and `package-lock.json` so npm resolves them again, and stops when a pin is still not met, as when two packages pin one dependency at different versions. It builds the app's files as [`build`](#build) does and keeps only `public/`, its `index.html` naming the API origin; writes `capacitor.config.json`, compiling in only the section's plugins; and generates the platform's native project again, replacing the last one.
|
|
651
651
|
- `--api` is the server the app calls, the app's `url` by default: an http or https origin, plain http only to `localhost`, `127.0.0.1` and, on iOS, `[::1]`, or on Android `10.0.2.2`. An Android app calling `localhost` gets the port forwarded.
|
|
652
652
|
- `run` runs the app on `--target`, or asks for one. On iOS, `--team` writes the Apple team id into the generated project and builds it once with xcodebuild allowed to create the development profile and register the device, `--target` or the one connected device, before Capacitor runs it there. With `--live` it opens `protobase dev` on `localhost:--port` instead of its own files. `open` opens the native project in Xcode or Android Studio.
|
|
653
|
-
- `build` makes a signed release, an `.aab` (Android) or `.ipa` (iOS), and copies it to `--out`, which ignores itself in git when the command creates it, as `<appId>-<version>-<build number>`. Android signs through Gradle with the keystore, alias and passwords in `PROTOBASE_ANDROID_KEYSTORE`, `PROTOBASE_ANDROID_KEY_ALIAS`, `PROTOBASE_ANDROID_KEYSTORE_PASSWORD` and `PROTOBASE_ANDROID_KEY_PASSWORD`; iOS
|
|
653
|
+
- `build` makes a signed release, an `.aab` (Android) or `.ipa` (iOS), and copies it to `--out`, which ignores itself in git when the command creates it, as `<appId>-<version>-<build number>`. Android signs through Gradle with the keystore, alias and passwords in `PROTOBASE_ANDROID_KEYSTORE`, `PROTOBASE_ANDROID_KEY_ALIAS`, `PROTOBASE_ANDROID_KEYSTORE_PASSWORD` and `PROTOBASE_ANDROID_KEY_PASSWORD`; iOS archives and exports with xcodebuild for `--team`, letting Xcode create certificates and profiles with the account in its settings, or with the App Store Connect API key in `PROTOBASE_IOS_API_KEY_PATH`, `PROTOBASE_IOS_API_KEY_ID` and `PROTOBASE_IOS_API_KEY_ISSUER`; `--certificate` with `--profile` signs manually through Capacitor's build.
|
|
654
654
|
- A release calls an https server that is not this machine, and `--app-version` (one to three numbers) and `--build-number` (1 to 2100000000) are required.
|
|
655
655
|
- npm and Capacitor get the CLI's environment without any variable named in the current directory's `.env*` files, nor the four `PROTOBASE_ANDROID_*` signing variables, which only Gradle gets, and with `CI=true`, which keeps Capacitor's telemetry off. The project's config, its extensions' configs and the web build run inside the CLI with no `PROTOBASE_ANDROID_*` variable left in its environment, even one the project's `.env` sets.
|
|
656
656
|
- It prints the workspace, the API origin and the native fingerprint, which the app also sends in its user agent as `ProtobaseNative/<fingerprint>`: a hash of the native packages `@protobase/mobile` pins, at their versions, its own native contract, and the section's `plugins`, at the versions they give. A [mobile release](#mobile-releases) runs only on a binary of its own fingerprint. A Protobase release that pins the same native packages and keeps the same native contract keeps it; raise a plugin's version when the app's files start using a newer native feature of it, which changes it.
|
|
@@ -17,7 +17,7 @@ const { record, etag } = await api.get(orders, id)
|
|
|
17
17
|
await api.update(orders, id, { status: 'shipped' }, etag) // 412 rejects with PreconditionFailedError
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Pass a resource builder to get its record type ([`InferRecord`](https://docs.protobase.net/api/schema/type-aliases/inferrecord/)), or a name for loose records. Filters are AIP-160 text or [`where`](https://docs.protobase.net/api/schema/variables/where/) results (printed with [`printFilter`](https://docs.protobase.net/api/schema/functions/printfilter/))
|
|
20
|
+
Pass a resource builder to get its record type ([`InferRecord`](https://docs.protobase.net/api/schema/type-aliases/inferrecord/)), or a name for loose records. Filters are AIP-160 text or [`where`](https://docs.protobase.net/api/schema/variables/where/) results (printed with [`printFilter`](https://docs.protobase.net/api/schema/functions/printfilter/)), and a list whose query string passes 4,000 characters goes by `:search`. Errors are `ApiError` (`status`, `slug`, `errors`, `filterErrors` with spans, `blockedBy` for a delete other records point at), which `isApiError(error)` tells apart. Also: `search`, `create`, `remove`, `facets`, `series`, `histogram`, `map(resource, { latitude, longitude, zoom, filter?, orderBy?, fields? })` (the records in a map's view, or buckets of them, see [`:map`](api.md#maps)), `seek`, `reveal(resource, key, field)` (a [sensitive field](data-config.md#sensitive-fields)'s value), `generate(resource, key, action)` (runs an [AI action](ai.md#ai-actions) and resolves to the record as written), `meta(etag?, { signal? })`, `invoke(name, { body?, method?, path?, query? })` (one of the app's [API functions](../guides/api-functions.md#calling-a-function); its answer as parsed JSON, text, or a `Blob` for any other type), `history(resource, key, { pageSize?, pageToken? })` (a page of a record's [changes](history.md), newest first), `comments(resource, key, { pageSize?, pageToken? })`, `comment(resource, key, { body, mentions? })` and `mentionable(resource, key, query)` (a record's [comments](comments.md#the-endpoints), adding one, and the people it may mention), `form(name)`, `formChallenge(name)` and `submitForm(name, values, proof)` (a [public form](forms.md#routes)), `upload(resource, field, file, { onProgress?, signal? })` (a file for a [file field](files.md#uploading); its `value` goes in the next write, and `onProgress` uses XMLHttpRequest, since fetch reports no upload progress), `live({ onChanges, onResync, onRetry? })` (hears which records of the [live](live.md) resources change until the function it returns is called, connecting again by itself; every caller of one client shares one stream), `channel(resource, key)` (a record's [channel](channels.md#from-a-page), with its presence and broadcasts, every channel of the client on one socket).
|
|
21
21
|
|
|
22
22
|
With an absolute `baseUrl`, such as another app's `https://desk.example.com/api/v1`, the `url` of each [file](files.md) in a record the client answers is absolute too, on that API's origin, where the API answers a path on its own origin. `routedClient(own, sourceOf)` is one client over several APIs: each call about a resource goes to the client `sourceOf(resource)` gives, else to `own`, as the admin app does for [remote resources](remote-resources.md), and `sourceMeta(meta, source)` keeps the part of another API's `/meta` an app draws.
|
|
23
23
|
|