@tearleads/client-sdk 0.1.71 → 0.1.72

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.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * On-device timing marks for the DevTools Performance panel. Nothing here
3
+ * leaves the device: entries reach only a locally recorded trace.
4
+ */
5
+ const USER_TIMING_TRACK_GROUP = "Tearleads";
6
+ /** Monotonic milliseconds on the same clock as recorded entries. */
7
+ export function readUserTimingClock() {
8
+ return typeof globalThis.performance?.now === "function"
9
+ ? globalThis.performance.now()
10
+ : Date.now();
11
+ }
12
+ /**
13
+ * Records one measure for a running trace, then clears it from the timeline
14
+ * buffer at once: DevTools captures the measure when it is created, while an
15
+ * uncleared buffer would grow for the whole session.
16
+ */
17
+ export function recordUserTimingEntry(entry) {
18
+ try {
19
+ const performance = globalThis.performance;
20
+ if (typeof performance?.measure !== "function") {
21
+ return;
22
+ }
23
+ performance.measure(entry.name, {
24
+ detail: {
25
+ devtools: {
26
+ color: entry.failed ? "error" : "primary",
27
+ dataType: "track-entry",
28
+ properties: entry.properties ?? [],
29
+ track: entry.track,
30
+ trackGroup: USER_TIMING_TRACK_GROUP,
31
+ },
32
+ },
33
+ end: entry.endMs,
34
+ start: entry.startMs,
35
+ });
36
+ performance.clearMeasures(entry.name);
37
+ }
38
+ catch {
39
+ // Diagnostics must never change a sync or SQL outcome.
40
+ }
41
+ }
42
+ //# sourceMappingURL=userTiming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userTiming.js","sourceRoot":"","sources":["userTiming.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAE5C,oEAAoE;AACpE,MAAM,UAAU,mBAAmB;IACjC,OAAO,OAAO,UAAU,CAAC,WAAW,EAAE,GAAG,KAAK,UAAU;QACtD,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE;QAC9B,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAsB;IAC1D,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC;QAC3C,IAAI,OAAO,WAAW,EAAE,OAAO,KAAK,UAAU,EAAE,CAAC;YAC/C,OAAO;QACT,CAAC;QACD,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE;YAC9B,MAAM,EAAE;gBACN,QAAQ,EAAE;oBACR,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;oBACzC,QAAQ,EAAE,aAAa;oBACvB,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE;oBAClC,KAAK,EAAE,KAAK,CAAC,KAAK;oBAClB,UAAU,EAAE,uBAAuB;iBACpC;aACF;YACD,GAAG,EAAE,KAAK,CAAC,KAAK;YAChB,KAAK,EAAE,KAAK,CAAC,OAAO;SACrB,CAAC,CAAC;QACH,WAAW,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,uDAAuD;IACzD,CAAC;AACH,CAAC","sourcesContent":["/**\n * On-device timing marks for the DevTools Performance panel. Nothing here\n * leaves the device: entries reach only a locally recorded trace.\n */\n\nexport interface UserTimingEntry {\n readonly name: string;\n /** The DevTools track the entry is drawn on, under the Tearleads group. */\n readonly track: string;\n readonly startMs: number;\n readonly endMs: number;\n readonly failed?: boolean | undefined;\n readonly properties?: ReadonlyArray<readonly [string, string]> | undefined;\n}\n\nconst USER_TIMING_TRACK_GROUP = \"Tearleads\";\n\n/** Monotonic milliseconds on the same clock as recorded entries. */\nexport function readUserTimingClock(): number {\n return typeof globalThis.performance?.now === \"function\"\n ? globalThis.performance.now()\n : Date.now();\n}\n\n/**\n * Records one measure for a running trace, then clears it from the timeline\n * buffer at once: DevTools captures the measure when it is created, while an\n * uncleared buffer would grow for the whole session.\n */\nexport function recordUserTimingEntry(entry: UserTimingEntry): void {\n try {\n const performance = globalThis.performance;\n if (typeof performance?.measure !== \"function\") {\n return;\n }\n performance.measure(entry.name, {\n detail: {\n devtools: {\n color: entry.failed ? \"error\" : \"primary\",\n dataType: \"track-entry\",\n properties: entry.properties ?? [],\n track: entry.track,\n trackGroup: USER_TIMING_TRACK_GROUP,\n },\n },\n end: entry.endMs,\n start: entry.startMs,\n });\n performance.clearMeasures(entry.name);\n } catch {\n // Diagnostics must never change a sync or SQL outcome.\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { readUserTimingClock, recordUserTimingEntry, } from "../shared/userTiming.js";
1
2
  export const unavailableExecSql = async () => {
2
3
  throw new Error("Database client is unavailable.");
3
4
  };
