@embeddables/forms 0.0.4 → 0.2.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.
@@ -31,112 +31,96 @@ var ValidatorError = class extends FormsError {
31
31
  this.name = "ValidatorError";
32
32
  }
33
33
  };
34
- const DEVELOPMENT_BASE_URL = void 0;
35
- /**
36
- * Returns null when persistence cannot be configured (missing publishable key
37
- * or fetch). The SDK keeps the no-op default in that case.
38
- */
39
- function resolvePersistenceConfig(config) {
40
- const core = config.core;
41
- const publishableKey = config.publishableKey ?? core.getPublishableKey();
42
- if (!publishableKey || !(0, _embeddables_core.isValidPublishableKey)({ value: publishableKey })) return null;
43
- const projectId = core.getProjectId();
44
- const appUserId = core.getAppUserId();
45
- if (!projectId || !appUserId) return null;
46
- const fetchImpl = config.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
47
- if (!fetchImpl) return null;
48
- return {
49
- core,
50
- projectId,
51
- appUserId,
52
- publishableKey,
53
- baseUrl: config.baseUrl ?? DEVELOPMENT_BASE_URL ?? "https://backend-worker.heysavvy.workers.dev",
54
- fetch: fetchImpl,
55
- timeoutMs: config.timeoutMs ?? 1e4
56
- };
57
- }
58
34
  //#endregion
