@proveanything/smartlinks 2.1.4 → 2.1.6
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/docs/API_SUMMARY.md +1 -1
- package/dist/docs/bookings.md +3 -1
- package/docs/API_SUMMARY.md +1 -1
- package/docs/bookings.md +3 -1
- package/package.json +1 -1
- package/scripts/publish.mjs +100 -28
package/dist/docs/API_SUMMARY.md
CHANGED
package/dist/docs/bookings.md
CHANGED
|
@@ -361,7 +361,9 @@ wording.
|
|
|
361
361
|
`hold_expired`, `slot.full` / `slot.place_freed`; `when.status` narrows to the booking's status at the
|
|
362
362
|
time (`"pending"` for "request received").
|
|
363
363
|
- `to`: `contact` — the booker, by their preferred channel (`channel` to force one; a template needs an
|
|
364
|
-
`sms` part to go by text); `team` — the collection's team addresses;
|
|
364
|
+
`sms` part to go by text); `team` — the collection's team addresses; an **address** (`"hello@bloom.com"`,
|
|
365
|
+
or a list of up to 10) — a particular inbox, written out: never built from what someone sent
|
|
366
|
+
(`{{ … }}` is refused), at most 200 a day per template; `resource` — the booked
|
|
365
367
|
person or room: a resource that is a contact, or a record with `data.email` / `data.contactId`.
|
|
366
368
|
A **public** resource record (a staff profile the booking page lists) keeps its address on a
|
|
367
369
|
**private child record** instead (`visibility: 'admin'`, `parentType: 'record'`, `parentId` = the
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/bookings.md
CHANGED
|
@@ -361,7 +361,9 @@ wording.
|
|
|
361
361
|
`hold_expired`, `slot.full` / `slot.place_freed`; `when.status` narrows to the booking's status at the
|
|
362
362
|
time (`"pending"` for "request received").
|
|
363
363
|
- `to`: `contact` — the booker, by their preferred channel (`channel` to force one; a template needs an
|
|
364
|
-
`sms` part to go by text); `team` — the collection's team addresses;
|
|
364
|
+
`sms` part to go by text); `team` — the collection's team addresses; an **address** (`"hello@bloom.com"`,
|
|
365
|
+
or a list of up to 10) — a particular inbox, written out: never built from what someone sent
|
|
366
|
+
(`{{ … }}` is refused), at most 200 a day per template; `resource` — the booked
|
|
365
367
|
person or room: a resource that is a contact, or a record with `data.email` / `data.contactId`.
|
|
366
368
|
A **public** resource record (a staff profile the booking page lists) keeps its address on a
|
|
367
369
|
**private child record** instead (`visibility: 'admin'`, `parentType: 'record'`, `parentId` = the
|
package/package.json
CHANGED
package/scripts/publish.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { readFileSync, readdirSync, statSync, watch } from 'node:fs'
|
|
20
20
|
import { join, relative, sep } from 'node:path'
|
|
21
|
+
import { createHash } from 'node:crypto'
|
|
21
22
|
|
|
22
23
|
const args = process.argv.slice(2)
|
|
23
24
|
const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : def }
|
|
@@ -66,6 +67,69 @@ const BATCH_FILES = 150
|
|
|
66
67
|
// through an API request, so no single file is ever too large to publish (e.g. a readable dev build).
|
|
67
68
|
const DIRECT_BYTES = 4 * 1024 * 1024
|
|
68
69
|
|
|
70
|
+
// A large bundle sends only what changed, and straight to storage (POST /publish/plan): the API compares
|
|
71
|
+
// each file's MD5 with what the dev folder already holds and hands out an upload session for the rest, which
|
|
72
|
+
// go up in parallel, never through the API. A site with 1,500 files, mostly images, sent all of them through
|
|
73
|
+
// the API on every publish (about two minutes); a text fix now sends the few files the build changed.
|
|
74
|
+
const PLAN_FILES = 500
|
|
75
|
+
const PARALLEL_UPLOADS = 8
|
|
76
|
+
const md5Of = (buf) => createHash('md5').update(buf).digest('base64')
|
|
77
|
+
|
|
78
|
+
async function pool(items, size, fn) {
|
|
79
|
+
let i = 0
|
|
80
|
+
await Promise.all(Array.from({ length: Math.min(size, items.length) }, async () => {
|
|
81
|
+
while (i < items.length) { const k = i++; await fn(items[k]) }
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// PUT one file to its upload session; a failed upload asks for a fresh session and tries again (3 tries).
|
|
86
|
+
async function putFile(path, buf, url, qs) {
|
|
87
|
+
let at = url
|
|
88
|
+
for (let attempt = 1; ; attempt++) {
|
|
89
|
+
const put = await fetch(at, { method: 'PUT', body: buf }).catch((e) => ({ ok: false, status: 0, text: async () => e.message }))
|
|
90
|
+
if (put.ok) return true
|
|
91
|
+
const why = `${put.status}: ${(await put.text().catch(() => '')).slice(0, 200)}`
|
|
92
|
+
if (attempt >= 3) { console.error(`❌ upload of ${path} failed (${why})`); return false }
|
|
93
|
+
const { res, b } = await post(`/publish/uploads${qs}`, { files: [{ path, size: buf.length }] })
|
|
94
|
+
if (!res.ok || !b.ok || !b.uploads || !b.uploads[0]) { console.error(`❌ upload of ${path} failed (${why}); no new upload session`); return false }
|
|
95
|
+
at = b.uploads[0].url
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// → the paths now in the dev folder (unchanged + uploaded), null when this API has no plans yet (an older
|
|
100
|
+
// API: the caller sends batches, as before), or false when an upload failed.
|
|
101
|
+
async function publishByPlan(rest, qs) {
|
|
102
|
+
// Empty files can't take an upload session: they go in a batch (they cost nothing).
|
|
103
|
+
const planned = rest.filter(([, buf]) => buf.length > 0)
|
|
104
|
+
const have = []
|
|
105
|
+
const uploads = []
|
|
106
|
+
for (let i = 0; i < planned.length; i += PLAN_FILES) {
|
|
107
|
+
const part = planned.slice(i, i + PLAN_FILES)
|
|
108
|
+
const { res, b } = await post(`/publish/plan${qs}`, { files: part.map(([p, buf]) => ({ path: p, size: buf.length, md5: md5Of(buf) })) })
|
|
109
|
+
if (res.status === 404 && i === 0) return null
|
|
110
|
+
if (!res.ok || !b.ok) { reportFailure(res, b); return false }
|
|
111
|
+
have.push(...(b.have || []))
|
|
112
|
+
uploads.push(...(b.uploads || []))
|
|
113
|
+
}
|
|
114
|
+
const bytes = new Map(rest)
|
|
115
|
+
const sendBytes = uploads.reduce((n, u) => n + (bytes.get(u.path) || []).length, 0)
|
|
116
|
+
console.log(`📦 ${APP_ID}: ${planned.length} files — ${have.length} unchanged, ${uploads.length} to send (${(sendBytes / 1048576).toFixed(1)} MB, straight to storage)`)
|
|
117
|
+
let sent = 0
|
|
118
|
+
let failed = false
|
|
119
|
+
await pool(uploads, PARALLEL_UPLOADS, async (u) => {
|
|
120
|
+
if (failed) return
|
|
121
|
+
if (!(await putFile(u.path, bytes.get(u.path), u.url, qs))) { failed = true; return }
|
|
122
|
+
if (++sent % 100 === 0 || sent === uploads.length) console.log(` ↑ ${sent}/${uploads.length}`)
|
|
123
|
+
})
|
|
124
|
+
if (failed) return false
|
|
125
|
+
const empty = rest.filter(([, buf]) => buf.length === 0)
|
|
126
|
+
if (empty.length) {
|
|
127
|
+
const { res, b } = await post(`/publish/files${qs}`, { files: toB64(empty) })
|
|
128
|
+
if (!res.ok || !b.ok) { reportFailure(res, b); return false }
|
|
129
|
+
}
|
|
130
|
+
return rest.map(([p]) => p)
|
|
131
|
+
}
|
|
132
|
+
|
|
69
133
|
// Big files → storage directly. Returns the paths uploaded, or null when the server has no direct
|
|
70
134
|
// uploads yet (an older API: the caller falls back to batches).
|
|
71
135
|
async function uploadDirect(entries, qs) {
|
|
@@ -122,37 +186,45 @@ async function publishOnce() {
|
|
|
122
186
|
if (totalBytes <= BATCH_BYTES && entries.length <= BATCH_FILES) {
|
|
123
187
|
finalBody = { files: toB64(entries) } // small bundle: one request, as before
|
|
124
188
|
} else {
|
|
125
|
-
// Large bundle:
|
|
126
|
-
// the server only registers the release once every
|
|
189
|
+
// Large bundle: everything except the manifest first (only what changed, straight to storage; or in
|
|
190
|
+
// batches on an older API), then the manifest last — the server only registers the release once every
|
|
191
|
+
// referenced file is confirmed uploaded.
|
|
127
192
|
const rest = entries.filter(([p]) => p !== 'app.manifest.json').sort((a, b) => b[1].length - a[1].length)
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
193
|
+
const byPlan = await publishByPlan(rest, qs)
|
|
194
|
+
if (byPlan === false) { if (WATCH) return false; process.exit(1) }
|
|
195
|
+
if (byPlan) {
|
|
196
|
+
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: byPlan }
|
|
197
|
+
} else {
|
|
198
|
+
console.warn(' ⚠︎ this SmartLinks API has no publish plans yet — sending every file in batches')
|
|
199
|
+
const big = rest.filter(([, buf]) => buf.length >= DIRECT_BYTES)
|
|
200
|
+
let direct = []
|
|
201
|
+
if (big.length) {
|
|
202
|
+
const done = await uploadDirect(big, qs)
|
|
203
|
+
if (done === false) { if (WATCH) return false; process.exit(1) }
|
|
204
|
+
if (done) direct = done
|
|
205
|
+
else console.warn(' ⚠︎ this SmartLinks API has no direct uploads yet — sending big files in batches')
|
|
206
|
+
}
|
|
207
|
+
const viaBatches = rest.filter(([p]) => !direct.includes(p))
|
|
208
|
+
const batches = []
|
|
209
|
+
let cur = [], curBytes = 0
|
|
210
|
+
for (const e of viaBatches) {
|
|
211
|
+
if (cur.length && (curBytes + e[1].length > BATCH_BYTES || cur.length >= BATCH_FILES)) { batches.push(cur); cur = []; curBytes = 0 }
|
|
212
|
+
if (e[1].length > BATCH_BYTES) console.warn(` ⚠︎ ${e[0]} is ${(e[1].length / 1048576).toFixed(1)} MB — larger than one request allows; the server will likely reject it`)
|
|
213
|
+
cur.push(e); curBytes += e[1].length
|
|
214
|
+
}
|
|
215
|
+
if (cur.length) batches.push(cur)
|
|
216
|
+
console.log(`📦 ${APP_ID}: ${(totalBytes / 1048576).toFixed(1)} MB in ${entries.length} files — ${direct.length ? `${direct.length} sent directly, ` : ''}${batches.length} batch(es)`)
|
|
217
|
+
for (let i = 0; i < batches.length; i++) {
|
|
218
|
+
const { res, b } = await post(`/publish/files${qs}`, { files: toB64(batches[i]) })
|
|
219
|
+
if (!res.ok || !b.ok) {
|
|
220
|
+
reportFailure(res, b)
|
|
221
|
+
if (WATCH) return false
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
224
|
+
console.log(` ↑ batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
|
|
152
225
|
}
|
|
153
|
-
|
|
226
|
+
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: rest.map(([p]) => p) }
|
|
154
227
|
}
|
|
155
|
-
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: rest.map(([p]) => p) }
|
|
156
228
|
}
|
|
157
229
|
|
|
158
230
|
const { res, b } = await post(`/publish${qs}`, finalBody)
|