@@ -92,27 +93,50 @@ function createSerializedSqlExec(execSql) {
92
93
  // Nested callers must keep passing the provided locked executor. Re-entering
93
94
  // with the original canonical executor will wait on the current mutation and
94
95
  // deadlock.
96
+ // Each outermost mutation is drawn on DevTools tracks: time spent queued
97
+ // behind another mutation, then time holding the lock.
95
98
  export async function runSerializedSqlMutation(execSql, operation) {
96
99
  if (isSerializedSqlExec(execSql)) {
97
100
  return operation(execSql);
98
101
  }
99
- const previous = sqlMutationQueue.get(execSql) ?? Promise.resolve();
102
+ const queuedAtMs = readUserTimingClock();
103
+ const previous = sqlMutationQueue.get(execSql);
100
104
  let releaseCurrent = () => { };
101
105
  const current = new Promise((resolve) => {
102
106
  releaseCurrent = resolve;
103
107
  });
104
- const waitForPrevious = previous.catch(() => undefined);
108
+ const waitForPrevious = (previous ?? Promise.resolve()).catch(() => undefined);
105
109
  const queuedCurrent = waitForPrevious.then(() => current);
106
110
  sqlMutationQueue.set(execSql, queuedCurrent);
107
111
  await waitForPrevious;
112
+ const acquiredAtMs = readUserTimingClock();
113
+ if (previous) {
114
+ recordUserTimingEntry({
115
+ endMs: acquiredAtMs,
116
+ name: "SQLite mutation wait",
117
+ startMs: queuedAtMs,
118
+ track: "SQLite mutation queue",
119
+ });
120
+ }
121
+ let failed = true;
108
122
  try {
109
- return await operation(createSerializedSqlExec(execSql));
123
+ const result = await operation(createSerializedSqlExec(execSql));
124
+ failed = false;
125
+ return result;
110
126
  }
111
127
  finally {
128
+ const releasedAtMs = readUserTimingClock();
112
129
  releaseCurrent();
113
130
  if (sqlMutationQueue.get(execSql) === queuedCurrent) {
114
131
  sqlMutationQueue.delete(execSql);
115
132
  }
133
+ recordUserTimingEntry({
134
+ endMs: releasedAtMs,
135
+ failed,
136
+ name: "SQLite mutation",
137
+ startMs: acquiredAtMs,
138
+ track: "SQLite mutations",
139
+ });
116
140
  }
117
141
  }
118
142
  //# sourceMappingURL=sqlExec.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlExec.js","sourceRoot":"","sources":["sqlExec.ts"],"names":[],"mappings":"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","sourcesContent":["export type SqlRowValue = string | number | null;\nexport type SqlRow = Record<string, SqlRowValue>;\nexport type SqlArrayRow = SqlRowValue[];\nexport type SqlBind = Record<string, SqlRowValue> | ReadonlyArray<SqlRowValue>;\nexport type SqlRowMode = \"object\" | \"array\";\n\nexport interface ExecSql {\n (\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: \"object\" },\n ): Promise<SqlRow[]>;\n (\n sql: string,\n bind: SqlBind | undefined,\n options: { rowMode: \"array\" },\n ): Promise<SqlArrayRow[]>;\n (\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ): Promise<Array<SqlRow | SqlArrayRow>>;\n}\n\nexport interface ExecSqlClientLike {\n exec(options: {\n sql: string;\n bind?: SqlBind;\n rowMode?: SqlRowMode;\n }): Promise<{ rows: Array<SqlRow | SqlArrayRow> }>;\n}\n\nexport const unavailableExecSql: ExecSql = async () => {\n throw new Error(\"Database client is unavailable.\");\n};\n\n// Tracks the active serialized mutation chain for each canonical SQL executor\n// so callers sharing one connection cannot interleave transactional statements.\nconst sqlMutationQueue = new WeakMap<ExecSql, Promise<void>>();\n\n// Marks executors that already run inside the shared mutation lock so nested\n// callers can reuse the current lock instead of queueing a second time.\nconst serializedSqlExecs = new WeakSet<ExecSql>();\n\n// Reuses one ExecSql adapter per client so all wrappers around the same\n// connection share the same serialized mutation queue.\nconst clientExecSqls = new WeakMap<ExecSqlClientLike, ExecSql>();\n\n// Maps each locked executor back to the canonical executor whose lock it runs\n// under, so per-connection caches key on one stable identity no matter which\n// wrapper a caller holds.\nconst serializedSqlExecCanonicals = new WeakMap<ExecSql, ExecSql>();\n\nexport function resolveCanonicalExecSql(execSql: ExecSql): ExecSql {\n return serializedSqlExecCanonicals.get(execSql) ?? execSql;\n}\n\n// Completed once-per-connection keys (schema ensures).\n// Only COMPLETED runs are recorded — never in-flight promises — so two lock\n// contexts racing the same key both run the (idempotent) work instead of one\n// awaiting a promise that can only settle after the lock the other context is\n// already holding.\nconst completedConnectionOnceKeys = new WeakMap<ExecSql, Set<string>>();\n\nexport function hasCompletedConnectionOnce(\n execSql: ExecSql,\n key: string,\n): boolean {\n return (\n completedConnectionOnceKeys\n .get(resolveCanonicalExecSql(execSql))\n ?.has(key) ?? false\n );\n}\n\nexport function markCompletedConnectionOnce(\n execSql: ExecSql,\n key: string,\n): void {\n const canonical = resolveCanonicalExecSql(execSql);\n const keys = completedConnectionOnceKeys.get(canonical) ?? new Set<string>();\n keys.add(key);\n completedConnectionOnceKeys.set(canonical, keys);\n}\n\n// Callers must not invoke this inside an explicit SQL transaction that can\n// roll back: completion is recorded as soon as `run` resolves, so a later\n// ROLLBACK would leave the memo asserting schema that no longer exists.\nexport async function runOncePerConnection(\n execSql: ExecSql,\n key: string,\n run: () => Promise<void>,\n): Promise<void> {\n if (hasCompletedConnectionOnce(execSql, key)) {\n return;\n }\n await run();\n markCompletedConnectionOnce(execSql, key);\n}\n\n/**\n * Forget every completed once-per-connection key for this connection. Required\n * after an operation that rebuilds the schema out from under the runtime (the\n * local backup restore drops and recreates all user tables) so the next query\n * re-runs its schema ensures.\n */\nexport function resetConnectionSchemaMemo(execSql: ExecSql): void {\n completedConnectionOnceKeys.delete(resolveCanonicalExecSql(execSql));\n}\n\nfunction defineExecSql(\n run: (\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ) => Promise<Array<SqlRow | SqlArrayRow>>,\n): ExecSql {\n function execSql(\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: \"object\" },\n ): Promise<SqlRow[]>;\n function execSql(\n sql: string,\n bind: SqlBind | undefined,\n options: { rowMode: \"array\" },\n ): Promise<SqlArrayRow[]>;\n function execSql(\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ): Promise<Array<SqlRow | SqlArrayRow>>;\n async function execSql(\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ): Promise<Array<SqlRow | SqlArrayRow>> {\n return run(sql, bind, options);\n }\n\n return execSql;\n}\n\nexport function createExecSql(client: ExecSqlClientLike): ExecSql {\n const existingExecSql = clientExecSqls.get(client);\n if (existingExecSql) {\n return existingExecSql;\n }\n\n const execSql = defineExecSql(async (sql, bind, options) => {\n const result = await client.exec({\n sql,\n ...(bind !== undefined ? { bind } : {}),\n ...(options?.rowMode ? { rowMode: options.rowMode } : {}),\n });\n return result.rows;\n });\n\n clientExecSqls.set(client, execSql);\n return execSql;\n}\n\nfunction isSerializedSqlExec(execSql: ExecSql): boolean {\n return serializedSqlExecs.has(execSql);\n}\n\nfunction createSerializedSqlExec(execSql: ExecSql): ExecSql {\n if (isSerializedSqlExec(execSql)) {\n return execSql;\n }\n\n const serializedExecSql = defineExecSql(execSql);\n serializedSqlExecs.add(serializedExecSql);\n serializedSqlExecCanonicals.set(\n serializedExecSql,\n resolveCanonicalExecSql(execSql),\n );\n return serializedExecSql;\n}\n\n// SQLite transactions are scoped to the shared connection, so we must hold a\n// connection-level lock across multi-statement mutations to prevent interleaving.\n// Nested callers must keep passing the provided locked executor. Re-entering\n// with the original canonical executor will wait on the current mutation and\n// deadlock.\nexport async function runSerializedSqlMutation<T>(\n execSql: ExecSql,\n operation: (execSql: ExecSql) => Promise<T> | T,\n): Promise<T> {\n if (isSerializedSqlExec(execSql)) {\n return operation(execSql);\n }\n\n const previous = sqlMutationQueue.get(execSql) ?? Promise.resolve();\n let releaseCurrent = () => {};\n const current = new Promise<void>((resolve) => {\n releaseCurrent = resolve;\n });\n const waitForPrevious = previous.catch(() => undefined);\n const queuedCurrent = waitForPrevious.then(() => current);\n\n sqlMutationQueue.set(execSql, queuedCurrent);\n await waitForPrevious;\n\n try {\n return await operation(createSerializedSqlExec(execSql));\n } finally {\n releaseCurrent();\n if (sqlMutationQueue.get(execSql) === queuedCurrent) {\n sqlMutationQueue.delete(execSql);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"sqlExec.js","sourceRoot":"","sources":["sqlExec.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n readUserTimingClock,\n recordUserTimingEntry,\n} from \"../shared/userTiming\";\n\nexport type SqlRowValue = string | number | null;\nexport type SqlRow = Record<string, SqlRowValue>;\nexport type SqlArrayRow = SqlRowValue[];\nexport type SqlBind = Record<string, SqlRowValue> | ReadonlyArray<SqlRowValue>;\nexport type SqlRowMode = \"object\" | \"array\";\n\nexport interface ExecSql {\n (\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: \"object\" },\n ): Promise<SqlRow[]>;\n (\n sql: string,\n bind: SqlBind | undefined,\n options: { rowMode: \"array\" },\n ): Promise<SqlArrayRow[]>;\n (\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ): Promise<Array<SqlRow | SqlArrayRow>>;\n}\n\nexport interface ExecSqlClientLike {\n exec(options: {\n sql: string;\n bind?: SqlBind;\n rowMode?: SqlRowMode;\n }): Promise<{ rows: Array<SqlRow | SqlArrayRow> }>;\n}\n\nexport const unavailableExecSql: ExecSql = async () => {\n throw new Error(\"Database client is unavailable.\");\n};\n\n// Tracks the active serialized mutation chain for each canonical SQL executor\n// so callers sharing one connection cannot interleave transactional statements.\nconst sqlMutationQueue = new WeakMap<ExecSql, Promise<void>>();\n\n// Marks executors that already run inside the shared mutation lock so nested\n// callers can reuse the current lock instead of queueing a second time.\nconst serializedSqlExecs = new WeakSet<ExecSql>();\n\n// Reuses one ExecSql adapter per client so all wrappers around the same\n// connection share the same serialized mutation queue.\nconst clientExecSqls = new WeakMap<ExecSqlClientLike, ExecSql>();\n\n// Maps each locked executor back to the canonical executor whose lock it runs\n// under, so per-connection caches key on one stable identity no matter which\n// wrapper a caller holds.\nconst serializedSqlExecCanonicals = new WeakMap<ExecSql, ExecSql>();\n\nexport function resolveCanonicalExecSql(execSql: ExecSql): ExecSql {\n return serializedSqlExecCanonicals.get(execSql) ?? execSql;\n}\n\n// Completed once-per-connection keys (schema ensures).\n// Only COMPLETED runs are recorded — never in-flight promises — so two lock\n// contexts racing the same key both run the (idempotent) work instead of one\n// awaiting a promise that can only settle after the lock the other context is\n// already holding.\nconst completedConnectionOnceKeys = new WeakMap<ExecSql, Set<string>>();\n\nexport function hasCompletedConnectionOnce(\n execSql: ExecSql,\n key: string,\n): boolean {\n return (\n completedConnectionOnceKeys\n .get(resolveCanonicalExecSql(execSql))\n ?.has(key) ?? false\n );\n}\n\nexport function markCompletedConnectionOnce(\n execSql: ExecSql,\n key: string,\n): void {\n const canonical = resolveCanonicalExecSql(execSql);\n const keys = completedConnectionOnceKeys.get(canonical) ?? new Set<string>();\n keys.add(key);\n completedConnectionOnceKeys.set(canonical, keys);\n}\n\n// Callers must not invoke this inside an explicit SQL transaction that can\n// roll back: completion is recorded as soon as `run` resolves, so a later\n// ROLLBACK would leave the memo asserting schema that no longer exists.\nexport async function runOncePerConnection(\n execSql: ExecSql,\n key: string,\n run: () => Promise<void>,\n): Promise<void> {\n if (hasCompletedConnectionOnce(execSql, key)) {\n return;\n }\n await run();\n markCompletedConnectionOnce(execSql, key);\n}\n\n/**\n * Forget every completed once-per-connection key for this connection. Required\n * after an operation that rebuilds the schema out from under the runtime (the\n * local backup restore drops and recreates all user tables) so the next query\n * re-runs its schema ensures.\n */\nexport function resetConnectionSchemaMemo(execSql: ExecSql): void {\n completedConnectionOnceKeys.delete(resolveCanonicalExecSql(execSql));\n}\n\nfunction defineExecSql(\n run: (\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ) => Promise<Array<SqlRow | SqlArrayRow>>,\n): ExecSql {\n function execSql(\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: \"object\" },\n ): Promise<SqlRow[]>;\n function execSql(\n sql: string,\n bind: SqlBind | undefined,\n options: { rowMode: \"array\" },\n ): Promise<SqlArrayRow[]>;\n function execSql(\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ): Promise<Array<SqlRow | SqlArrayRow>>;\n async function execSql(\n sql: string,\n bind?: SqlBind,\n options?: { rowMode?: SqlRowMode },\n ): Promise<Array<SqlRow | SqlArrayRow>> {\n return run(sql, bind, options);\n }\n\n return execSql;\n}\n\nexport function createExecSql(client: ExecSqlClientLike): ExecSql {\n const existingExecSql = clientExecSqls.get(client);\n if (existingExecSql) {\n return existingExecSql;\n }\n\n const execSql = defineExecSql(async (sql, bind, options) => {\n const result = await client.exec({\n sql,\n ...(bind !== undefined ? { bind } : {}),\n ...(options?.rowMode ? { rowMode: options.rowMode } : {}),\n });\n return result.rows;\n });\n\n clientExecSqls.set(client, execSql);\n return execSql;\n}\n\nfunction isSerializedSqlExec(execSql: ExecSql): boolean {\n return serializedSqlExecs.has(execSql);\n}\n\nfunction createSerializedSqlExec(execSql: ExecSql): ExecSql {\n if (isSerializedSqlExec(execSql)) {\n return execSql;\n }\n\n const serializedExecSql = defineExecSql(execSql);\n serializedSqlExecs.add(serializedExecSql);\n serializedSqlExecCanonicals.set(\n serializedExecSql,\n resolveCanonicalExecSql(execSql),\n );\n return serializedExecSql;\n}\n\n// SQLite transactions are scoped to the shared connection, so we must hold a\n// connection-level lock across multi-statement mutations to prevent interleaving.\n// Nested callers must keep passing the provided locked executor. Re-entering\n// with the original canonical executor will wait on the current mutation and\n// deadlock.\n// Each outermost mutation is drawn on DevTools tracks: time spent queued\n// behind another mutation, then time holding the lock.\nexport async function runSerializedSqlMutation<T>(\n execSql: ExecSql,\n operation: (execSql: ExecSql) => Promise<T> | T,\n): Promise<T> {\n if (isSerializedSqlExec(execSql)) {\n return operation(execSql);\n }\n\n const queuedAtMs = readUserTimingClock();\n const previous = sqlMutationQueue.get(execSql);\n let releaseCurrent = () => {};\n const current = new Promise<void>((resolve) => {\n releaseCurrent = resolve;\n });\n const waitForPrevious = (previous ?? Promise.resolve()).catch(\n () => undefined,\n );\n const queuedCurrent = waitForPrevious.then(() => current);\n\n sqlMutationQueue.set(execSql, queuedCurrent);\n await waitForPrevious;\n const acquiredAtMs = readUserTimingClock();\n if (previous) {\n recordUserTimingEntry({\n endMs: acquiredAtMs,\n name: \"SQLite mutation wait\",\n startMs: queuedAtMs,\n track: \"SQLite mutation queue\",\n });\n }\n\n let failed = true;\n try {\n const result = await operation(createSerializedSqlExec(execSql));\n failed = false;\n return result;\n } finally {\n const releasedAtMs = readUserTimingClock();\n releaseCurrent();\n if (sqlMutationQueue.get(execSql) === queuedCurrent) {\n sqlMutationQueue.delete(execSql);\n }\n recordUserTimingEntry({\n endMs: releasedAtMs,\n failed,\n name: \"SQLite mutation\",\n startMs: acquiredAtMs,\n track: \"SQLite mutations\",\n });\n }\n}\n"]}
@@ -1,5 +1,6 @@
1
+ import { readUserTimingClock, recordUserTimingEntry, } from "../shared/userTiming.js";
1
2
  import { describeSyncLaneError, publishSyncCoordinatorSnapshot, } from "./coordinatorState.js";
2
- import { createSyncTimestamp } from "./syncTelemetry.js";
3
+ import { addSyncLaneRunDuration, createSyncTimestamp } from "./syncTelemetry.js";
3
4
  const SYNC_LANE_WATCHDOG_MS = 120_000;
4
5
  const FAILED_LANE_REARM_BACKOFF_MS = 1000;
5
6
  // Keyed on the registered config so a discarded lane is collectable.
@@ -72,13 +73,33 @@ function recordSyncLaneRunResult(lane, runResult) {
72
73
  lane.lastError = null;
73
74
  }
74
75
  }
76
+ // Times a settled run and draws it on the DevTools "Sync lanes" track. A
77
+ // watchdog verdict is not a settlement and records nothing: the abandoned run
78
+ // is timed when its late result arrives.
79
+ function recordSyncLaneRunTiming(lane, startedAtMs, runResult, abandoned) {
80
+ const endedAtMs = readUserTimingClock();
81
+ lane.runDurations = addSyncLaneRunDuration(lane.runDurations, endedAtMs - startedAtMs);
82
+ recordUserTimingEntry({
83
+ endMs: endedAtMs,
84
+ failed: runResult.status === "failed",
85
+ name: `Sync lane: ${lane.config.label ?? lane.key}`,
86
+ properties: [
87
+ ["phase", lane.config.phase ?? "document"],
88
+ ["outcome", runResult.status],
89
+ ["watchdog", abandoned ? "abandoned" : "within limit"],
90
+ ],
91
+ startMs: startedAtMs,
92
+ track: "Sync lanes",
93
+ });
94
+ }
75
95
  // Replace the watchdog failure with the late result, then release queued work.