59
- //#region src/storage/persistence.ts
35
+ //#region src/core/validation.ts
36
+ /** Runtime counterpart to `FieldType`. Exhaustive by construction. */
37
+ const FIELD_TYPE_PREDICATES = {
38
+ text: (value) => typeof value === "string",
39
+ email: (value) => typeof value === "string",
40
+ number: (value) => typeof value === "number",
41
+ boolean: (value) => typeof value === "boolean",
42
+ select: (value) => typeof value === "string",
43
+ multiselect: (value) => Array.isArray(value),
44
+ json: () => true
45
+ };
46
+ const EMAIL_PATTERN = /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?(?:\.[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?)*$/;
60
47
  /**
61
- * The default: does nothing, never throws, and recovers nothing. With this in
62
- * place a form is pure local state, exactly as before the port existed.
48
+ * Every message a single field's value earns. Empty means valid. Not generic:
49
+ * the per-field types live at the instance boundary, and the cast down to
50
+ * `JsonValue` happens once, in `initForm`.
63
51
  */
64
- function createNoopPersistence() {
65
- return {
66
- savePartial: () => void 0,
67
- saveFields: () => void 0,
68
- saveSubmission: () => void 0,
69
- recoverRegistryFields: () => ({})
70
- };
71
- }
72
- //#endregion
73
- //#region src/storage/persistence-client.ts
74
- const PUBLISHABLE_KEY_HEADER = "x-publishable-key";
75
- const hcWithType = (...args) => (0, hono_client.hc)(...args);
76
- function withTimeout(fetchImpl, timeoutMs) {
77
- return async (input, init) => {
78
- const controller = new AbortController();
79
- const timer = setTimeout(() => controller.abort(), timeoutMs);
80
- const path = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
81
- try {
82
- return await fetchImpl(input, {
83
- ...init,
84
- signal: controller.signal
85
- });
86
- } catch (error) {
87
- if (error instanceof Error && error.name === "AbortError") throw new Error(`Request to ${path} timed out after ${timeoutMs}ms`, { cause: error });
88
- throw new Error(`Request to ${path} failed to reach the API`, { cause: error });
89
- } finally {
90
- clearTimeout(timer);
52
+ function validateValue({ field, value, values, pattern, validator }) {
53
+ const rules = field.validations;
54
+ const messages = [];
55
+ const isAbsent = value === void 0 || value === null;
56
+ const isBlank = isAbsent || value === "" || Array.isArray(value) && value.length === 0;
57
+ if (rules?.required === true && isBlank) messages.push(`${field.label} is required`);
58
+ if (isAbsent) return messages;
59
+ if (!FIELD_TYPE_PREDICATES[field.type](value)) messages.push(`${field.label} expects ${/^[aeiou]/.test(field.type) ? "an" : "a"} ${field.type} value`);
60
+ if (typeof value === "string") {
61
+ if (field.type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
62
+ if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must be at least ${rules.minLength} characters`);
63
+ if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must be at most ${rules.maxLength} characters`);
64
+ if (pattern && !pattern.test(value)) messages.push(`${field.label} is not in the expected format`);
65
+ }
66
+ if (Array.isArray(value)) {
67
+ if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must have at least ${rules.minLength} items`);
68
+ if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must have at most ${rules.maxLength} items`);
69
+ }
70
+ if (typeof value === "number") {
71
+ if (rules?.min !== void 0 && value < rules.min) messages.push(`${field.label} must be at least ${rules.min}`);
72
+ if (rules?.max !== void 0 && value > rules.max) messages.push(`${field.label} must be at most ${rules.max}`);
73
+ }
74
+ if (field.type === "select" || field.type === "multiselect") {
75
+ const options = field.options;
76
+ if (options) {
77
+ const allowed = new Set(options.map((option) => option.value));
78
+ if (field.type === "select" && typeof value === "string" && !allowed.has(value)) messages.push(`${field.label} must be one of the allowed options`);
79
+ if (field.type === "multiselect" && Array.isArray(value)) {
80
+ if (value.some((entry) => typeof entry !== "string" || !allowed.has(entry))) messages.push(`${field.label} has values that are not allowed options`);
81
+ }
91
82
  }
92
- };
93
- }
94
- async function rpcCall(fn) {
95
- const res = await fn();
96
- if (!res.ok) {
97
- const problem = await res.json().catch(() => null);
98
- const message = problem?.detail ?? problem?.title ?? `forms persistence failed: ${res.status}`;
99
- throw new Error(message);
100
83
  }
101
- return res.json();
102
- }
103
- function createApiPersistence(config) {
104
- const root = config.baseUrl.replace(/\/+$/, "");
105
- const rpc = hcWithType(`${root}/forms`, {
106
- fetch: withTimeout(config.fetch, config.timeoutMs),
107
- headers: { [PUBLISHABLE_KEY_HEADER]: config.publishableKey }
84
+ if (rules?.oneOf) {
85
+ const encoded = canonicalize(value);
86
+ if (!rules.oneOf.some((option) => canonicalize(option) === encoded)) messages.push(`${field.label} must be one of the allowed options`);
87
+ }
88
+ if (!validator || messages.length > 0) return messages;
89
+ return normalizeValidatorResult({
90
+ result: validator({
91
+ value,
92
+ values
93
+ }),
94
+ field
108
95
  });
109
- return {
110
- savePartial({ formKey, values }) {
111
- return rpcCall(() => rpc.v1.public.sessions.$post({ json: {
112
- project_id: config.projectId,
113
- app_user_id: config.appUserId,
114
- form_id: formKey,
115
- data: values
116
- } })).then(() => void 0);
117
- },
118
- saveFields: () => void 0,
119
- saveSubmission({ formKey, values }) {
120
- return rpcCall(() => rpc.v1.public.submissions.$post({ json: {
121
- project_id: config.projectId,
122
- app_user_id: config.appUserId,
123
- form_id: formKey,
124
- values
125
- } })).then(() => void 0);
126
- },
127
- recoverRegistryFields: () => ({})
128
- };
129
96
  }
130
- function resolveDefaultPersistence(config) {
131
- const resolved = resolvePersistenceConfig(config);
132
- if (!resolved) return createNoopPersistence();
133
- return createApiPersistence(resolved);
97
+ function normalizeValidatorResult({ result, field }) {
98
+ if (isThenable(result)) throw new ValidatorError(`Validator for "${field.key}" returned a promise; custom validators must be synchronous`);
99
+ if (result === null || result === void 0) return [];
100
+ if (typeof result === "string") return [result];
101
+ if (Array.isArray(result)) return result.filter((entry) => typeof entry === "string");
102
+ throw new ValidatorError(`Validator for "${field.key}" returned ${typeof result}; expected a string, an array of strings, or null`);
103
+ }
104
+ function isThenable(value) {
105
+ return typeof value?.then === "function";
106
+ }
107
+ /**
108
+ * JSON encoding with object keys sorted at every depth, so two values compare
109
+ * by content rather than by insertion order.
110
+ */
111
+ function canonicalize(value) {
112
+ const walk = (input) => {
113
+ if (Array.isArray(input)) return input.map(walk);
114
+ if (input !== null && typeof input === "object") return Object.fromEntries(Object.entries(input).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, walk(item)]));
115
+ return input;
116
+ };
117
+ return JSON.stringify(walk(value));
134
118
  }
135
119
  //#endregion
136
120
  //#region src/storage/storage.ts
137
121
  /** Every form on the origin shares this one entry, indexed by form ID. */
138
122
  const FORM_DATA_KEY = "EMBEDDABLES-FORM-DATA";
139
- const FIELD_TYPES$1 = [
123
+ const FIELD_TYPES$2 = [
140
124
  "text",
141
125
  "email",
142
126
  "number",
@@ -151,7 +135,7 @@ function readDocumentFromStorage({ storage }) {
151
135
  const raw = storage.getItem(FORM_DATA_KEY);
152
136
  if (raw === null) return {};
153
137
  const parsed = JSON.parse(raw);
154
- if (!isObjectLike$1(parsed)) return {};
138
+ if (!isObjectLike$2(parsed)) return {};
155
139
  return parsed;
156
140
  } catch {
157
141
  return {};
@@ -178,7 +162,7 @@ function resolveStorage({ storage }) {
178
162
  candidate.getItem(FORM_DATA_KEY);
179
163
  return candidate;
180
164
  } catch {
181
- return createMemoryStorage();
165
+ return createMemoryFormsStorage();
182
166
  }
183
167
  }
184
168
  /**
@@ -186,7 +170,7 @@ function resolveStorage({ storage }) {
186
170
  * whose real storage started throwing mid-session.
187
171
  */
188
172
  function degradeToMemory({ storage }) {
189
- const shim = createMemoryStorage();
173
+ const shim = createMemoryFormsStorage();
190
174
  const snapshot = { ...LIVE_DOCUMENTS.get(storage) ?? readDocumentFromStorage({ storage }) };
191
175
  shim.setItem(FORM_DATA_KEY, JSON.stringify(snapshot));
192
176
  LIVE_DOCUMENTS.set(shim, snapshot);
@@ -194,18 +178,18 @@ function degradeToMemory({ storage }) {
194
178
  }
195
179
  function readFields({ storage, formKey, fieldDefinitions }) {
196
180
  const bag = loadDocument({ storage })[formKey];
197
- if (!isObjectLike$1(bag)) return {};
181
+ if (!isObjectLike$2(bag)) return {};
198
182
  const declared = new Set(fieldDefinitions.map((field) => field.key));
199
183
  const values = {};
200
184
  for (const [key, entry] of Object.entries(bag)) {
201
185
  if (!declared.has(key)) continue;
202
- if (isStoredField(entry)) values[key] = entry.value;
186
+ if (isStoredField$1(entry)) values[key] = entry.value;
203
187
  }
204
188
  return values;
205
189
  }
206
190
  function writeFields({ storage, formKey, fields, fieldDefinitions }) {
207
191
  const current = loadDocument({ storage });
208
- const currentForm = isObjectLike$1(current[formKey]) ? current[formKey] : {};
192
+ const currentForm = isObjectLike$2(current[formKey]) ? current[formKey] : {};
209
193
  const declaredKeys = new Set(fieldDefinitions.map((field) => field.key));
210
194
  const nextForm = {};
211
195
  for (const [key, entry] of Object.entries(currentForm)) if (!declaredKeys.has(key)) nextForm[key] = entry;
@@ -250,7 +234,8 @@ function isSerializable({ value }) {
250
234
  return false;
251
235
  }
252
236
  }
253
- function createMemoryStorage() {
237
+ /** In-memory storage for server-side form init and tests that inject a store. */
238
+ function createMemoryFormsStorage() {
254
239
  const entries = /* @__PURE__ */ new Map();
255
240
  return {
256
241
  getItem: (key) => entries.get(key) ?? null,
@@ -262,6 +247,47 @@ function createMemoryStorage() {
262
247
  }
263
248
  };
264
249
  }
250
+ function isObjectLike$2(value) {
251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
252
+ }
253
+ function isStoredField$1(value) {
254
+ if (!isObjectLike$2(value) || !Object.hasOwn(value, "value")) return false;
255
+ if (!isSerializable({ value: value["value"] })) return false;
256
+ if (typeof value["type"] !== "string" || !FIELD_TYPES$2.includes(value["type"]) || typeof value["label"] !== "string") return false;
257
+ if (value["registryId"] !== void 0 && typeof value["registryId"] !== "string") return false;
258
+ if (value["protocolFieldId"] !== void 0 && typeof value["protocolFieldId"] !== "string") return false;
259
+ return true;
260
+ }
261
+ //#endregion
262
+ //#region src/storage/cookie-form-data.ts
263
+ const FIELD_TYPES$1 = [
264
+ "text",
265
+ "email",
266
+ "number",
267
+ "boolean",
268
+ "select",
269
+ "multiselect",
270
+ "json"
271
+ ];
272
+ function formatFormCookieDataKey({ projectId, formId }) {
273
+ return `EMBEDDABLES--${projectId}--COOKIES-FORM-DATA--${formId}`;
274
+ }
275
+ function buildCookiePayloadFromBag({ bag, fieldDefinitions }) {
276
+ const payload = {};
277
+ for (const field of fieldDefinitions) {
278
+ if (field.includeInCookies !== true) continue;
279
+ const value = bag[field.key];
280
+ if (value === void 0) continue;
281
+ payload[field.key] = {
282
+ value,
283
+ type: field.type,
284
+ label: field.label,
285
+ ...field.registryId === void 0 ? {} : { registryId: field.registryId },
286
+ ...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
287
+ };
288
+ }
289
+ return payload;
290
+ }
265
291
  function isObjectLike$1(value) {
266
292
  return typeof value === "object" && value !== null && !Array.isArray(value);
267
293
  }
@@ -273,6 +299,204 @@ function isStoredField(value) {
273
299
  if (value["protocolFieldId"] !== void 0 && typeof value["protocolFieldId"] !== "string") return false;
274
300
  return true;
275
301
  }
302
+ function entryMatchesFieldDefinition(entry, field) {
303
+ if (entry.type !== field.type || entry.label !== field.label) return false;
304
+ if ((entry.registryId ?? void 0) !== (field.registryId ?? void 0)) return false;
305
+ if ((entry.protocolFieldId ?? void 0) !== (field.protocolFieldId ?? void 0)) return false;
306
+ return FIELD_TYPE_PREDICATES[field.type](entry.value);
307
+ }
308
+ function serializeBrowserCookie(key, value) {
309
+ const locationRef = globalThis.location;
310
+ const parts = [
311
+ `${key}=${encodeURIComponent(value)}`,
312
+ "Path=/",
313
+ "SameSite=Lax"
314
+ ];
315
+ if (locationRef?.protocol === "https:") parts.push("Secure");
316
+ return parts.join("; ");
317
+ }
318
+ function writeBrowserCookie(key, value) {
319
+ const documentRef = globalThis.document;
320
+ if (!documentRef) return;
321
+ try {
322
+ documentRef.cookie = serializeBrowserCookie(key, value);
323
+ } catch {}
324
+ }
325
+ function clearBrowserCookie(key) {
326
+ const documentRef = globalThis.document;
327
+ if (!documentRef) return;
328
+ try {
329
+ const locationRef = globalThis.location;
330
+ const parts = [
331
+ `${key}=`,
332
+ "Path=/",
333
+ "Max-Age=0",
334
+ "SameSite=Lax"
335
+ ];
336
+ if (locationRef?.protocol === "https:") parts.push("Secure");
337
+ documentRef.cookie = parts.join("; ");
338
+ } catch {}
339
+ }
340
+ function writeFormCookieData({ projectId, formId, bag, fieldDefinitions }) {
341
+ try {
342
+ const payload = buildCookiePayloadFromBag({
343
+ bag,
344
+ fieldDefinitions
345
+ });
346
+ writeBrowserCookie(formatFormCookieDataKey({
347
+ projectId,
348
+ formId
349
+ }), JSON.stringify(payload));
350
+ } catch {}
351
+ }
352
+ function clearFormCookieData({ projectId, formId }) {
353
+ try {
354
+ clearBrowserCookie(formatFormCookieDataKey({
355
+ projectId,
356
+ formId
357
+ }));
358
+ } catch {}
359
+ }
360
+ function readFormCookieStoredForm({ projectId, formId, getCookie, fieldDefinitions }) {
361
+ try {
362
+ const raw = getCookie(formatFormCookieDataKey({
363
+ projectId,
364
+ formId
365
+ }));
366
+ if (raw === null || raw === "") return {};
367
+ const parsed = JSON.parse(decodeURIComponent(raw));
368
+ if (!isObjectLike$1(parsed)) return {};
369
+ const optedInFields = new Map(fieldDefinitions.filter((field) => field.includeInCookies === true).map((field) => [field.key, field]));
370
+ const storedForm = {};
371
+ for (const [key, entry] of Object.entries(parsed)) {
372
+ const field = optedInFields.get(key);
373
+ if (!field || !isStoredField(entry)) continue;
374
+ if (!entryMatchesFieldDefinition(entry, field)) continue;
375
+ storedForm[key] = entry;
376
+ }
377
+ return storedForm;
378
+ } catch {
379
+ return {};
380
+ }
381
+ }
382
+ function seedServerFormsStorageFromCookies({ storage, projectId, formIds, getFormSchema, getCookie }) {
383
+ const document = {};
384
+ for (const formId of formIds) {
385
+ const rawSchema = getFormSchema(formId);
386
+ if (typeof rawSchema !== "object" || rawSchema === null) continue;
387
+ const storedForm = readFormCookieStoredForm({
388
+ projectId,
389
+ formId,
390
+ getCookie,
391
+ fieldDefinitions: rawSchema.fields
392
+ });
393
+ if (Object.keys(storedForm).length === 0) continue;
394
+ document[formId] = storedForm;
395
+ }
396
+ if (Object.keys(document).length === 0) return;
397
+ storage.setItem(FORM_DATA_KEY, JSON.stringify(document));
398
+ }
399
+ const DEVELOPMENT_BASE_URL = void 0;
400
+ /**
401
+ * Returns null when persistence cannot be configured (missing publishable key
402
+ * or fetch). The SDK keeps the no-op default in that case.
403
+ */
404
+ function resolvePersistenceConfig(config) {
405
+ const core = config.core;
406
+ const publishableKey = config.publishableKey ?? core.getPublishableKey();
407
+ if (!publishableKey || !(0, _embeddables_core.isValidPublishableKey)({ value: publishableKey })) return null;
408
+ const projectId = core.getProjectId();
409
+ const appUserId = core.getAppUserId();
410
+ if (!projectId || !appUserId) return null;
411
+ const fetchImpl = config.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
412
+ if (!fetchImpl) return null;
413
+ return {
414
+ core,
415
+ projectId,
416
+ appUserId,
417
+ publishableKey,
418
+ baseUrl: config.baseUrl ?? DEVELOPMENT_BASE_URL ?? "https://backend-worker.heysavvy.workers.dev",
419
+ fetch: fetchImpl,
420
+ timeoutMs: config.timeoutMs ?? 1e4
421
+ };
422
+ }
423
+ //#endregion
424
+ //#region src/storage/persistence.ts
425
+ /**
426
+ * The default: does nothing, never throws, and recovers nothing. With this in
427
+ * place a form is pure local state, exactly as before the port existed.
428
+ */
429
+ function createNoopPersistence() {
430
+ return {
431
+ savePartial: () => void 0,
432
+ saveFields: () => void 0,
433
+ saveSubmission: () => void 0,
434
+ recoverRegistryFields: () => ({})
435
+ };
436
+ }
437
+ //#endregion
438
+ //#region src/storage/persistence-client.ts
439
+ const PUBLISHABLE_KEY_HEADER = "x-publishable-key";
440
+ const hcWithType = (...args) => (0, hono_client.hc)(...args);
441
+ function withTimeout(fetchImpl, timeoutMs) {
442
+ return async (input, init) => {
443
+ const controller = new AbortController();
444
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
445
+ const path = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
446
+ try {
447
+ return await fetchImpl(input, {
448
+ ...init,
449
+ signal: controller.signal
450
+ });
451
+ } catch (error) {
452
+ if (error instanceof Error && error.name === "AbortError") throw new Error(`Request to ${path} timed out after ${timeoutMs}ms`, { cause: error });
453
+ throw new Error(`Request to ${path} failed to reach the API`, { cause: error });
454
+ } finally {
455
+ clearTimeout(timer);
456
+ }
457
+ };
458
+ }
459
+ async function rpcCall(fn) {
460
+ const res = await fn();
461
+ if (!res.ok) {
462
+ const problem = await res.json().catch(() => null);
463
+ const message = problem?.detail ?? problem?.title ?? `forms persistence failed: ${res.status}`;
464
+ throw new Error(message);
465
+ }
466
+ return res.json();
467
+ }
468
+ function createApiPersistence(config) {
469
+ const root = config.baseUrl.replace(/\/+$/, "");
470
+ const rpc = hcWithType(`${root}/forms`, {
471
+ fetch: withTimeout(config.fetch, config.timeoutMs),
472
+ headers: { [PUBLISHABLE_KEY_HEADER]: config.publishableKey }
473
+ });
474
+ return {
475
+ savePartial({ formKey, values }) {
476
+ return rpcCall(() => rpc.v1.public.sessions.$post({ json: {
477
+ project_id: config.projectId,
478
+ app_user_id: config.appUserId,
479
+ form_id: formKey,
480
+ data: values
481
+ } })).then(() => void 0);
482
+ },
483
+ saveFields: () => void 0,
484
+ saveSubmission({ formKey, values }) {
485
+ return rpcCall(() => rpc.v1.public.submissions.$post({ json: {
486
+ project_id: config.projectId,
487
+ app_user_id: config.appUserId,
488
+ form_id: formKey,
489
+ values
490
+ } })).then(() => void 0);
491
+ },
492
+ recoverRegistryFields: () => ({})
493
+ };
494
+ }
495
+ function resolveDefaultPersistence(config) {
496
+ const resolved = resolvePersistenceConfig(config);
497
+ if (!resolved) return createNoopPersistence();
498
+ return createApiPersistence(resolved);
499
+ }
276
500
  //#endregion
277
501
  //#region src/core/analytics.ts
278
502
  /** The ingest bound on a `data:updated` entry's `value`. */
@@ -288,10 +512,11 @@ function mapFieldUpdatedType(type) {
288
512
  * raw `field_value`; only the batch event caps and stringifies for ingest.
289
513
  */
290
514
  function formatFieldValue({ value }) {
515
+ if (value === void 0) return "";
291
516
  return (typeof value === "string" ? value : JSON.stringify(value)).slice(0, MAX_VALUE_LENGTH);
292
517
  }
293
518
  /** One event carrying every key in one `.set()` call. */
294
- function buildDataUpdatedEvent({ fields, patch }) {
519
+ function buildDataUpdatedEvent({ fields, patch, formId }) {
295
520
  const byKey = new Map(fields.map((field) => [field.key, field]));
296
521
  return {
297
522
  event_name: "data:updated",
@@ -304,11 +529,12 @@ function buildDataUpdatedEvent({ fields, patch }) {
304
529
  }),
305
530
  label: (field?.label ?? key).slice(0, MAX_LABEL_LENGTH)
306
531
  }];
307
- }))
532
+ })),
533
+ form_id: formId
308
534
  };
309
535
  }
310
536
  /** One `field:updated` per changed key, emitted alongside `data:updated`. */
311
- function buildFieldUpdatedEvents({ fields, patch }) {
537
+ function buildFieldUpdatedEvents({ fields, patch, formId }) {
312
538
  const byKey = new Map(fields.map((field) => [field.key, field]));
313
539
  return Object.entries(patch).map(([key, value]) => {
314
540
  const field = byKey.get(key);
@@ -316,14 +542,33 @@ function buildFieldUpdatedEvents({ fields, patch }) {
316
542
  event_name: "field:updated",
317
543
  field_key: key,
318
544
  field_type: mapFieldUpdatedType(field?.type ?? "text"),
319
- field_value: value
545
+ form_id: formId
320
546
  };
547
+ if (value !== void 0) event.field_value = value;
321
548
  if (field?.registryId !== void 0) event.registry_field_id = field.registryId;
322
549
  if (field?.protocolFieldId !== void 0) event.protocol_field_id = field.protocolFieldId;
323
550
  return event;
324
551
  });
325
552
  }
326
553
  //#endregion
554
+ //#region src/core/options.ts
555
+ /**
556
+ * The array a `multiselect` patch should actually store, given what it added
557
+ * relative to the previous value. An added exclusive option wins outright; an
558
+ * added regular option evicts every exclusive value; a patch that only removed
559
+ * values passes through.
560
+ */
561
+ function normalizeExclusiveSelection({ next, previous, options }) {
562
+ const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.value));
563
+ if (exclusive.size === 0) return next;
564
+ const previousValues = new Set(previous);
565
+ const added = next.filter((entry) => !previousValues.has(entry));
566
+ const addedExclusive = added.filter((entry) => exclusive.has(entry));
567
+ if (addedExclusive.length > 0) return [addedExclusive[addedExclusive.length - 1]];
568
+ if (added.length > 0) return next.filter((entry) => !exclusive.has(entry));
569
+ return next;
570
+ }
571
+ //#endregion
327
572
  //#region src/core/resolve.ts
328
573
  const FIELD_TYPES = [
329
574
  "text",
@@ -341,7 +586,6 @@ const VALIDATION_RULES = [
341
586
  "min",
342
587
  "max",
343
588
  "pattern",
344
- "patternFlags",
345
589
  "oneOf",
346
590
  "custom"
347
591
  ];
@@ -351,11 +595,18 @@ const NUMERIC_RULES = [
351
595
  "min",
352
596
  "max"
353
597
  ];
598
+ const OPTION_KEYS = [
599
+ "value",
600
+ "label",
601
+ "exclusive"
602
+ ];
603
+ /** The two field types whose choices are declared through a field-level `options`. */
604
+ const OPTION_FIELD_TYPES = ["select", "multiselect"];
354
605
  /** The ingest `z.string().max(128)` bound on a `data:updated` key. */
355
606
  const MAX_FIELD_KEY_LENGTH = 128;
356
607
  /** The ingest `z.string().max(128)` bound on a `form:submitted` key. */
357
608
  const MAX_FORM_KEY_LENGTH = 128;
358
- const VALID_PATTERN_FLAGS = /^[dgimsuvy]*$/;
609
+ const INLINE_PATTERN_FLAGS = /^\(\?([dgimsuvy]+)\)/;
359
610
  const RESOLVED_SCHEMAS = /* @__PURE__ */ new WeakMap();
360
611
  function resolveForm({ schema }) {
361
612
  const memoized = RESOLVED_SCHEMAS.get(schema);
@@ -411,23 +662,51 @@ function validateField({ field, path, seenKeys, patterns }) {
411
662
  if (registryId !== void 0 && typeof registryId !== "string") throw new SchemaError(`${path}.registryId: must be a string`);
412
663
  const protocolFieldId = field["protocolFieldId"];
413
664
  if (protocolFieldId !== void 0 && typeof protocolFieldId !== "string") throw new SchemaError(`${path}.protocolFieldId: must be a string`);
665
+ const includeInCookies = field["includeInCookies"];
666
+ if (includeInCookies !== void 0 && typeof includeInCookies !== "boolean") throw new SchemaError(`${path}.includeInCookies: must be a boolean`);
667
+ validateOptions({
668
+ field,
669
+ type,
670
+ path
671
+ });
414
672
  const validations = field["validations"];
415
673
  if (validations === void 0) return;
416
674
  validateValidations({
417
675
  validations,
676
+ type,
418
677
  path: `${path}.validations`
419
678
  });
420
679
  if (!isObjectLike(validations)) return;
421
680
  const pattern = validations["pattern"];
422
681
  if (typeof pattern !== "string") return;
423
- const flags = validations["patternFlags"];
424
682
  patterns.set(key, compilePattern({
425
683
  pattern,
426
- flags: typeof flags === "string" ? flags : "",
427
684
  path: `${path}.validations.pattern`
428
685
  }));
429
686
  }
430
- function validateValidations({ validations, path }) {
687
+ function validateOptions({ field, type, path }) {
688
+ const options = field["options"];
689
+ if (options === void 0) return;
690
+ if (!OPTION_FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.options: only a select or multiselect field may declare options`);
691
+ if (!(Array.isArray(options) && options.length > 0)) throw new SchemaError(`${path}.options: must be a non-empty array`);
692
+ const entries = options;
693
+ const seenValues = /* @__PURE__ */ new Set();
694
+ entries.forEach((option, index) => {
695
+ const at = `${path}.options[${index}]`;
696
+ if (!isObjectLike(option)) throw new SchemaError(`${at}: must be an object`);
697
+ for (const optionKey of Object.keys(option)) if (!OPTION_KEYS.includes(optionKey)) throw new SchemaError(`${at}.${optionKey}: unknown option key; expected one of ${OPTION_KEYS.join(", ")}`);
698
+ const value = option["value"];
699
+ if (typeof value !== "string" || value.trim() === "") throw new SchemaError(`${at}.value: must be a non-empty string`);
700
+ const label = option["label"];
701
+ if (label !== void 0 && typeof label !== "string") throw new SchemaError(`${at}.label: must be a string`);
702
+ const exclusive = option["exclusive"];
703
+ if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new SchemaError(`${at}.exclusive: must be a boolean`);
704
+ if (exclusive === true && type === "select") throw new SchemaError(`${at}.exclusive: only a multiselect field may declare an exclusive option`);
705
+ if (seenValues.has(value)) throw new SchemaError(`${at}.value: duplicate option value "${value}" on this field`);
706
+ seenValues.add(value);
707
+ });
708
+ }
709
+ function validateValidations({ validations, type, path }) {
431
710
  if (!isObjectLike(validations)) throw new SchemaError(`${path}: must be an object`);
432
711
  for (const rule of Object.keys(validations)) if (!VALIDATION_RULES.includes(rule)) throw new SchemaError(`${path}.${rule}: unknown validation rule; expected one of ${VALIDATION_RULES.join(", ")}`);
433
712
  const required = validations["required"];
@@ -443,17 +722,19 @@ function validateValidations({ validations, path }) {
443
722
  const max = validations["max"];
444
723
  if (typeof min === "number" && typeof max === "number" && max < min) throw new SchemaError(`${path}.max: must be greater than or equal to min`);
445
724
  const oneOf = validations["oneOf"];
725
+ if (oneOf !== void 0 && OPTION_FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.oneOf: not allowed on a ${type} field; declare choices through the field's options instead`);
446
726
  if (oneOf !== void 0 && !(Array.isArray(oneOf) && oneOf.length > 0)) throw new SchemaError(`${path}.oneOf: must be a non-empty array`);
447
727
  const pattern = validations["pattern"];
448
728
  if (pattern !== void 0 && typeof pattern !== "string") throw new SchemaError(`${path}.pattern: must be a string`);
449
- const patternFlags = validations["patternFlags"];
450
- if (patternFlags !== void 0 && !(typeof patternFlags === "string" && VALID_PATTERN_FLAGS.test(patternFlags))) throw new SchemaError(`${path}.patternFlags: must contain only the characters dgimsuvy`);
451
729
  const custom = validations["custom"];
452
730
  if (custom !== void 0 && typeof custom !== "function") throw new SchemaError(`${path}.custom: must be a function (received ${typeof custom})`);
453
731
  }
454
- function compilePattern({ pattern, flags, path }) {
732
+ function compilePattern({ pattern, path }) {
733
+ const prefix = INLINE_PATTERN_FLAGS.exec(pattern);
734
+ const source = prefix ? pattern.slice(prefix[0].length) : pattern;
735
+ const flags = prefix?.[1] ?? "";
455
736
  try {
456
- return new RegExp(pattern, flags.replace(/[gy]/g, ""));
737
+ return new RegExp(source, flags.replace(/[gy]/g, ""));
457
738
  } catch (error) {
458
739
  throw new SchemaError(`${path}: invalid pattern — ${errorMessage(error)}`, { cause: error });
459
740
  }
@@ -576,81 +857,6 @@ function errorMessage(error) {
576
857
  return error instanceof Error ? error.message : String(error);
577
858
  }
578
859
  //#endregion
579
- //#region src/core/validation.ts
580
- /** Runtime counterpart to `FieldType`. Exhaustive by construction. */
581
- const FIELD_TYPE_PREDICATES = {
582
- text: (value) => typeof value === "string",
583
- email: (value) => typeof value === "string",
584
- number: (value) => typeof value === "number",
585
- boolean: (value) => typeof value === "boolean",
586
- select: (value) => typeof value === "string",
587
- multiselect: (value) => Array.isArray(value),
588
- json: () => true
589
- };
590
- const EMAIL_PATTERN = /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?(?:\.[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?)*$/;
591
- /**
592
- * Every message a single field's value earns. Empty means valid. Not generic:
593
- * the per-field types live at the instance boundary, and the cast down to
594
- * `JsonValue` happens once, in `initForm`.
595
- */
596
- function validateValue({ field, value, values, pattern, validator }) {
597
- const rules = field.validations;
598
- const messages = [];
599
- const isAbsent = value === void 0 || value === null;
600
- const isBlank = isAbsent || value === "" || Array.isArray(value) && value.length === 0;
601
- if (rules?.required === true && isBlank) messages.push(`${field.label} is required`);
602
- if (isAbsent) return messages;
603
- if (!FIELD_TYPE_PREDICATES[field.type](value)) messages.push(`${field.label} expects ${/^[aeiou]/.test(field.type) ? "an" : "a"} ${field.type} value`);
604
- if (typeof value === "string") {
605
- if (field.type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
606
- if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must be at least ${rules.minLength} characters`);
607
- if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must be at most ${rules.maxLength} characters`);
608
- if (pattern && !pattern.test(value)) messages.push(`${field.label} is not in the expected format`);
609
- }
610
- if (Array.isArray(value)) {
611
- if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must have at least ${rules.minLength} items`);
612
- if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must have at most ${rules.maxLength} items`);
613
- }
614
- if (typeof value === "number") {
615
- if (rules?.min !== void 0 && value < rules.min) messages.push(`${field.label} must be at least ${rules.min}`);
616
- if (rules?.max !== void 0 && value > rules.max) messages.push(`${field.label} must be at most ${rules.max}`);
617
- }
618
- if (rules?.oneOf) {
619
- const encoded = canonicalize(value);
620
- if (!rules.oneOf.some((option) => canonicalize(option) === encoded)) messages.push(`${field.label} must be one of the allowed options`);
621
- }
622
- if (!validator || messages.length > 0) return messages;
623
- return normalizeValidatorResult({
624
- result: validator({
625
- value,
626
- values
627
- }),
628
- field
629
- });
630
- }
631
- function normalizeValidatorResult({ result, field }) {
632
- if (isThenable(result)) throw new ValidatorError(`Validator for "${field.key}" returned a promise; custom validators must be synchronous`);
633
- if (result === null || result === void 0) return [];
634
- if (typeof result === "string") return [result];
635
- if (Array.isArray(result)) return result.filter((entry) => typeof entry === "string");
636
- throw new ValidatorError(`Validator for "${field.key}" returned ${typeof result}; expected a string, an array of strings, or null`);
637
- }
638
- function isThenable(value) {
639
- return typeof value?.then === "function";
640
- }
641
- /**
642
- * JSON encoding with object keys sorted at every depth, so two values compare
643
- * by content rather than by insertion order.
644
- */
645
- function canonicalize(value) {
646
- const walk = (input) => {
647
- if (Array.isArray(input)) return input.map(walk);
648
- if (input !== null && typeof input === "object") return Object.fromEntries(Object.entries(input).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, walk(item)]));
649
- return input;
650
- };
651
- return JSON.stringify(walk(value));
652
- }
653
- //#endregion
654
860
  //#region src/core/form.ts
655
861
  const REQUIRED_CORE_METHODS = [
656
862
  "getAppUserId",
@@ -685,41 +891,110 @@ function mergeCustomValidations({ schema, customValidations }) {
685
891
  };
686
892
  }
687
893
  /**
688
- * Validates the core instance and the schema map once, then returns a client
689
- * whose `getForm` builds each form by id.
894
+ * Validates the core instance and registered form schemas once, then returns a
895
+ * client whose `getForm` builds each form by id.
690
896
  */
691
897
  function initForms(options) {
692
898
  return createFormsClient(options);
693
899
  }
694
- function createFormsClient({ core, schemas, analyticsInstance, baseUrl, storage, persistence }) {
695
- if (!hasRequiredCoreMethods(core)) throw new FormsError("initForms requires an initialized Embeddables core instance.");
696
- if (typeof schemas !== "object" || schemas === null) throw new SchemaError("initForms requires a schemas map keyed by form id.");
697
- for (const [formId, schema] of Object.entries(schemas)) {
698
- if (typeof schema !== "object" || schema === null) throw new SchemaError(`schemas["${formId}"]: must be a form schema object`);
699
- if (typeof schema.id === "string" && schema.id !== "" && schema.id !== formId) throw new SchemaError(`schemas["${formId}"]: schema.id is "${String(schema.id)}" — it must equal its map key`);
900
+ const REQUIRED_CORE_FORM_METHODS = ["getFormIds", "getFormSchema"];
901
+ function hasCoreFormSchemaMethods(value) {
902
+ return REQUIRED_CORE_FORM_METHODS.every((method) => typeof value[method] === "function");
903
+ }
904
+ function toFormSchema(value, formId) {
905
+ if (typeof value !== "object" || value === null) throw new SchemaError(`Registered form "${formId}": schema must be an object.`);
906
+ const schema = value;
907
+ if (typeof schema.id !== "string" || schema.id === "") throw new SchemaError(`Registered form "${formId}": schema.id must be a non-empty string.`);
908
+ if (schema.id !== formId) throw new SchemaError(`Registered form "${formId}": schema id must match map key.`);
909
+ return schema;
910
+ }
911
+ function indexOverrideEntries({ entries, label, valueKey }) {
912
+ if (entries === void 0) return /* @__PURE__ */ new Map();
913
+ const byFormId = /* @__PURE__ */ new Map();
914
+ for (let index = 0; index < entries.length; index++) {
915
+ const entry = entries[index];
916
+ if (typeof entry !== "object" || entry === null) throw new SchemaError(`${label}[${index}]: must be an object.`);
917
+ const { formId } = entry;
918
+ if (typeof formId !== "string" || formId === "") throw new SchemaError(`${label}[${index}].formId: must be a non-empty string.`);
919
+ const value = entry[valueKey];
920
+ if (value === void 0) throw new SchemaError(`${label}[${index}].${String(valueKey)}: is required.`);
921
+ if (byFormId.has(formId)) throw new SchemaError(`${label}: duplicate form id "${formId}".`);
922
+ byFormId.set(formId, value);
700
923
  }
924
+ return byFormId;
925
+ }
926
+ function resolveInitConfig({ core, customValidations, serverFormData }) {
927
+ if (!hasCoreFormSchemaMethods(core)) throw new FormsError("initForms requires a Core instance with registered form schemas.");
928
+ const formIds = core.getFormIds();
929
+ if (formIds.length === 0) throw new FormsError("initForms requires at least one form schema registered on Core.");
930
+ const customByFormId = indexOverrideEntries({
931
+ entries: customValidations,
932
+ label: "customValidations",
933
+ valueKey: "customValidations"
934
+ });
935
+ const serverDataByFormId = indexOverrideEntries({
936
+ entries: serverFormData,
937
+ label: "serverFormData",
938
+ valueKey: "serverFormData"
939
+ });
940
+ const registeredIds = new Set(formIds);
941
+ for (const formId of customByFormId.keys()) if (!registeredIds.has(formId)) throw new SchemaError(`customValidations: unknown form id "${formId}".`);
942
+ for (const formId of serverDataByFormId.keys()) if (!registeredIds.has(formId)) throw new SchemaError(`serverFormData: unknown form id "${formId}".`);
943
+ const registry = /* @__PURE__ */ new Map();
944
+ for (const formId of formIds) {
945
+ const schema = toFormSchema(core.getFormSchema(formId), formId);
946
+ const entryCustomValidations = customByFormId.get(formId);
947
+ const entryServerFormData = serverDataByFormId.get(formId);
948
+ mergeCustomValidations({
949
+ schema,
950
+ customValidations: entryCustomValidations
951
+ });
952
+ registry.set(formId, {
953
+ schema,
954
+ customValidations: entryCustomValidations,
955
+ serverFormData: entryServerFormData
956
+ });
957
+ }
958
+ return registry;
959
+ }
960
+ function createFormsClient({ core, customValidations, serverFormData, analyticsInstance, baseUrl, storage, persistence }) {
961
+ if (!hasRequiredCoreMethods(core)) throw new FormsError("initForms requires an initialized Embeddables core instance.");
962
+ const registry = resolveInitConfig({
963
+ core,
964
+ customValidations,
965
+ serverFormData
966
+ });
701
967
  const resolvedPersistence = persistence ?? resolveDefaultPersistence({
702
968
  core,
703
969
  baseUrl
704
970
  });
705
971
  const instances = /* @__PURE__ */ new Map();
706
- return { getForm: ({ formId, customValidations }) => {
972
+ return { getForm({ formId }) {
707
973
  const existing = instances.get(formId);
708
974
  if (existing !== void 0) return existing;
709
- const schema = schemas[formId];
710
- if (schema === void 0) throw new FormsError(`Unknown form id "${formId}".`);
975
+ const entry = registry.get(formId);
976
+ if (entry === void 0) throw new FormsError(`Unknown form id "${formId}".`);
711
977
  const instance = createFormInstance({
712
978
  analyticsInstance,
713
979
  storage,
714
980
  persistence: resolvedPersistence,
715
- schema,
716
- customValidations
981
+ schema: entry.schema,
982
+ customValidations: entry.customValidations,
983
+ serverFormData: entry.serverFormData,
984
+ projectId: core.getProjectId()
717
985
  });
718
986
  instances.set(formId, instance);
719
987
  return instance;
720
988
  } };
721
989
  }
722
- function createFormInstance({ analyticsInstance, storage, persistence, schema, customValidations }) {
990
+ function mergeInitialBag({ fromStorage, serverFormData }) {
991
+ if (serverFormData === void 0) return { ...fromStorage };
992
+ return {
993
+ ...fromStorage,
994
+ ...serverFormData
995
+ };
996
+ }
997
+ function createFormInstance({ analyticsInstance, storage, persistence, schema, customValidations, serverFormData, projectId }) {
723
998
  const resolved = resolveForm({ schema: mergeCustomValidations({
724
999
  schema,
725
1000
  customValidations
@@ -739,11 +1014,14 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
739
1014
  };
740
1015
  const state = {
741
1016
  storage: resolvedStorage,
742
- bag: { ...readFields({
743
- storage: resolvedStorage,
744
- formKey: resolved.formKey,
745
- fieldDefinitions: resolved.fields
746
- }) },
1017
+ bag: mergeInitialBag({
1018
+ fromStorage: readFields({
1019
+ storage: resolvedStorage,
1020
+ formKey: resolved.formKey,
1021
+ fieldDefinitions: resolved.fields
1022
+ }),
1023
+ serverFormData
1024
+ }),
747
1025
  errors: /* @__PURE__ */ new Map()
748
1026
  };
749
1027
  const firePersistence = (run) => {
@@ -814,12 +1092,24 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
814
1092
  }
815
1093
  };
816
1094
  const set = (patch) => {
817
- const changes = patch;
818
- const entries = Object.entries(changes);
819
- if (entries.length === 0) return Promise.resolve({
1095
+ const changes = { ...patch };
1096
+ if (Object.keys(changes).length === 0) return Promise.resolve({
820
1097
  ok: true,
821
1098
  errors: noErrors()
822
1099
  });
1100
+ const previousBag = readBag();
1101
+ for (const [key, value] of Object.entries(changes)) {
1102
+ const field = declared.get(key);
1103
+ if (!field || field.type !== "multiselect" || !field.options) continue;
1104
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) continue;
1105
+ const stored = previousBag[key];
1106
+ changes[key] = [...normalizeExclusiveSelection({
1107
+ next: value,
1108
+ previous: Array.isArray(stored) ? stored : [],
1109
+ options: field.options
1110
+ })];
1111
+ }
1112
+ const entries = Object.entries(changes);
823
1113
  const errors = /* @__PURE__ */ new Map();
824
1114
  for (const [key, value] of entries) {
825
1115
  const field = declared.get(key);
@@ -827,12 +1117,14 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
827
1117
  errors.set(key, [`Unknown field: ${key}`]);
828
1118
  continue;
829
1119
  }
1120
+ if (value === void 0) continue;
830
1121
  if (!isSerializable({ value })) errors.set(key, [`${field.label} value is not JSON-serializable`]);
831
1122
  }
832
1123
  const candidate = {
833
- ...readBag(),
1124
+ ...previousBag,
834
1125
  ...changes
835
1126
  };
1127
+ for (const [key, value] of entries) if (value === void 0) delete candidate[key];
836
1128
  const snapshot = narrow(candidate);
837
1129
  for (const [key, value] of entries) {
838
1130
  const field = declared.get(key);
@@ -866,12 +1158,18 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
866
1158
  } catch {
867
1159
  return Promise.resolve(degradeAndReport({ entries }));
868
1160
  }
1161
+ writeFormCookieData({
1162
+ projectId,
1163
+ formId: resolved.formKey,
1164
+ bag: candidate,
1165
+ fieldDefinitions: resolved.fields
1166
+ });
869
1167
  notify();
870
1168
  const persistedFields = entries.map(([key, value]) => {
871
1169
  const field = declared.get(key);
872
1170
  return {
873
1171
  key,
874
- value,
1172
+ value: value === void 0 ? null : value,
875
1173
  ...field?.registryId === void 0 ? {} : { registryId: field.registryId },
876
1174
  ...field?.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
877
1175
  };
@@ -890,10 +1188,12 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
890
1188
  });
891
1189
  return analyticsInstance.trackEvent([buildDataUpdatedEvent({
892
1190
  fields: resolved.fields,
893
- patch: changes
1191
+ patch: changes,
1192
+ formId: schema.id
894
1193
  }), ...buildFieldUpdatedEvents({
895
1194
  fields: resolved.fields,
896
- patch: changes
1195
+ patch: changes,
1196
+ formId: schema.id
897
1197
  })]).then(() => ({
898
1198
  ok: true,
899
1199
  errors: noErrors()
@@ -1058,6 +1358,10 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
1058
1358
  });
1059
1359
  state.bag = {};
1060
1360
  state.errors.clear();
1361
+ clearFormCookieData({
1362
+ projectId,
1363
+ formId: resolved.formKey
1364
+ });
1061
1365
  notify();
1062
1366
  };
1063
1367
  const subscribe = (listener) => {
@@ -1104,11 +1408,35 @@ Object.defineProperty(exports, "ValidatorError", {
1104
1408
  return ValidatorError;
1105
1409
  }
1106
1410
  });
1411
+ Object.defineProperty(exports, "createFormsClient", {
1412
+ enumerable: true,
1413
+ get: function() {
1414
+ return createFormsClient;
1415
+ }
1416
+ });
1417
+ Object.defineProperty(exports, "createMemoryFormsStorage", {
1418
+ enumerable: true,
1419
+ get: function() {
1420
+ return createMemoryFormsStorage;
1421
+ }
1422
+ });
1423
+ Object.defineProperty(exports, "createNoopPersistence", {
1424
+ enumerable: true,
1425
+ get: function() {
1426
+ return createNoopPersistence;
1427
+ }
1428
+ });
1107
1429
  Object.defineProperty(exports, "initForms", {
1108
1430
  enumerable: true,
1109
1431
  get: function() {
1110
1432
  return initForms;
1111
1433
  }
1112
1434
  });
1435
+ Object.defineProperty(exports, "seedServerFormsStorageFromCookies", {
1436
+ enumerable: true,
1437
+ get: function() {
1438
+ return seedServerFormsStorageFromCookies;
1439
+ }
1440
+ });
1113
1441
 
1114
- //# sourceMappingURL=form-DzfCc5X2.cjs.map
1442
+ //# sourceMappingURL=form-BOHdxeuO.cjs.map