76
- function settleAbandonedSyncLaneRun(coordinatorState, lane, runToken, runResult, wake) {
77
- if (lane.activeRunToken !== runToken) {
96
+ function settleAbandonedSyncLaneRun(coordinatorState, lane, run, runResult, wake) {
97
+ if (lane.activeRunToken !== run.token) {
78
98
  return;
79
99
  }
80
100
  lane.activeRunToken = null;
81
101
  lane.runAbandoned = false;
102
+ recordSyncLaneRunTiming(lane, run.startedAtMs, runResult, true);
82
103
  recordSyncLaneRunResult(lane, runResult);
83
104
  publishSyncCoordinatorSnapshot(coordinatorState);
84
105
  if (!coordinatorState.disposed && lane.requested) {
@@ -97,6 +118,7 @@ function startSyncLaneRun(coordinatorState, lane) {
97
118
  const runToken = {};
98
119
  lane.activeRunToken = runToken;
99
120
  publishSyncCoordinatorSnapshot(coordinatorState);
121
+ const startedAtMs = readUserTimingClock();
100
122
  // Keep follow-up requests even on failure to preserve phase ordering; see
101
123
  // docs/client-sync-ordering.md.
102
124
  // The fallback also catches errors thrown by the lane's error handlers.
@@ -104,10 +126,10 @@ function startSyncLaneRun(coordinatorState, lane) {
104
126
  reportUnexpectedSyncLaneError(lane, error);
105
127
  return { status: "failed", error };
106
128
  });
107
- return { runToken, runResultPromise };
129
+ return { runToken, runResultPromise, startedAtMs };
108
130
  }
109
131
  export async function runCoordinatorLane(coordinatorState, lane, wake) {
110
- const { runToken, runResultPromise } = startSyncLaneRun(coordinatorState, lane);
132
+ const { runToken, runResultPromise, startedAtMs } = startSyncLaneRun(coordinatorState, lane);
111
133
  // Clamp overrides to at least 1ms so every run has a chance to complete.
112
134
  const watchdogMs = Math.max(1, lane.config.watchdogMs ?? SYNC_LANE_WATCHDOG_MS);
113
135
  let watchdogTimer = null;
@@ -128,6 +150,7 @@ export async function runCoordinatorLane(coordinatorState, lane, wake) {
128
150
  if (raceOutcome.kind === "settled") {
129
151
  lane.activeRunToken = null;
130
152
  runResult = raceOutcome.result;
153
+ recordSyncLaneRunTiming(lane, startedAtMs, runResult, false);
131
154
  }
132
155
  else {
133
156
  lane.runAbandoned = true;
@@ -135,7 +158,7 @@ export async function runCoordinatorLane(coordinatorState, lane, wake) {
135
158
  status: "failed",
136
159
  error: new Error(`Sync lane watchdog: run exceeded ${watchdogMs}ms; continuing with other lanes`),
137
160
  };
138
- void runResultPromise.then((result) => settleAbandonedSyncLaneRun(coordinatorState, lane, runToken, result, wake));
161
+ void runResultPromise.then((result) => settleAbandonedSyncLaneRun(coordinatorState, lane, { startedAtMs, token: runToken }, result, wake));
139
162
  }
140
163
  recordSyncLaneRunResult(lane, runResult);
141
164
  publishSyncCoordinatorSnapshot(coordinatorState);
@@ -1 +1 @@
1
- {"version":3,"file":"coordinatorLaneRun.js","sourceRoot":"","sources":["coordinatorLaneRun.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,qBAAqB,EACrB,8BAA8B,GAC/B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,MAAM,qBAAqB,GAAG,OAAO,CAAC;AACtC,MAAM,4BAA4B,GAAG,IAAI,CAAC;AAM1C,qEAAqE;AACrE,MAAM,2BAA2B,GAAG,IAAI,OAAO,EAA0B,CAAC;AAE1E,2EAA2E;AAC3E,8EAA8E;AAC9E,2EAA2E;AAC3E,oCAAoC;AACpC,SAAS,gCAAgC,CAAC,KAAc;IACtD,MAAM,IAAI,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC;IAChE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,0EAA0E;AAC1E,2EAA2E;AAC3E,qCAAqC;AACrC,SAAS,+BAA+B,CACtC,KAAoB,EACpB,KAAc;IAEd,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,qBAAqB,CAAC;IAClD,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,MAAM,SAAS,GAAG,gCAAgC,CAAC,KAAK,CAAC,CAAC;IAC1D,IAAI,2BAA2B,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,SAAS;QAAE,OAAO;IACxE,2BAA2B,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzD,IAAI,CAAC;QACH,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;IAChE,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,KAAoB;IAC7C,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QACjC,CAAC;QACD,+BAA+B,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAE9C,IAAI,KAAK,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC;YACnC,KAAK,CAAC,MAAM,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;YACtC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACrC,CAAC;QAED,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,gFAAgF;AAChF,+EAA+E;AAC/E,sEAAsE;AACtE,2EAA2E;AAC3E,SAAS,6BAA6B,CAAC,KAAoB,EAAE,KAAc;IACzE,OAAO,CAAC,KAAK,CAAC,2BAA2B,KAAK,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,uBAAuB,CAC9B,IAAmB,EACnB,SAA4B;IAE5B,IAAI,CAAC,YAAY,GAAG,mBAAmB,EAAE,CAAC;IAC1C,IAAI,SAAS,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAClC,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,qBAAqB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACxD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;IACxC,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC;QAC9B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,SAAS,0BAA0B,CACjC,gBAA4C,EAC5C,IAAmB,EACnB,QAAgB,EAChB,SAA4B,EAC5B,IAAgB;IAEhB,IAAI,IAAI,CAAC,cAAc,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO;IACT,CAAC;IACD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;IAC3B,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;IAC1B,uBAAuB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACzC,8BAA8B,CAAC,gBAAgB,CAAC,CAAC;IACjD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACjD,IAAI,EAAE,CAAC;IACT,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CACvB,gBAA4C,EAC5C,IAAmB;IAEnB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACvB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACpB,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;IACnB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,YAAY,GAAG,mBAAmB,EAAE,CAAC;IAC1C,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,YAAY,CAAC;IACvC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;IAC1B,MAAM,QAAQ,GAAG,EAAE,CAAC;IACpB,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC;IAC/B,8BAA8B,CAAC,gBAAgB,CAAC,CAAC;IAEjD,0EAA0E;IAC1E,gCAAgC;IAChC,wEAAwE;IACxE,MAAM,gBAAgB,GAA+B,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,CACzE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAClB,CAAC,KAAc,EAAE,EAAE;QACjB,6BAA6B,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACrC,CAAC,CACF,CAAC;IAEF,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,gBAA4C,EAC5C,IAAmB,EACnB,IAAgB;IAEhB,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CACrD,gBAAgB,EAChB,IAAI,CACL,CAAC;IACF,yEAAyE;IACzE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CACzB,CAAC,EACD,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,qBAAqB,CAChD,CAAC;IACF,IAAI,aAAa,GAAyC,IAAI,CAAC;IAC/D,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;QACrC,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACjC,IAAI,EAAE,SAAkB;YACxB,MAAM;SACP,CAAC,CAAC;QACH,IAAI,OAAO,CAAwB,CAAC,OAAO,EAAE,EAAE;YAC7C,aAAa,GAAG,UAAU,CACxB,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,EACpC,UAAU,CACX,CAAC;QACJ,CAAC,CAAC;KACH,CAAC,CAAC;IACH,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;QAC3B,YAAY,CAAC,aAAa,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACrB,IAAI,SAA4B,CAAC;IACjC,IAAI,WAAW,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACnC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC;IACjC,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,SAAS,GAAG;YACV,MAAM,EAAE,QAAQ;YAChB,KAAK,EAAE,IAAI,KAAK,CACd,oCAAoC,UAAU,iCAAiC,CAChF;SACF,CAAC;QACF,KAAK,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CACpC,0BAA0B,CACxB,gBAAgB,EAChB,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,IAAI,CACL,CACF,CAAC;IACJ,CAAC;IACD,uBAAuB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACzC,8BAA8B,CAAC,gBAAgB,CAAC,CAAC;IAEjD,IACE,SAAS,CAAC,MAAM,KAAK,QAAQ;QAC7B,IAAI,CAAC,SAAS;QACd,WAAW,CAAC,IAAI,KAAK,SAAS,EAC9B,CAAC;QACD,sEAAsE;QACtE,wEAAwE;QACxE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,UAAU,CAAC,GAAG,EAAE;YACd,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YACxB,IAAI,EAAE,CAAC;QACT,CAAC,EAAE,4BAA4B,CAAC,CAAC;IACnC,CAAC;AACH,CAAC","sourcesContent":["import type {\n DomainSyncCoordinatorState,\n SyncLaneState,\n} from \"./coordinatorState\";\nimport {\n describeSyncLaneError,\n publishSyncCoordinatorSnapshot,\n} from \"./coordinatorState\";\nimport type { SyncLaneConfig } from \"./syncLaneConfig\";\nimport { createSyncTimestamp } from \"./syncTelemetry\";\n\nconst SYNC_LANE_WATCHDOG_MS = 120_000;\nconst FAILED_LANE_REARM_BACKOFF_MS = 1000;\n\ntype SyncLaneRunResult =\n | { status: \"completed\" }\n | { status: \"failed\"; error: unknown };\n\n// Keyed on the registered config so a discarded lane is collectable.\nconst lastReportedFailureByConfig = new WeakMap<SyncLaneConfig, string>();\n\n// Local-only dedup key. describeSyncLaneError joins messages without their\n// types, so the name keeps a swallowed DatabaseUnavailableError distinct from\n// a plain Error carrying the same text; the cause chain keeps a re-wrapped\n// failure distinct from its origin.\nfunction describeSyncLaneFailureSignature(error: unknown): string {\n const name = error instanceof Error ? error.name : typeof error;\n return JSON.stringify([name, describeSyncLaneError(error)]);\n}\n\n// Observability only: a failing lane re-arms every FAILED_LANE_REARM_BACKOFF_MS\n// for the rest of the session, and mobile WebViews are never reloaded, so an\n// unrecoverable lane would report once a second. The signature is deliberately\n// never cleared on a completed run: alternating success and failure would\n// re-admit the same report every pass and evict the surrounding diagnostic\n// trail from the host's bounded log.\nfunction reportUnexpectedSyncLaneFailure(\n state: SyncLaneState,\n error: unknown,\n): void {\n const report = state.config.reportUnexpectedError;\n if (!report) return;\n const signature = describeSyncLaneFailureSignature(error);\n if (lastReportedFailureByConfig.get(state.config) === signature) return;\n lastReportedFailureByConfig.set(state.config, signature);\n try {\n void Promise.resolve(report(error)).catch(() => undefined);\n } catch {\n // Hosts may throw synchronously or return a rejected promise.\n }\n}\n\nasync function runSyncLane(state: SyncLaneState): Promise<SyncLaneRunResult> {\n try {\n await state.config.run();\n return { status: \"completed\" };\n } catch (error: unknown) {\n if (state.config.shouldIgnoreError?.(error)) {\n return { status: \"completed\" };\n }\n reportUnexpectedSyncLaneFailure(state, error);\n\n if (state.config.onUnexpectedError) {\n state.config.onUnexpectedError(error);\n return { status: \"failed\", error };\n }\n\n throw error;\n }\n}\n\n// Console-only on purpose. This runs only when the lane's OWN error handler\n// already threw, so it must never route through reportUnexpectedSyncLaneFailure\n// as well: re-entering a handler that just threw would reject runResultPromise\n// from inside its own onRejected callback, leaving `running` true and\n// `activeRunToken` set, which wedges the lane for the rest of the session.\nfunction reportUnexpectedSyncLaneError(state: SyncLaneState, error: unknown) {\n console.error(`Failed to run sync lane ${state.key}:`, error);\n}\n\nfunction recordSyncLaneRunResult(\n lane: SyncLaneState,\n runResult: SyncLaneRunResult,\n): void {\n lane.lastActionAt = createSyncTimestamp();\n if (runResult.status === \"failed\") {\n lane.errorCount += 1;\n lane.lastAction = \"failed\";\n lane.lastError = describeSyncLaneError(runResult.error);\n lane.lastFailedAt = lane.lastActionAt;\n } else {\n lane.lastAction = \"completed\";\n lane.lastCompletedAt = lane.lastActionAt;\n lane.lastError = null;\n }\n}\n\n// Replace the watchdog failure with the late result, then release queued work.\nfunction settleAbandonedSyncLaneRun(\n coordinatorState: DomainSyncCoordinatorState,\n lane: SyncLaneState,\n runToken: object,\n runResult: SyncLaneRunResult,\n wake: () => void,\n): void {\n if (lane.activeRunToken !== runToken) {\n return;\n }\n lane.activeRunToken = null;\n lane.runAbandoned = false;\n recordSyncLaneRunResult(lane, runResult);\n publishSyncCoordinatorSnapshot(coordinatorState);\n if (!coordinatorState.disposed && lane.requested) {\n wake();\n }\n}\n\nfunction startSyncLaneRun(\n coordinatorState: DomainSyncCoordinatorState,\n lane: SyncLaneState,\n) {\n lane.requested = false;\n lane.notBeforeAtMs = null;\n lane.running = true;\n lane.runCount += 1;\n lane.lastAction = \"started\";\n lane.lastActionAt = createSyncTimestamp();\n lane.lastStartedAt = lane.lastActionAt;\n lane.runAbandoned = false;\n const runToken = {};\n lane.activeRunToken = runToken;\n publishSyncCoordinatorSnapshot(coordinatorState);\n\n // Keep follow-up requests even on failure to preserve phase ordering; see\n // docs/client-sync-ordering.md.\n // The fallback also catches errors thrown by the lane's error handlers.\n const runResultPromise: Promise<SyncLaneRunResult> = runSyncLane(lane).then(\n (result) => result,\n (error: unknown) => {\n reportUnexpectedSyncLaneError(lane, error);\n return { status: \"failed\", error };\n },\n );\n\n return { runToken, runResultPromise };\n}\n\nexport async function runCoordinatorLane(\n coordinatorState: DomainSyncCoordinatorState,\n lane: SyncLaneState,\n wake: () => void,\n): Promise<void> {\n const { runToken, runResultPromise } = startSyncLaneRun(\n coordinatorState,\n lane,\n );\n // Clamp overrides to at least 1ms so every run has a chance to complete.\n const watchdogMs = Math.max(\n 1,\n lane.config.watchdogMs ?? SYNC_LANE_WATCHDOG_MS,\n );\n let watchdogTimer: ReturnType<typeof setTimeout> | null = null;\n const raceOutcome = await Promise.race([\n runResultPromise.then((result) => ({\n kind: \"settled\" as const,\n result,\n })),\n new Promise<{ kind: \"timed-out\" }>((resolve) => {\n watchdogTimer = setTimeout(\n () => resolve({ kind: \"timed-out\" }),\n watchdogMs,\n );\n }),\n ]);\n if (watchdogTimer !== null) {\n clearTimeout(watchdogTimer);\n }\n\n lane.running = false;\n let runResult: SyncLaneRunResult;\n if (raceOutcome.kind === \"settled\") {\n lane.activeRunToken = null;\n runResult = raceOutcome.result;\n } else {\n lane.runAbandoned = true;\n runResult = {\n status: \"failed\",\n error: new Error(\n `Sync lane watchdog: run exceeded ${watchdogMs}ms; continuing with other lanes`,\n ),\n };\n void runResultPromise.then((result) =>\n settleAbandonedSyncLaneRun(\n coordinatorState,\n lane,\n runToken,\n result,\n wake,\n ),\n );\n }\n recordSyncLaneRunResult(lane, runResult);\n publishSyncCoordinatorSnapshot(coordinatorState);\n\n if (\n runResult.status === \"failed\" &&\n lane.requested &&\n raceOutcome.kind === \"settled\"\n ) {\n // A fresh hint cannot bypass a failed lane's cooldown. Other document\n // lanes remain runnable; structural retries still preserve the barrier.\n lane.backingOff = true;\n setTimeout(() => {\n lane.backingOff = false;\n wake();\n }, FAILED_LANE_REARM_BACKOFF_MS);\n }\n}\n"]}
1
+ {"version":3,"file":"coordinatorLaneRun.js","sourceRoot":"","sources":["coordinatorLaneRun.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n readUserTimingClock,\n recordUserTimingEntry,\n} from \"../shared/userTiming\";\nimport type {\n DomainSyncCoordinatorState,\n SyncLaneState,\n} from \"./coordinatorState\";\nimport {\n describeSyncLaneError,\n publishSyncCoordinatorSnapshot,\n} from \"./coordinatorState\";\nimport type { SyncLaneConfig } from \"./syncLaneConfig\";\nimport { addSyncLaneRunDuration, createSyncTimestamp } from \"./syncTelemetry\";\n\nconst SYNC_LANE_WATCHDOG_MS = 120_000;\nconst FAILED_LANE_REARM_BACKOFF_MS = 1000;\n\ntype SyncLaneRunResult =\n | { status: \"completed\" }\n | { status: \"failed\"; error: unknown };\n\n// Keyed on the registered config so a discarded lane is collectable.\nconst lastReportedFailureByConfig = new WeakMap<SyncLaneConfig, string>();\n\n// Local-only dedup key. describeSyncLaneError joins messages without their\n// types, so the name keeps a swallowed DatabaseUnavailableError distinct from\n// a plain Error carrying the same text; the cause chain keeps a re-wrapped\n// failure distinct from its origin.\nfunction describeSyncLaneFailureSignature(error: unknown): string {\n const name = error instanceof Error ? error.name : typeof error;\n return JSON.stringify([name, describeSyncLaneError(error)]);\n}\n\n// Observability only: a failing lane re-arms every FAILED_LANE_REARM_BACKOFF_MS\n// for the rest of the session, and mobile WebViews are never reloaded, so an\n// unrecoverable lane would report once a second. The signature is deliberately\n// never cleared on a completed run: alternating success and failure would\n// re-admit the same report every pass and evict the surrounding diagnostic\n// trail from the host's bounded log.\nfunction reportUnexpectedSyncLaneFailure(\n state: SyncLaneState,\n error: unknown,\n): void {\n const report = state.config.reportUnexpectedError;\n if (!report) return;\n const signature = describeSyncLaneFailureSignature(error);\n if (lastReportedFailureByConfig.get(state.config) === signature) return;\n lastReportedFailureByConfig.set(state.config, signature);\n try {\n void Promise.resolve(report(error)).catch(() => undefined);\n } catch {\n // Hosts may throw synchronously or return a rejected promise.\n }\n}\n\nasync function runSyncLane(state: SyncLaneState): Promise<SyncLaneRunResult> {\n try {\n await state.config.run();\n return { status: \"completed\" };\n } catch (error: unknown) {\n if (state.config.shouldIgnoreError?.(error)) {\n return { status: \"completed\" };\n }\n reportUnexpectedSyncLaneFailure(state, error);\n\n if (state.config.onUnexpectedError) {\n state.config.onUnexpectedError(error);\n return { status: \"failed\", error };\n }\n\n throw error;\n }\n}\n\n// Console-only on purpose. This runs only when the lane's OWN error handler\n// already threw, so it must never route through reportUnexpectedSyncLaneFailure\n// as well: re-entering a handler that just threw would reject runResultPromise\n// from inside its own onRejected callback, leaving `running` true and\n// `activeRunToken` set, which wedges the lane for the rest of the session.\nfunction reportUnexpectedSyncLaneError(state: SyncLaneState, error: unknown) {\n console.error(`Failed to run sync lane ${state.key}:`, error);\n}\n\nfunction recordSyncLaneRunResult(\n lane: SyncLaneState,\n runResult: SyncLaneRunResult,\n): void {\n lane.lastActionAt = createSyncTimestamp();\n if (runResult.status === \"failed\") {\n lane.errorCount += 1;\n lane.lastAction = \"failed\";\n lane.lastError = describeSyncLaneError(runResult.error);\n lane.lastFailedAt = lane.lastActionAt;\n } else {\n lane.lastAction = \"completed\";\n lane.lastCompletedAt = lane.lastActionAt;\n lane.lastError = null;\n }\n}\n\n// Times a settled run and draws it on the DevTools \"Sync lanes\" track. A\n// watchdog verdict is not a settlement and records nothing: the abandoned run\n// is timed when its late result arrives.\nfunction recordSyncLaneRunTiming(\n lane: SyncLaneState,\n startedAtMs: number,\n runResult: SyncLaneRunResult,\n abandoned: boolean,\n): void {\n const endedAtMs = readUserTimingClock();\n lane.runDurations = addSyncLaneRunDuration(\n lane.runDurations,\n endedAtMs - startedAtMs,\n );\n recordUserTimingEntry({\n endMs: endedAtMs,\n failed: runResult.status === \"failed\",\n name: `Sync lane: ${lane.config.label ?? lane.key}`,\n properties: [\n [\"phase\", lane.config.phase ?? \"document\"],\n [\"outcome\", runResult.status],\n [\"watchdog\", abandoned ? \"abandoned\" : \"within limit\"],\n ],\n startMs: startedAtMs,\n track: \"Sync lanes\",\n });\n}\n\n// Replace the watchdog failure with the late result, then release queued work.\nfunction settleAbandonedSyncLaneRun(\n coordinatorState: DomainSyncCoordinatorState,\n lane: SyncLaneState,\n run: { readonly startedAtMs: number; readonly token: object },\n runResult: SyncLaneRunResult,\n wake: () => void,\n): void {\n if (lane.activeRunToken !== run.token) {\n return;\n }\n lane.activeRunToken = null;\n lane.runAbandoned = false;\n recordSyncLaneRunTiming(lane, run.startedAtMs, runResult, true);\n recordSyncLaneRunResult(lane, runResult);\n publishSyncCoordinatorSnapshot(coordinatorState);\n if (!coordinatorState.disposed && lane.requested) {\n wake();\n }\n}\n\nfunction startSyncLaneRun(\n coordinatorState: DomainSyncCoordinatorState,\n lane: SyncLaneState,\n) {\n lane.requested = false;\n lane.notBeforeAtMs = null;\n lane.running = true;\n lane.runCount += 1;\n lane.lastAction = \"started\";\n lane.lastActionAt = createSyncTimestamp();\n lane.lastStartedAt = lane.lastActionAt;\n lane.runAbandoned = false;\n const runToken = {};\n lane.activeRunToken = runToken;\n publishSyncCoordinatorSnapshot(coordinatorState);\n const startedAtMs = readUserTimingClock();\n\n // Keep follow-up requests even on failure to preserve phase ordering; see\n // docs/client-sync-ordering.md.\n // The fallback also catches errors thrown by the lane's error handlers.\n const runResultPromise: Promise<SyncLaneRunResult> = runSyncLane(lane).then(\n (result) => result,\n (error: unknown) => {\n reportUnexpectedSyncLaneError(lane, error);\n return { status: \"failed\", error };\n },\n );\n\n return { runToken, runResultPromise, startedAtMs };\n}\n\nexport async function runCoordinatorLane(\n coordinatorState: DomainSyncCoordinatorState,\n lane: SyncLaneState,\n wake: () => void,\n): Promise<void> {\n const { runToken, runResultPromise, startedAtMs } = startSyncLaneRun(\n coordinatorState,\n lane,\n );\n // Clamp overrides to at least 1ms so every run has a chance to complete.\n const watchdogMs = Math.max(\n 1,\n lane.config.watchdogMs ?? SYNC_LANE_WATCHDOG_MS,\n );\n let watchdogTimer: ReturnType<typeof setTimeout> | null = null;\n const raceOutcome = await Promise.race([\n runResultPromise.then((result) => ({\n kind: \"settled\" as const,\n result,\n })),\n new Promise<{ kind: \"timed-out\" }>((resolve) => {\n watchdogTimer = setTimeout(\n () => resolve({ kind: \"timed-out\" }),\n watchdogMs,\n );\n }),\n ]);\n if (watchdogTimer !== null) {\n clearTimeout(watchdogTimer);\n }\n\n lane.running = false;\n let runResult: SyncLaneRunResult;\n if (raceOutcome.kind === \"settled\") {\n lane.activeRunToken = null;\n runResult = raceOutcome.result;\n recordSyncLaneRunTiming(lane, startedAtMs, runResult, false);\n } else {\n lane.runAbandoned = true;\n runResult = {\n status: \"failed\",\n error: new Error(\n `Sync lane watchdog: run exceeded ${watchdogMs}ms; continuing with other lanes`,\n ),\n };\n void runResultPromise.then((result) =>\n settleAbandonedSyncLaneRun(\n coordinatorState,\n lane,\n { startedAtMs, token: runToken },\n result,\n wake,\n ),\n );\n }\n recordSyncLaneRunResult(lane, runResult);\n publishSyncCoordinatorSnapshot(coordinatorState);\n\n if (\n runResult.status === \"failed\" &&\n lane.requested &&\n raceOutcome.kind === \"settled\"\n ) {\n // A fresh hint cannot bypass a failed lane's cooldown. Other document\n // lanes remain runnable; structural retries still preserve the barrier.\n lane.backingOff = true;\n setTimeout(() => {\n lane.backingOff = false;\n wake();\n }, FAILED_LANE_REARM_BACKOFF_MS);\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { SyncLaneConfig } from "./syncLaneConfig.js";
2
- import type { DomainSyncSnapshot, SyncLaneLastAction, SyncLaneProgress } from "./syncTelemetry.js";
2
+ import type { DomainSyncSnapshot, SyncLaneLastAction, SyncLaneProgress, SyncLaneRunDurations } from "./syncTelemetry.js";
3
3
  export interface SyncLaneState {
4
4
  activeRunToken: object | null;
5
5
  backingOff: boolean;
@@ -23,6 +23,7 @@ export interface SyncLaneState {
23
23
  requested: boolean;
24
24
  runAbandoned: boolean;
25
25
  runCount: number;
26
+ runDurations: SyncLaneRunDurations | null;
26
27
  running: boolean;
27
28
  }
28
29
  export interface DomainSyncCoordinatorState {
@@ -41,6 +42,7 @@ export declare const INITIAL_SYNC_LANE_RUN_STATE: {
41
42
  readonly lastRunOrder: 0;
42
43
  readonly runAbandoned: false;
43
44
  readonly runCount: 0;
45
+ readonly runDurations: null;
44
46
  readonly running: false;
45
47
  };
46
48
  export declare function describeSyncLaneError(error: unknown): string;
@@ -5,6 +5,7 @@ export const INITIAL_SYNC_LANE_RUN_STATE = {
5
5
  lastRunOrder: 0,
6
6
  runAbandoned: false,
7
7
  runCount: 0,
8
+ runDurations: null,
8
9
  running: false,
9
10
  };
10
11
  function describeSingleSyncLaneError(error) {
@@ -1 +1 @@
1
- {"version":3,"file":"coordinatorState.js","sourceRoot":"","sources":["coordinatorState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SyncLaneConfig } from \"./syncLaneConfig\";\nimport type {\n DomainSyncSnapshot,\n SyncLaneLastAction,\n SyncLaneProgress,\n} from \"./syncTelemetry\";\nimport { createDomainSyncSnapshot } from \"./syncTelemetry\";\n\nexport interface SyncLaneState {\n // Identity of the lane's live run, set for the duration of a run INCLUDING\n // after a watchdog timeout abandons it. Selection skips lanes whose token is\n // set, so a lane can never run concurrently with its own abandoned run; the\n // late-settle continuation clears it (matching on identity) and re-pumps.\n activeRunToken: object | null;\n backingOff: boolean;\n lastRunOrder: number;\n blobStorageKey: string | null;\n config: SyncLaneConfig;\n errorCount: number;\n key: string;\n lastAction: SyncLaneLastAction;\n lastActionAt: string;\n lastCompletedAt: string | null;\n lastError: string | null;\n lastFailedAt: string | null;\n lastRequestedAt: string | null;\n lastStartedAt: string | null;\n notBeforeAtMs: number | null;\n progress: SyncLaneProgress | null;\n // Registered lanes are driven by the coordinator pump. Blob upload lanes\n // are observational telemetry driven directly by the upload workflow.\n pumpDriven: boolean;\n registrationIndex: number;\n requestCount: number;\n requested: boolean;\n runAbandoned: boolean;\n runCount: number;\n running: boolean;\n}\n\nexport interface DomainSyncCoordinatorState {\n // Set by dispose(): stop admitting work and drain already-running callbacks\n // (or their watchdogs). No new pump can be scheduled after runtime teardown.\n disposed: boolean;\n lanes: Map<string, SyncLaneState>;\n listeners: Set<() => void>;\n nextRegistrationIndex: number;\n nextRunOrder: number;\n wakePump: (() => void) | null;\n pump: Promise<void> | null;\n snapshot: DomainSyncSnapshot;\n}\n\nexport const INITIAL_SYNC_LANE_RUN_STATE = {\n activeRunToken: null,\n backingOff: false,\n lastRunOrder: 0,\n runAbandoned: false,\n runCount: 0,\n running: false,\n} as const;\n\nfunction describeSingleSyncLaneError(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n return error.message;\n }\n\n return String(error);\n}\n\nfunction readErrorCause(error: unknown): unknown {\n if (typeof error !== \"object\" || error === null) {\n return undefined;\n }\n\n return Reflect.get(error, \"cause\");\n}\n\nexport function describeSyncLaneError(error: unknown): string {\n const messages: string[] = [];\n const seen = new Set<unknown>();\n let current: unknown = error;\n\n while (current !== undefined && current !== null && !seen.has(current)) {\n seen.add(current);\n const message = describeSingleSyncLaneError(current);\n if (message.length > 0) {\n messages.push(message);\n }\n current = readErrorCause(current);\n }\n\n return messages.length > 0 ? messages.join(\" Caused by: \") : String(error);\n}\n\n// Observational upload lanes never enter the pump, so their run is never\n// invoked; this satisfies the SyncLaneConfig contract without behavior.\nexport function noopLaneRun(): Promise<void> {\n return Promise.resolve();\n}\n\n// Deliberately ignores activeRunToken (unlike the pump's hasRunnableLaneWork):\n// a watchdog-abandoned run is detached background work, and idle/pending-work\n// reporting must not block on a hung run that may never settle. Its late\n// settle still publishes a snapshot and re-pumps any queued re-request.\nexport function hasPendingLaneWork(lanes: Iterable<SyncLaneState>): boolean {\n for (const lane of lanes) {\n if (lane.requested || lane.running) {\n return true;\n }\n }\n\n return false;\n}\n\nexport function publishSyncCoordinatorSnapshot(\n state: DomainSyncCoordinatorState,\n) {\n state.snapshot = createDomainSyncSnapshot({\n hasPendingWork: !!state.pump || hasPendingLaneWork(state.lanes.values()),\n lanes: state.lanes.values(),\n pumpActive: !!state.pump,\n });\n for (const listener of [...state.listeners]) {\n try {\n listener();\n } catch {\n // Keep one observer failure from blocking sync or later observers.\n }\n }\n}\n"]}
1
+ {"version":3,"file":"coordinatorState.js","sourceRoot":"","sources":["coordinatorState.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SyncLaneConfig } from \"./syncLaneConfig\";\nimport type {\n DomainSyncSnapshot,\n SyncLaneLastAction,\n SyncLaneProgress,\n SyncLaneRunDurations,\n} from \"./syncTelemetry\";\nimport { createDomainSyncSnapshot } from \"./syncTelemetry\";\n\nexport interface SyncLaneState {\n // Identity of the lane's live run, set for the duration of a run INCLUDING\n // after a watchdog timeout abandons it. Selection skips lanes whose token is\n // set, so a lane can never run concurrently with its own abandoned run; the\n // late-settle continuation clears it (matching on identity) and re-pumps.\n activeRunToken: object | null;\n backingOff: boolean;\n lastRunOrder: number;\n blobStorageKey: string | null;\n config: SyncLaneConfig;\n errorCount: number;\n key: string;\n lastAction: SyncLaneLastAction;\n lastActionAt: string;\n lastCompletedAt: string | null;\n lastError: string | null;\n lastFailedAt: string | null;\n lastRequestedAt: string | null;\n lastStartedAt: string | null;\n notBeforeAtMs: number | null;\n progress: SyncLaneProgress | null;\n // Registered lanes are driven by the coordinator pump. Blob upload lanes\n // are observational telemetry driven directly by the upload workflow.\n pumpDriven: boolean;\n registrationIndex: number;\n requestCount: number;\n requested: boolean;\n runAbandoned: boolean;\n runCount: number;\n runDurations: SyncLaneRunDurations | null;\n running: boolean;\n}\n\nexport interface DomainSyncCoordinatorState {\n // Set by dispose(): stop admitting work and drain already-running callbacks\n // (or their watchdogs). No new pump can be scheduled after runtime teardown.\n disposed: boolean;\n lanes: Map<string, SyncLaneState>;\n listeners: Set<() => void>;\n nextRegistrationIndex: number;\n nextRunOrder: number;\n wakePump: (() => void) | null;\n pump: Promise<void> | null;\n snapshot: DomainSyncSnapshot;\n}\n\nexport const INITIAL_SYNC_LANE_RUN_STATE = {\n activeRunToken: null,\n backingOff: false,\n lastRunOrder: 0,\n runAbandoned: false,\n runCount: 0,\n runDurations: null,\n running: false,\n} as const;\n\nfunction describeSingleSyncLaneError(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n return error.message;\n }\n\n return String(error);\n}\n\nfunction readErrorCause(error: unknown): unknown {\n if (typeof error !== \"object\" || error === null) {\n return undefined;\n }\n\n return Reflect.get(error, \"cause\");\n}\n\nexport function describeSyncLaneError(error: unknown): string {\n const messages: string[] = [];\n const seen = new Set<unknown>();\n let current: unknown = error;\n\n while (current !== undefined && current !== null && !seen.has(current)) {\n seen.add(current);\n const message = describeSingleSyncLaneError(current);\n if (message.length > 0) {\n messages.push(message);\n }\n current = readErrorCause(current);\n }\n\n return messages.length > 0 ? messages.join(\" Caused by: \") : String(error);\n}\n\n// Observational upload lanes never enter the pump, so their run is never\n// invoked; this satisfies the SyncLaneConfig contract without behavior.\nexport function noopLaneRun(): Promise<void> {\n return Promise.resolve();\n}\n\n// Deliberately ignores activeRunToken (unlike the pump's hasRunnableLaneWork):\n// a watchdog-abandoned run is detached background work, and idle/pending-work\n// reporting must not block on a hung run that may never settle. Its late\n// settle still publishes a snapshot and re-pumps any queued re-request.\nexport function hasPendingLaneWork(lanes: Iterable<SyncLaneState>): boolean {\n for (const lane of lanes) {\n if (lane.requested || lane.running) {\n return true;\n }\n }\n\n return false;\n}\n\nexport function publishSyncCoordinatorSnapshot(\n state: DomainSyncCoordinatorState,\n) {\n state.snapshot = createDomainSyncSnapshot({\n hasPendingWork: !!state.pump || hasPendingLaneWork(state.lanes.values()),\n lanes: state.lanes.values(),\n pumpActive: !!state.pump,\n });\n for (const listener of [...state.listeners]) {\n try {\n listener();\n } catch {\n // Keep one observer failure from blocking sync or later observers.\n }\n }\n}\n"]}
@@ -12,6 +12,17 @@ export interface SyncLaneProgress {
12
12
  partsCompleted: number;
13
13
  partsTotal: number;
14
14
  }
15
+ /**
16
+ * Monotonic durations of a lane's settled runs. A run abandoned by its
17
+ * watchdog is counted when it finally settles, with its real duration; the
18
+ * watchdog verdict itself records none, since the run had not finished.
19
+ */
20
+ export interface SyncLaneRunDurations {
21
+ count: number;
22
+ lastMs: number;
23
+ maxMs: number;
24
+ totalMs: number;
25
+ }
15
26
  export interface SyncLaneSnapshot {
16
27
  /** Storage key used to open the associated blob, when this is a blob lane. */
17
28
  blobStorageKey: string | null;
@@ -32,6 +43,8 @@ export interface SyncLaneSnapshot {
32
43
  requested: boolean;
33
44
  runAbandoned: boolean;
34
45
  runCount: number;
46
+ /** Null until a run settles. */
47
+ runDurations: SyncLaneRunDurations | null;
35
48
  running: boolean;
36
49
  status: SyncLaneStatus;
37
50
  }
@@ -63,6 +76,7 @@ interface SyncLaneTelemetryState {
63
76
  requested: boolean;
64
77
  runAbandoned: boolean;
65
78
  runCount: number;
79
+ runDurations: SyncLaneRunDurations | null;
66
80
  running: boolean;
67
81
  }
68
82
  /**
@@ -77,6 +91,7 @@ interface SyncLaneOrderInput {
77
91
  registrationIndex: number;
78
92
  }
79
93
  export declare function createSyncTimestamp(): string;
94
+ export declare function addSyncLaneRunDuration(durations: SyncLaneRunDurations | null, durationMs: number): SyncLaneRunDurations;
80
95
  export declare function compareSyncLaneOrder(left: SyncLaneOrderInput, right: SyncLaneOrderInput): number;
81
96
  export declare function createDomainSyncSnapshot(input: {
82
97
  hasPendingWork: boolean;
@@ -7,6 +7,15 @@ const SYNC_LANE_PHASE_RANK = {
7
7
  export function createSyncTimestamp() {
8
8
  return new Date().toISOString();
9
9
  }
10
+ // Returns a new object, so a published snapshot can share it safely.
11
+ export function addSyncLaneRunDuration(durations, durationMs) {
12
+ return {
13
+ count: (durations?.count ?? 0) + 1,
14
+ lastMs: durationMs,
15
+ maxMs: Math.max(durations?.maxMs ?? 0, durationMs),
16
+ totalMs: (durations?.totalMs ?? 0) + durationMs,
17
+ };
18
+ }
10
19
  function getSyncLanePhaseRank(state) {
11
20
  return SYNC_LANE_PHASE_RANK[state.config.phase ?? DEFAULT_SYNC_LANE_PHASE];
12
21
  }
@@ -52,6 +61,7 @@ function createSyncLaneSnapshot(state) {
52
61
  requested: state.requested,
53
62
  runAbandoned: state.runAbandoned,
54
63
  runCount: state.runCount,
64
+ runDurations: state.runDurations,
55
65
  running: state.running,
56
66
  status: getSyncLaneStatus(state),
57
67
  };
@@ -1 +1 @@
1
- {"version":3,"file":"syncTelemetry.js","sourceRoot":"","sources":["syncTelemetry.ts"],"names":[],"mappings":"AAmFA,MAAM,uBAAuB,GAAkB,UAAU,CAAC;AAC1D,MAAM,oBAAoB,GAAkC;IAC1D,UAAU,EAAE,CAAC;IACb,QAAQ,EAAE,CAAC;IACX,IAAI,EAAE,CAAC;CACR,CAAC;AAYF,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAyB;IACrD,OAAO,oBAAoB,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,uBAAuB,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAwB,EACxB,KAAyB;IAEzB,MAAM,SAAS,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAC3E,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC,iBAAiB,CAAC;AAC1D,CAAC;AAED,SAAS,iBAAiB,CAAC,KAA6B;IACtD,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;QAC1B,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,sBAAsB,CAC7B,KAA6B;IAE7B,OAAO;QACL,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG;QACtC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,uBAAuB;QACpD,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI;QAChC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;QAC1C,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC;KACjC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAIxC;IACC,OAAO;QACL,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;aAC3B,IAAI,CAAC,oBAAoB,CAAC;aAC1B,GAAG,CAAC,sBAAsB,CAAC;QAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,SAAS,EAAE,mBAAmB,EAAE;KACjC,CAAC;AACJ,CAAC","sourcesContent":["export type SyncLanePhase = \"structural\" | \"document\" | \"blob\";\nexport type SyncLaneStatus =\n | \"idle\"\n | \"queued\"\n | \"running\"\n | \"complete\"\n | \"error\";\nexport type SyncLaneLastAction =\n | \"registered\"\n | \"requested\"\n | \"started\"\n | \"completed\"\n | \"failed\";\n\n/**\n * Fine-grained progress for lanes that expose it (currently multipart blob\n * uploads). Coarse status lanes leave this null and fall back to the\n * status-keyed progress bar in the visualizer.\n */\nexport interface SyncLaneProgress {\n bytesTotal: number;\n bytesUploaded: number;\n partsCompleted: number;\n partsTotal: number;\n}\n\nexport interface SyncLaneSnapshot {\n /** Storage key used to open the associated blob, when this is a blob lane. */\n blobStorageKey: string | null;\n errorCount: number;\n key: string;\n label: string;\n lastAction: SyncLaneLastAction;\n lastActionAt: string;\n lastCompletedAt: string | null;\n lastError: string | null;\n lastFailedAt: string | null;\n lastRequestedAt: string | null;\n lastStartedAt: string | null;\n phase: SyncLanePhase;\n progress: SyncLaneProgress | null;\n registrationIndex: number;\n requestCount: number;\n requested: boolean;\n runAbandoned: boolean;\n runCount: number;\n running: boolean;\n status: SyncLaneStatus;\n}\n\nexport interface DomainSyncSnapshot {\n hasPendingWork: boolean;\n lanes: ReadonlyArray<SyncLaneSnapshot>;\n pumpActive: boolean;\n updatedAt: string;\n}\n\ninterface SyncLaneTelemetryConfig {\n label?: string | undefined;\n phase?: SyncLanePhase | undefined;\n}\n\ninterface SyncLaneTelemetryState {\n blobStorageKey: string | null;\n config: SyncLaneTelemetryConfig;\n errorCount: number;\n key: string;\n lastAction: SyncLaneLastAction;\n lastActionAt: string;\n lastCompletedAt: string | null;\n lastError: string | null;\n lastFailedAt: string | null;\n lastRequestedAt: string | null;\n lastStartedAt: string | null;\n progress?: SyncLaneProgress | null;\n registrationIndex: number;\n requestCount: number;\n requested: boolean;\n runAbandoned: boolean;\n runCount: number;\n running: boolean;\n}\n\nconst DEFAULT_SYNC_LANE_PHASE: SyncLanePhase = \"document\";\nconst SYNC_LANE_PHASE_RANK: Record<SyncLanePhase, number> = {\n structural: 0,\n document: 1,\n blob: 2,\n};\n\n/**\n * Structural subset of a sync lane needed to order it: its phase and the\n * registration index that breaks phase ties. Accepts both the coordinator's\n * `SyncLaneState` and the telemetry `SyncLaneTelemetryState`.\n */\ninterface SyncLaneOrderInput {\n config: { phase?: SyncLanePhase | undefined };\n registrationIndex: number;\n}\n\nexport function createSyncTimestamp(): string {\n return new Date().toISOString();\n}\n\nfunction getSyncLanePhaseRank(state: SyncLaneOrderInput): number {\n return SYNC_LANE_PHASE_RANK[state.config.phase ?? DEFAULT_SYNC_LANE_PHASE];\n}\n\nexport function compareSyncLaneOrder(\n left: SyncLaneOrderInput,\n right: SyncLaneOrderInput,\n): number {\n const phaseRank = getSyncLanePhaseRank(left) - getSyncLanePhaseRank(right);\n if (phaseRank !== 0) {\n return phaseRank;\n }\n\n return left.registrationIndex - right.registrationIndex;\n}\n\nfunction getSyncLaneStatus(state: SyncLaneTelemetryState): SyncLaneStatus {\n if (state.running) {\n return \"running\";\n }\n\n if (state.requested) {\n return \"queued\";\n }\n\n if (state.lastAction === \"failed\") {\n return \"error\";\n }\n\n if (state.lastCompletedAt) {\n return \"complete\";\n }\n\n return \"idle\";\n}\n\nfunction createSyncLaneSnapshot(\n state: SyncLaneTelemetryState,\n): SyncLaneSnapshot {\n return {\n blobStorageKey: state.blobStorageKey,\n errorCount: state.errorCount,\n key: state.key,\n label: state.config.label ?? state.key,\n lastAction: state.lastAction,\n lastActionAt: state.lastActionAt,\n lastCompletedAt: state.lastCompletedAt,\n lastError: state.lastError,\n lastFailedAt: state.lastFailedAt,\n lastRequestedAt: state.lastRequestedAt,\n lastStartedAt: state.lastStartedAt,\n phase: state.config.phase ?? DEFAULT_SYNC_LANE_PHASE,\n progress: state.progress ?? null,\n registrationIndex: state.registrationIndex,\n requestCount: state.requestCount,\n requested: state.requested,\n runAbandoned: state.runAbandoned,\n runCount: state.runCount,\n running: state.running,\n status: getSyncLaneStatus(state),\n };\n}\n\nexport function createDomainSyncSnapshot(input: {\n hasPendingWork: boolean;\n lanes: Iterable<SyncLaneTelemetryState>;\n pumpActive: boolean;\n}): DomainSyncSnapshot {\n return {\n hasPendingWork: input.hasPendingWork,\n lanes: Array.from(input.lanes)\n .sort(compareSyncLaneOrder)\n .map(createSyncLaneSnapshot),\n pumpActive: input.pumpActive,\n updatedAt: createSyncTimestamp(),\n };\n}\n"]}
1
+ {"version":3,"file":"syncTelemetry.js","sourceRoot":"","sources":["syncTelemetry.ts"],"names":[],"mappings":"AAkGA,MAAM,uBAAuB,GAAkB,UAAU,CAAC;AAC1D,MAAM,oBAAoB,GAAkC;IAC1D,UAAU,EAAE,CAAC;IACb,QAAQ,EAAE,CAAC;IACX,IAAI,EAAE,CAAC;CACR,CAAC;AAYF,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,sBAAsB,CACpC,SAAsC,EACtC,UAAkB;IAElB,OAAO;QACL,KAAK,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC;QAClC,MAAM,EAAE,UAAU;QAClB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,IAAI,CAAC,EAAE,UAAU,CAAC;QAClD,OAAO,EAAE,CAAC,SAAS,EAAE,OAAO,IAAI,CAAC,CAAC,GAAG,UAAU;KAChD,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAyB;IACrD,OAAO,oBAAoB,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,uBAAuB,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAwB,EACxB,KAAyB;IAEzB,MAAM,SAAS,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAC3E,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC,iBAAiB,CAAC;AAC1D,CAAC;AAED,SAAS,iBAAiB,CAAC,KAA6B;IACtD,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;QAC1B,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,sBAAsB,CAC7B,KAA6B;IAE7B,OAAO;QACL,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG;QACtC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,uBAAuB;QACpD,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI;QAChC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;QAC1C,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC;KACjC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAIxC;IACC,OAAO;QACL,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;aAC3B,IAAI,CAAC,oBAAoB,CAAC;aAC1B,GAAG,CAAC,sBAAsB,CAAC;QAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,SAAS,EAAE,mBAAmB,EAAE;KACjC,CAAC;AACJ,CAAC","sourcesContent":["export type SyncLanePhase = \"structural\" | \"document\" | \"blob\";\nexport type SyncLaneStatus =\n | \"idle\"\n | \"queued\"\n | \"running\"\n | \"complete\"\n | \"error\";\nexport type SyncLaneLastAction =\n | \"registered\"\n | \"requested\"\n | \"started\"\n | \"completed\"\n | \"failed\";\n\n/**\n * Fine-grained progress for lanes that expose it (currently multipart blob\n * uploads). Coarse status lanes leave this null and fall back to the\n * status-keyed progress bar in the visualizer.\n */\nexport interface SyncLaneProgress {\n bytesTotal: number;\n bytesUploaded: number;\n partsCompleted: number;\n partsTotal: number;\n}\n\n/**\n * Monotonic durations of a lane's settled runs. A run abandoned by its\n * watchdog is counted when it finally settles, with its real duration; the\n * watchdog verdict itself records none, since the run had not finished.\n */\nexport interface SyncLaneRunDurations {\n count: number;\n lastMs: number;\n maxMs: number;\n totalMs: number;\n}\n\nexport interface SyncLaneSnapshot {\n /** Storage key used to open the associated blob, when this is a blob lane. */\n blobStorageKey: string | null;\n errorCount: number;\n key: string;\n label: string;\n lastAction: SyncLaneLastAction;\n lastActionAt: string;\n lastCompletedAt: string | null;\n lastError: string | null;\n lastFailedAt: string | null;\n lastRequestedAt: string | null;\n lastStartedAt: string | null;\n phase: SyncLanePhase;\n progress: SyncLaneProgress | null;\n registrationIndex: number;\n requestCount: number;\n requested: boolean;\n runAbandoned: boolean;\n runCount: number;\n /** Null until a run settles. */\n runDurations: SyncLaneRunDurations | null;\n running: boolean;\n status: SyncLaneStatus;\n}\n\nexport interface DomainSyncSnapshot {\n hasPendingWork: boolean;\n lanes: ReadonlyArray<SyncLaneSnapshot>;\n pumpActive: boolean;\n updatedAt: string;\n}\n\ninterface SyncLaneTelemetryConfig {\n label?: string | undefined;\n phase?: SyncLanePhase | undefined;\n}\n\ninterface SyncLaneTelemetryState {\n blobStorageKey: string | null;\n config: SyncLaneTelemetryConfig;\n errorCount: number;\n key: string;\n lastAction: SyncLaneLastAction;\n lastActionAt: string;\n lastCompletedAt: string | null;\n lastError: string | null;\n lastFailedAt: string | null;\n lastRequestedAt: string | null;\n lastStartedAt: string | null;\n progress?: SyncLaneProgress | null;\n registrationIndex: number;\n requestCount: number;\n requested: boolean;\n runAbandoned: boolean;\n runCount: number;\n runDurations: SyncLaneRunDurations | null;\n running: boolean;\n}\n\nconst DEFAULT_SYNC_LANE_PHASE: SyncLanePhase = \"document\";\nconst SYNC_LANE_PHASE_RANK: Record<SyncLanePhase, number> = {\n structural: 0,\n document: 1,\n blob: 2,\n};\n\n/**\n * Structural subset of a sync lane needed to order it: its phase and the\n * registration index that breaks phase ties. Accepts both the coordinator's\n * `SyncLaneState` and the telemetry `SyncLaneTelemetryState`.\n */\ninterface SyncLaneOrderInput {\n config: { phase?: SyncLanePhase | undefined };\n registrationIndex: number;\n}\n\nexport function createSyncTimestamp(): string {\n return new Date().toISOString();\n}\n\n// Returns a new object, so a published snapshot can share it safely.\nexport function addSyncLaneRunDuration(\n durations: SyncLaneRunDurations | null,\n durationMs: number,\n): SyncLaneRunDurations {\n return {\n count: (durations?.count ?? 0) + 1,\n lastMs: durationMs,\n maxMs: Math.max(durations?.maxMs ?? 0, durationMs),\n totalMs: (durations?.totalMs ?? 0) + durationMs,\n };\n}\n\nfunction getSyncLanePhaseRank(state: SyncLaneOrderInput): number {\n return SYNC_LANE_PHASE_RANK[state.config.phase ?? DEFAULT_SYNC_LANE_PHASE];\n}\n\nexport function compareSyncLaneOrder(\n left: SyncLaneOrderInput,\n right: SyncLaneOrderInput,\n): number {\n const phaseRank = getSyncLanePhaseRank(left) - getSyncLanePhaseRank(right);\n if (phaseRank !== 0) {\n return phaseRank;\n }\n\n return left.registrationIndex - right.registrationIndex;\n}\n\nfunction getSyncLaneStatus(state: SyncLaneTelemetryState): SyncLaneStatus {\n if (state.running) {\n return \"running\";\n }\n\n if (state.requested) {\n return \"queued\";\n }\n\n if (state.lastAction === \"failed\") {\n return \"error\";\n }\n\n if (state.lastCompletedAt) {\n return \"complete\";\n }\n\n return \"idle\";\n}\n\nfunction createSyncLaneSnapshot(\n state: SyncLaneTelemetryState,\n): SyncLaneSnapshot {\n return {\n blobStorageKey: state.blobStorageKey,\n errorCount: state.errorCount,\n key: state.key,\n label: state.config.label ?? state.key,\n lastAction: state.lastAction,\n lastActionAt: state.lastActionAt,\n lastCompletedAt: state.lastCompletedAt,\n lastError: state.lastError,\n lastFailedAt: state.lastFailedAt,\n lastRequestedAt: state.lastRequestedAt,\n lastStartedAt: state.lastStartedAt,\n phase: state.config.phase ?? DEFAULT_SYNC_LANE_PHASE,\n progress: state.progress ?? null,\n registrationIndex: state.registrationIndex,\n requestCount: state.requestCount,\n requested: state.requested,\n runAbandoned: state.runAbandoned,\n runCount: state.runCount,\n runDurations: state.runDurations,\n running: state.running,\n status: getSyncLaneStatus(state),\n };\n}\n\nexport function createDomainSyncSnapshot(input: {\n hasPendingWork: boolean;\n lanes: Iterable<SyncLaneTelemetryState>;\n pumpActive: boolean;\n}): DomainSyncSnapshot {\n return {\n hasPendingWork: input.hasPendingWork,\n lanes: Array.from(input.lanes)\n .sort(compareSyncLaneOrder)\n .map(createSyncLaneSnapshot),\n pumpActive: input.pumpActive,\n updatedAt: createSyncTimestamp(),\n };\n}\n"]}
@@ -1,5 +1,6 @@
1
+ import { readUserTimingClock } from "../shared/userTiming.js";
1
2
  import { describeSyncLaneError, INITIAL_SYNC_LANE_RUN_STATE, noopLaneRun, publishSyncCoordinatorSnapshot, } from "./coordinatorState.js";
2
- import { createSyncTimestamp } from "./syncTelemetry.js";
3
+ import { addSyncLaneRunDuration, createSyncTimestamp } from "./syncTelemetry.js";
3
4
  // Completed/failed upload lanes are retained so a finished upload stays visible
4
5
  // in the visualizer, but capped so a long-lived session can't grow `lanes`
5
6
  // without bound (one upload lane is created per attachment slot upload).
@@ -20,6 +21,21 @@ function evictRetainedUploadLanes(coordinatorState) {
20
21
  coordinatorState.lanes.delete(lane.key);
21
22
  }
22
23
  }
24
+ // Returns the session's run-ending step. Upload rows are timed like pump runs,
25
+ // once per session, but stay off the User Timing tracks: the owning document
26
+ // lane's run already spans them.
27
+ function startUploadLaneRun(lane) {
28
+ const startedAtMs = readUserTimingClock();
29
+ let timed = false;
30
+ return () => {
31
+ lane.running = false;
32
+ if (timed) {
33
+ return;
34
+ }
35
+ timed = true;
36
+ lane.runDurations = addSyncLaneRunDuration(lane.runDurations, readUserTimingClock() - startedAtMs);
37
+ };
38
+ }
23
39
  export function beginUploadLane(coordinatorState, key, options) {
24
40
  const startedAt = createSyncTimestamp();
25
41
  let lane = coordinatorState.lanes.get(key);
@@ -70,12 +86,13 @@ export function beginUploadLane(coordinatorState, key, options) {
70
86
  // callbacks can't clobber the newer upload's state.
71
87
  const sessionRunCount = uploadLane.runCount;
72
88
  const isCurrentSession = () => uploadLane.runCount === sessionRunCount;
89
+ const endRun = startUploadLaneRun(uploadLane);
73
90
  return {
74
91
  complete() {
75
92
  if (!isCurrentSession()) {
76
93
  return;
77
94
  }
78
- uploadLane.running = false;
95
+ endRun();
79
96
  uploadLane.lastAction = "completed";
80
97
  uploadLane.lastActionAt = createSyncTimestamp();
81
98
  uploadLane.lastCompletedAt = uploadLane.lastActionAt;
@@ -94,7 +111,7 @@ export function beginUploadLane(coordinatorState, key, options) {
94
111
  if (!isCurrentSession()) {
95
112
  return;
96
113
  }
97
- uploadLane.running = false;
114
+ endRun();
98
115
  uploadLane.errorCount += 1;
99
116
  uploadLane.lastAction = "failed";
100
117
  uploadLane.lastActionAt = createSyncTimestamp();
@@ -1 +1 @@
1
- {"version":3,"file":"uploadLane.js","sourceRoot":"","sources":["uploadLane.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DomainSyncCoordinatorState,\n SyncLaneState,\n} from \"./coordinatorState\";\nimport {\n describeSyncLaneError,\n INITIAL_SYNC_LANE_RUN_STATE,\n noopLaneRun,\n publishSyncCoordinatorSnapshot,\n} from \"./coordinatorState\";\nimport type { SyncLaneProgress } from \"./syncTelemetry\";\nimport { createSyncTimestamp } from \"./syncTelemetry\";\n\n/**\n * Handle for an observational upload lane. Unlike pump-driven lanes, these are\n * never `requested` — the upload path that owns the handle drives the lane's\n * running/progress/terminal state directly. The coordinator pump ignores them\n * because `selectNextRequestedLane` only ever picks `requested` lanes.\n */\nexport interface UploadSyncLane {\n reportProgress: (progress: SyncLaneProgress) => void;\n complete: () => void;\n fail: (error: unknown) => void;\n}\n\nexport interface UploadSyncLaneOptions {\n blobStorageKey?: string | undefined;\n label?: string | undefined;\n}\n\n// Completed/failed upload lanes are retained so a finished upload stays visible\n// in the visualizer, but capped so a long-lived session can't grow `lanes`\n// without bound (one upload lane is created per attachment slot upload).\nconst MAX_RETAINED_UPLOAD_LANES = 10;\n\nfunction evictRetainedUploadLanes(\n coordinatorState: DomainSyncCoordinatorState,\n) {\n const settledUploadLanes: SyncLaneState[] = [];\n for (const lane of coordinatorState.lanes.values()) {\n if (lane.config.phase === \"blob\" && !lane.running) {\n settledUploadLanes.push(lane);\n }\n }\n\n if (settledUploadLanes.length <= MAX_RETAINED_UPLOAD_LANES) {\n return;\n }\n\n settledUploadLanes.sort(\n (left, right) => left.registrationIndex - right.registrationIndex,\n );\n const removeCount = settledUploadLanes.length - MAX_RETAINED_UPLOAD_LANES;\n for (const lane of settledUploadLanes.slice(0, removeCount)) {\n coordinatorState.lanes.delete(lane.key);\n }\n}\n\nexport function beginUploadLane(\n coordinatorState: DomainSyncCoordinatorState,\n key: string,\n options: UploadSyncLaneOptions,\n): UploadSyncLane {\n const startedAt = createSyncTimestamp();\n let lane = coordinatorState.lanes.get(key);\n if (lane) {\n lane.config = { label: options.label, phase: \"blob\", run: noopLaneRun };\n lane.blobStorageKey = options.blobStorageKey ?? null;\n } else {\n lane = {\n ...INITIAL_SYNC_LANE_RUN_STATE,\n blobStorageKey: options.blobStorageKey ?? null,\n config: { label: options.label, phase: \"blob\", run: noopLaneRun },\n errorCount: 0,\n key,\n lastAction: \"started\",\n lastActionAt: startedAt,\n lastCompletedAt: null,\n lastError: null,\n lastFailedAt: null,\n lastRequestedAt: null,\n lastStartedAt: startedAt,\n notBeforeAtMs: null,\n progress: null,\n pumpDriven: false,\n registrationIndex: coordinatorState.nextRegistrationIndex,\n requestCount: 0,\n requested: false,\n };\n coordinatorState.nextRegistrationIndex += 1;\n coordinatorState.lanes.set(key, lane);\n }\n\n const uploadLane = lane;\n // A stale executable handle may share this key. Make the upload row\n // observational before publishing and discard any queued pump request.\n uploadLane.pumpDriven = false;\n uploadLane.requested = false;\n uploadLane.running = true;\n uploadLane.runCount += 1;\n uploadLane.lastAction = \"started\";\n uploadLane.lastActionAt = startedAt;\n uploadLane.lastStartedAt = startedAt;\n uploadLane.lastError = null;\n uploadLane.progress = null;\n evictRetainedUploadLanes(coordinatorState);\n publishSyncCoordinatorSnapshot(coordinatorState);\n\n // If this lane key is re-begun (or evicted and recreated), runCount advances\n // and a stale handle from the prior session becomes a no-op, so out-of-order\n // callbacks can't clobber the newer upload's state.\n const sessionRunCount = uploadLane.runCount;\n const isCurrentSession = () => uploadLane.runCount === sessionRunCount;\n\n return {\n complete() {\n if (!isCurrentSession()) {\n return;\n }\n uploadLane.running = false;\n uploadLane.lastAction = \"completed\";\n uploadLane.lastActionAt = createSyncTimestamp();\n uploadLane.lastCompletedAt = uploadLane.lastActionAt;\n uploadLane.lastError = null;\n if (uploadLane.progress) {\n uploadLane.progress = {\n ...uploadLane.progress,\n bytesUploaded: uploadLane.progress.bytesTotal,\n partsCompleted: uploadLane.progress.partsTotal,\n };\n }\n evictRetainedUploadLanes(coordinatorState);\n publishSyncCoordinatorSnapshot(coordinatorState);\n },\n fail(error: unknown) {\n if (!isCurrentSession()) {\n return;\n }\n uploadLane.running = false;\n uploadLane.errorCount += 1;\n uploadLane.lastAction = \"failed\";\n uploadLane.lastActionAt = createSyncTimestamp();\n uploadLane.lastError = describeSyncLaneError(error);\n uploadLane.lastFailedAt = uploadLane.lastActionAt;\n evictRetainedUploadLanes(coordinatorState);\n publishSyncCoordinatorSnapshot(coordinatorState);\n },\n reportProgress(progress: SyncLaneProgress) {\n if (!isCurrentSession()) {\n return;\n }\n uploadLane.progress = { ...progress };\n uploadLane.lastActionAt = createSyncTimestamp();\n publishSyncCoordinatorSnapshot(coordinatorState);\n },\n };\n}\n"]}
1
+ {"version":3,"file":"uploadLane.js","sourceRoot":"","sources":["uploadLane.ts"],"names":[],"mappings":"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","sourcesContent":["import { readUserTimingClock } from \"../shared/userTiming\";\nimport type {\n DomainSyncCoordinatorState,\n SyncLaneState,\n} from \"./coordinatorState\";\nimport {\n describeSyncLaneError,\n INITIAL_SYNC_LANE_RUN_STATE,\n noopLaneRun,\n publishSyncCoordinatorSnapshot,\n} from \"./coordinatorState\";\nimport type { SyncLaneProgress } from \"./syncTelemetry\";\nimport { addSyncLaneRunDuration, createSyncTimestamp } from \"./syncTelemetry\";\n\n/**\n * Handle for an observational upload lane. Unlike pump-driven lanes, these are\n * never `requested` — the upload path that owns the handle drives the lane's\n * running/progress/terminal state directly. The coordinator pump ignores them\n * because `selectNextRequestedLane` only ever picks `requested` lanes.\n */\nexport interface UploadSyncLane {\n reportProgress: (progress: SyncLaneProgress) => void;\n complete: () => void;\n fail: (error: unknown) => void;\n}\n\nexport interface UploadSyncLaneOptions {\n blobStorageKey?: string | undefined;\n label?: string | undefined;\n}\n\n// Completed/failed upload lanes are retained so a finished upload stays visible\n// in the visualizer, but capped so a long-lived session can't grow `lanes`\n// without bound (one upload lane is created per attachment slot upload).\nconst MAX_RETAINED_UPLOAD_LANES = 10;\n\nfunction evictRetainedUploadLanes(\n coordinatorState: DomainSyncCoordinatorState,\n) {\n const settledUploadLanes: SyncLaneState[] = [];\n for (const lane of coordinatorState.lanes.values()) {\n if (lane.config.phase === \"blob\" && !lane.running) {\n settledUploadLanes.push(lane);\n }\n }\n\n if (settledUploadLanes.length <= MAX_RETAINED_UPLOAD_LANES) {\n return;\n }\n\n settledUploadLanes.sort(\n (left, right) => left.registrationIndex - right.registrationIndex,\n );\n const removeCount = settledUploadLanes.length - MAX_RETAINED_UPLOAD_LANES;\n for (const lane of settledUploadLanes.slice(0, removeCount)) {\n coordinatorState.lanes.delete(lane.key);\n }\n}\n\n// Returns the session's run-ending step. Upload rows are timed like pump runs,\n// once per session, but stay off the User Timing tracks: the owning document\n// lane's run already spans them.\nfunction startUploadLaneRun(lane: SyncLaneState): () => void {\n const startedAtMs = readUserTimingClock();\n let timed = false;\n return () => {\n lane.running = false;\n if (timed) {\n return;\n }\n timed = true;\n lane.runDurations = addSyncLaneRunDuration(\n lane.runDurations,\n readUserTimingClock() - startedAtMs,\n );\n };\n}\n\nexport function beginUploadLane(\n coordinatorState: DomainSyncCoordinatorState,\n key: string,\n options: UploadSyncLaneOptions,\n): UploadSyncLane {\n const startedAt = createSyncTimestamp();\n let lane = coordinatorState.lanes.get(key);\n if (lane) {\n lane.config = { label: options.label, phase: \"blob\", run: noopLaneRun };\n lane.blobStorageKey = options.blobStorageKey ?? null;\n } else {\n lane = {\n ...INITIAL_SYNC_LANE_RUN_STATE,\n blobStorageKey: options.blobStorageKey ?? null,\n config: { label: options.label, phase: \"blob\", run: noopLaneRun },\n errorCount: 0,\n key,\n lastAction: \"started\",\n lastActionAt: startedAt,\n lastCompletedAt: null,\n lastError: null,\n lastFailedAt: null,\n lastRequestedAt: null,\n lastStartedAt: startedAt,\n notBeforeAtMs: null,\n progress: null,\n pumpDriven: false,\n registrationIndex: coordinatorState.nextRegistrationIndex,\n requestCount: 0,\n requested: false,\n };\n coordinatorState.nextRegistrationIndex += 1;\n coordinatorState.lanes.set(key, lane);\n }\n\n const uploadLane = lane;\n // A stale executable handle may share this key. Make the upload row\n // observational before publishing and discard any queued pump request.\n uploadLane.pumpDriven = false;\n uploadLane.requested = false;\n uploadLane.running = true;\n uploadLane.runCount += 1;\n uploadLane.lastAction = \"started\";\n uploadLane.lastActionAt = startedAt;\n uploadLane.lastStartedAt = startedAt;\n uploadLane.lastError = null;\n uploadLane.progress = null;\n evictRetainedUploadLanes(coordinatorState);\n publishSyncCoordinatorSnapshot(coordinatorState);\n\n // If this lane key is re-begun (or evicted and recreated), runCount advances\n // and a stale handle from the prior session becomes a no-op, so out-of-order\n // callbacks can't clobber the newer upload's state.\n const sessionRunCount = uploadLane.runCount;\n const isCurrentSession = () => uploadLane.runCount === sessionRunCount;\n const endRun = startUploadLaneRun(uploadLane);\n\n return {\n complete() {\n if (!isCurrentSession()) {\n return;\n }\n endRun();\n uploadLane.lastAction = \"completed\";\n uploadLane.lastActionAt = createSyncTimestamp();\n uploadLane.lastCompletedAt = uploadLane.lastActionAt;\n uploadLane.lastError = null;\n if (uploadLane.progress) {\n uploadLane.progress = {\n ...uploadLane.progress,\n bytesUploaded: uploadLane.progress.bytesTotal,\n partsCompleted: uploadLane.progress.partsTotal,\n };\n }\n evictRetainedUploadLanes(coordinatorState);\n publishSyncCoordinatorSnapshot(coordinatorState);\n },\n fail(error: unknown) {\n if (!isCurrentSession()) {\n return;\n }\n endRun();\n uploadLane.errorCount += 1;\n uploadLane.lastAction = \"failed\";\n uploadLane.lastActionAt = createSyncTimestamp();\n uploadLane.lastError = describeSyncLaneError(error);\n uploadLane.lastFailedAt = uploadLane.lastActionAt;\n evictRetainedUploadLanes(coordinatorState);\n publishSyncCoordinatorSnapshot(coordinatorState);\n },\n reportProgress(progress: SyncLaneProgress) {\n if (!isCurrentSession()) {\n return;\n }\n uploadLane.progress = { ...progress };\n uploadLane.lastActionAt = createSyncTimestamp();\n publishSyncCoordinatorSnapshot(coordinatorState);\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tearleads/client-sdk",
3
- "version": "0.1.71",
3
+ "version": "0.1.72",
4
4
  "description": "React-free Tearleads client runtime: local SQLite persistence, keys, sync, encrypted blobs, and domain workflows.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {