@powerhousedao/reactor-attachments 6.2.3-dev.50 → 6.2.3-dev.51

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,262 @@
1
+ import { a as parseRef } from "./switchboard-attachment-transport-BH3ZAmxE.js";
2
+ import { generatorTypeDefs } from "@powerhousedao/document-engineering/graphql";
3
+ import { constantCase, pascalCase } from "change-case";
4
+ import { Kind, buildASTSchema, getNamedType, isInputObjectType, isListType, isNonNullType, parse, print } from "graphql";
5
+ //#region src/reference-index/attachment-schema-compiler.ts
6
+ const ATTACHMENT_REF_TYPE = "AttachmentRef";
7
+ const CODEGEN_SCALAR_NAMES = /* @__PURE__ */ new Set([
8
+ "Unknown",
9
+ "DateTime",
10
+ "Address",
11
+ ATTACHMENT_REF_TYPE,
12
+ ...Object.keys(generatorTypeDefs)
13
+ ]);
14
+ function describeContext(context) {
15
+ return `document type "${context.documentType}", version ${context.version}, action "${context.actionType}"`;
16
+ }
17
+ function compilationError(context, reason) {
18
+ return /* @__PURE__ */ new Error(`Attachment schema compilation failed for ${describeContext(context)}: ${reason}`);
19
+ }
20
+ function extractionError(context, path, reason) {
21
+ return /* @__PURE__ */ new Error(`Attachment extraction failed for ${describeContext(context)} at ${path}: ${reason}`);
22
+ }
23
+ function applicableSpecification(module, context) {
24
+ const matches = module.documentModel.global.specifications.filter((specification) => specification.version === context.version);
25
+ if (matches.length !== 1) throw compilationError(context, matches.length === 0 ? "the module has no matching specification" : "the module has multiple matching specifications");
26
+ return matches[0];
27
+ }
28
+ function parseOperations(specification, context) {
29
+ return specification.modules.flatMap((moduleSpecification) => moduleSpecification.operations.map((operation) => {
30
+ if (operation.schema === null) return {
31
+ document: null,
32
+ operation
33
+ };
34
+ try {
35
+ return {
36
+ document: parse(operation.schema),
37
+ operation
38
+ };
39
+ } catch {
40
+ throw compilationError(context, "an operation has invalid GraphQL SDL");
41
+ }
42
+ }));
43
+ }
44
+ function selectOperation(operations, context) {
45
+ const matches = operations.filter(({ operation }) => operation.name !== null && constantCase(operation.name) === context.actionType);
46
+ if (matches.length > 1) throw compilationError(context, "multiple operations map to the action");
47
+ if (matches.length === 0) return null;
48
+ return matches[0];
49
+ }
50
+ function buildEffectiveSchema(specification, context) {
51
+ const scalarSchemas = Array.from(CODEGEN_SCALAR_NAMES, (name) => `scalar ${name}`);
52
+ const stateSchemas = Object.values(specification.state).map((state) => state.schema);
53
+ const operationSchemas = specification.modules.flatMap((moduleSpecification) => moduleSpecification.operations.flatMap((operation) => operation.schema === null ? [] : [operation.schema]));
54
+ try {
55
+ const document = parse([
56
+ ...scalarSchemas,
57
+ ...stateSchemas,
58
+ ...operationSchemas
59
+ ].filter(Boolean).join("\n\n"));
60
+ return buildASTSchema(dedupeTypeDefinitions(document));
61
+ } catch {
62
+ throw compilationError(context, "the effective GraphQL schema is invalid");
63
+ }
64
+ }
65
+ function dedupeTypeDefinitions(document) {
66
+ const seen = /* @__PURE__ */ new Map();
67
+ const definitions = document.definitions.filter((definition) => {
68
+ if (!("name" in definition) || definition.name?.value === void 0 || definition.kind === Kind.SCALAR_TYPE_DEFINITION) return true;
69
+ const name = definition.name.value;
70
+ const printed = print(definition);
71
+ const existing = seen.get(name);
72
+ if (existing === void 0) {
73
+ seen.set(name, printed);
74
+ return true;
75
+ }
76
+ return existing !== printed;
77
+ });
78
+ return {
79
+ ...document,
80
+ definitions
81
+ };
82
+ }
83
+ function attachmentReachableTypes(definitions) {
84
+ const reachable = /* @__PURE__ */ new Set();
85
+ let changed = true;
86
+ while (changed) {
87
+ changed = false;
88
+ for (const [name, definition] of definitions) {
89
+ if (reachable.has(name)) continue;
90
+ if (Object.values(definition.getFields()).some((field) => {
91
+ const typeName = getNamedType(field.type).name;
92
+ return typeName === ATTACHMENT_REF_TYPE || definitions.has(typeName) && reachable.has(typeName);
93
+ })) {
94
+ reachable.add(name);
95
+ changed = true;
96
+ }
97
+ }
98
+ }
99
+ return reachable;
100
+ }
101
+ function compileValuePlan(type, objectPlans) {
102
+ if (isNonNullType(type)) return {
103
+ ...compileValuePlan(type.ofType, objectPlans),
104
+ required: true
105
+ };
106
+ if (isListType(type)) return {
107
+ item: compileValuePlan(type.ofType, objectPlans),
108
+ kind: "list",
109
+ required: false
110
+ };
111
+ const typeName = type.name;
112
+ if (typeName === ATTACHMENT_REF_TYPE) return {
113
+ kind: "attachment",
114
+ required: false
115
+ };
116
+ const body = objectPlans.get(typeName);
117
+ if (!body) throw new Error(`Internal attachment schema plan error for ${typeName}`);
118
+ return {
119
+ body,
120
+ kind: "object",
121
+ required: false
122
+ };
123
+ }
124
+ function compileRootPlan(rootName, definitions) {
125
+ const reachable = attachmentReachableTypes(definitions);
126
+ if (!reachable.has(rootName)) return null;
127
+ const objectPlans = /* @__PURE__ */ new Map();
128
+ for (const name of reachable) objectPlans.set(name, { fields: [] });
129
+ for (const name of reachable) {
130
+ const definition = definitions.get(name);
131
+ const body = objectPlans.get(name);
132
+ if (!definition || !body) continue;
133
+ for (const field of Object.values(definition.getFields())) {
134
+ const typeName = getNamedType(field.type).name;
135
+ if (typeName !== ATTACHMENT_REF_TYPE && !reachable.has(typeName)) continue;
136
+ body.fields.push({
137
+ hasDefault: field.defaultValue !== void 0,
138
+ name: field.name,
139
+ value: compileValuePlan(field.type, objectPlans)
140
+ });
141
+ }
142
+ }
143
+ return objectPlans.get(rootName) ?? null;
144
+ }
145
+ function readOwnField(value, fieldName, context, path) {
146
+ try {
147
+ if (!Object.prototype.hasOwnProperty.call(value, fieldName)) return {
148
+ present: false,
149
+ value: void 0
150
+ };
151
+ return {
152
+ present: true,
153
+ value: value[fieldName]
154
+ };
155
+ } catch {
156
+ throw extractionError(context, path, "the declared field cannot be read");
157
+ }
158
+ }
159
+ function extractValue(plan, value, path, context, refs, seenRefs, activeObjects) {
160
+ if (value === null || value === void 0) {
161
+ if (plan.required) throw extractionError(context, path, "a required value is missing or null");
162
+ return;
163
+ }
164
+ if (plan.kind === "attachment") {
165
+ if (typeof value !== "string") throw extractionError(context, path, "expected an AttachmentRef string");
166
+ try {
167
+ parseRef(value);
168
+ } catch {
169
+ throw extractionError(context, path, "the AttachmentRef is malformed");
170
+ }
171
+ if (!seenRefs.has(value)) {
172
+ seenRefs.add(value);
173
+ refs.push(value);
174
+ }
175
+ return;
176
+ }
177
+ if (plan.kind === "list") {
178
+ if (!Array.isArray(value)) throw extractionError(context, path, "expected a list");
179
+ for (let index = 0; index < value.length; index += 1) extractValue(plan.item, value[index], `${path}[${index}]`, context, refs, seenRefs, activeObjects);
180
+ return;
181
+ }
182
+ if (typeof value !== "object" || Array.isArray(value)) throw extractionError(context, path, "expected an input object");
183
+ if (activeObjects.has(value)) throw extractionError(context, path, "the input value contains a cycle");
184
+ activeObjects.add(value);
185
+ try {
186
+ const record = value;
187
+ for (const field of plan.body.fields) {
188
+ const fieldPath = `${path}.${field.name}`;
189
+ const result = readOwnField(record, field.name, context, fieldPath);
190
+ if ((!result.present || result.value === void 0) && field.hasDefault) continue;
191
+ extractValue(field.value, result.value, fieldPath, context, refs, seenRefs, activeObjects);
192
+ }
193
+ } finally {
194
+ activeObjects.delete(value);
195
+ }
196
+ }
197
+ var SchemaCompiledAttachmentExtractor = class {
198
+ context;
199
+ rootPlan;
200
+ constructor(context, rootPlan) {
201
+ this.context = context;
202
+ this.rootPlan = rootPlan;
203
+ }
204
+ extract(action) {
205
+ if (action.type !== this.context.actionType) throw extractionError(this.context, "input", "the action type does not match the compiled schema");
206
+ if (!this.rootPlan) return [];
207
+ if (action.input === null || typeof action.input !== "object" || Array.isArray(action.input)) throw extractionError(this.context, "input", "expected an input object");
208
+ const refs = [];
209
+ const seenRefs = /* @__PURE__ */ new Set();
210
+ const activeObjects = /* @__PURE__ */ new WeakSet();
211
+ activeObjects.add(action.input);
212
+ try {
213
+ const input = action.input;
214
+ for (const field of this.rootPlan.fields) {
215
+ const path = `input.${field.name}`;
216
+ const result = readOwnField(input, field.name, this.context, path);
217
+ if ((!result.present || result.value === void 0) && field.hasDefault) continue;
218
+ extractValue(field.value, result.value, path, this.context, refs, seenRefs, activeObjects);
219
+ }
220
+ } finally {
221
+ activeObjects.delete(action.input);
222
+ }
223
+ return refs;
224
+ }
225
+ };
226
+ function compileExtractor(module, actionType) {
227
+ const context = {
228
+ actionType,
229
+ documentType: module.documentModel.global.id,
230
+ version: module.version ?? 1
231
+ };
232
+ const specification = applicableSpecification(module, context);
233
+ const selected = selectOperation(parseOperations(specification, context), context);
234
+ if (selected === null || selected.operation.schema === null) return new SchemaCompiledAttachmentExtractor(context, null);
235
+ const effectiveSchema = buildEffectiveSchema(specification, context);
236
+ const operationName = selected.operation.name;
237
+ if (operationName === null) throw compilationError(context, "the operation has no name");
238
+ const rootName = `${pascalCase(operationName)}Input`;
239
+ const definitions = /* @__PURE__ */ new Map();
240
+ for (const type of Object.values(effectiveSchema.getTypeMap())) if (isInputObjectType(type) && !type.name.startsWith("__")) definitions.set(type.name, type);
241
+ if (!(selected.document?.definitions.filter((definition) => (definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION || definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION) && definition.name.value === rootName))?.length || !definitions.has(rootName)) throw compilationError(context, `the operation does not declare its expected root input "${rootName}"`);
242
+ return new SchemaCompiledAttachmentExtractor(context, compileRootPlan(rootName, definitions));
243
+ }
244
+ var AttachmentSchemaCompiler = class {
245
+ cache = /* @__PURE__ */ new WeakMap();
246
+ forModuleAction(module, actionType) {
247
+ let moduleCache = this.cache.get(module);
248
+ if (!moduleCache) {
249
+ moduleCache = /* @__PURE__ */ new Map();
250
+ this.cache.set(module, moduleCache);
251
+ }
252
+ const cached = moduleCache.get(actionType);
253
+ if (cached) return cached;
254
+ const compiled = compileExtractor(module, actionType);
255
+ moduleCache.set(actionType, compiled);
256
+ return compiled;
257
+ }
258
+ };
259
+ //#endregion
260
+ export { AttachmentSchemaCompiler as t };
261
+
262
+ //# sourceMappingURL=attachment-schema-compiler-rIaJpiHO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-schema-compiler-rIaJpiHO.js","names":[],"sources":["../src/reference-index/attachment-schema-compiler.ts"],"sourcesContent":["import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport { generatorTypeDefs } from \"@powerhousedao/document-engineering/graphql\";\nimport type {\n Action,\n DocumentModelModule,\n DocumentSpecification,\n OperationSpecification,\n} from \"@powerhousedao/shared/document-model\";\nimport { constantCase, pascalCase } from \"change-case\";\nimport {\n buildASTSchema,\n getNamedType,\n isInputObjectType,\n isListType,\n isNonNullType,\n Kind,\n parse,\n print,\n type DocumentNode,\n type GraphQLInputObjectType,\n type GraphQLInputType,\n type GraphQLSchema,\n} from \"graphql\";\nimport { parseRef } from \"../ref.js\";\nimport type {\n CompiledAttachmentExtractor,\n IAttachmentSchemaCompiler,\n} from \"./types.js\";\n\nconst ATTACHMENT_REF_TYPE = \"AttachmentRef\";\nconst CODEGEN_SCALAR_NAMES = new Set([\n \"Unknown\",\n \"DateTime\",\n \"Address\",\n ATTACHMENT_REF_TYPE,\n ...Object.keys(generatorTypeDefs as Record<string, string>),\n]);\n\ntype CompilerContext = {\n actionType: string;\n documentType: string;\n version: number;\n};\n\ntype ParsedOperation = {\n document: DocumentNode | null;\n operation: OperationSpecification;\n};\n\ntype ObjectPlan = {\n fields: FieldPlan[];\n};\n\ntype FieldPlan = {\n hasDefault: boolean;\n name: string;\n value: ValuePlan;\n};\n\ntype ReadFieldResult =\n | { present: false; value: undefined }\n | { present: true; value: unknown };\n\ntype ValuePlan =\n | {\n kind: \"attachment\";\n required: boolean;\n }\n | {\n item: ValuePlan;\n kind: \"list\";\n required: boolean;\n }\n | {\n body: ObjectPlan;\n kind: \"object\";\n required: boolean;\n };\n\nfunction describeContext(context: CompilerContext): string {\n return `document type \"${context.documentType}\", version ${context.version}, action \"${context.actionType}\"`;\n}\n\nfunction compilationError(context: CompilerContext, reason: string): Error {\n return new Error(\n `Attachment schema compilation failed for ${describeContext(context)}: ${reason}`,\n );\n}\n\nfunction extractionError(\n context: CompilerContext,\n path: string,\n reason: string,\n): Error {\n return new Error(\n `Attachment extraction failed for ${describeContext(context)} at ${path}: ${reason}`,\n );\n}\n\nfunction applicableSpecification(\n module: DocumentModelModule,\n context: CompilerContext,\n): DocumentSpecification {\n const matches = module.documentModel.global.specifications.filter(\n (specification) => specification.version === context.version,\n );\n if (matches.length !== 1) {\n throw compilationError(\n context,\n matches.length === 0\n ? \"the module has no matching specification\"\n : \"the module has multiple matching specifications\",\n );\n }\n return matches[0];\n}\n\nfunction parseOperations(\n specification: DocumentSpecification,\n context: CompilerContext,\n): ParsedOperation[] {\n return specification.modules.flatMap((moduleSpecification) =>\n moduleSpecification.operations.map((operation) => {\n if (operation.schema === null) return { document: null, operation };\n try {\n return { document: parse(operation.schema), operation };\n } catch {\n throw compilationError(context, \"an operation has invalid GraphQL SDL\");\n }\n }),\n );\n}\n\nfunction selectOperation(\n operations: ParsedOperation[],\n context: CompilerContext,\n): ParsedOperation | null {\n const matches = operations.filter(\n ({ operation }) =>\n operation.name !== null &&\n constantCase(operation.name) === context.actionType,\n );\n if (matches.length > 1) {\n throw compilationError(context, \"multiple operations map to the action\");\n }\n // Base/system actions (document creation, renames, undo) are not part of a\n // model's specification, so they cannot declare AttachmentRef fields.\n // They compile to the no-reference fast path instead of failing the stream.\n if (matches.length === 0) return null;\n return matches[0];\n}\n\nfunction buildEffectiveSchema(\n specification: DocumentSpecification,\n context: CompilerContext,\n): GraphQLSchema {\n const scalarSchemas = Array.from(\n CODEGEN_SCALAR_NAMES,\n (name) => `scalar ${name}`,\n );\n const stateSchemas = Object.values(specification.state).map(\n (state) => state.schema,\n );\n const operationSchemas = specification.modules.flatMap(\n (moduleSpecification) =>\n moduleSpecification.operations.flatMap((operation) =>\n operation.schema === null ? [] : [operation.schema],\n ),\n );\n\n try {\n const document = parse(\n [...scalarSchemas, ...stateSchemas, ...operationSchemas]\n .filter(Boolean)\n .join(\"\\n\\n\"),\n );\n return buildASTSchema(dedupeTypeDefinitions(document));\n } catch {\n throw compilationError(context, \"the effective GraphQL schema is invalid\");\n }\n}\n\n// Collapse identical repeats of a type (state schema vs operation schemas).\n// Conflicting or scalar duplicates stay in so the build still rejects them.\nfunction dedupeTypeDefinitions(document: DocumentNode): DocumentNode {\n const seen = new Map<string, string>();\n const definitions = document.definitions.filter((definition) => {\n if (\n !(\"name\" in definition) ||\n definition.name?.value === undefined ||\n definition.kind === Kind.SCALAR_TYPE_DEFINITION\n ) {\n return true;\n }\n const name = definition.name.value;\n const printed = print(definition);\n const existing = seen.get(name);\n if (existing === undefined) {\n seen.set(name, printed);\n return true;\n }\n return existing !== printed;\n });\n return { ...document, definitions };\n}\n\nfunction attachmentReachableTypes(\n definitions: Map<string, GraphQLInputObjectType>,\n): Set<string> {\n const reachable = new Set<string>();\n let changed = true;\n\n while (changed) {\n changed = false;\n for (const [name, definition] of definitions) {\n if (reachable.has(name)) continue;\n const reachesAttachment = Object.values(definition.getFields()).some(\n (field) => {\n const typeName = getNamedType(field.type).name;\n return (\n typeName === ATTACHMENT_REF_TYPE ||\n (definitions.has(typeName) && reachable.has(typeName))\n );\n },\n );\n if (reachesAttachment) {\n reachable.add(name);\n changed = true;\n }\n }\n }\n\n return reachable;\n}\n\nfunction compileValuePlan(\n type: GraphQLInputType,\n objectPlans: Map<string, ObjectPlan>,\n): ValuePlan {\n if (isNonNullType(type)) {\n return { ...compileValuePlan(type.ofType, objectPlans), required: true };\n }\n if (isListType(type)) {\n return {\n item: compileValuePlan(type.ofType, objectPlans),\n kind: \"list\",\n required: false,\n };\n }\n\n const typeName = type.name;\n if (typeName === ATTACHMENT_REF_TYPE) {\n return { kind: \"attachment\", required: false };\n }\n const body = objectPlans.get(typeName);\n if (!body) {\n throw new Error(`Internal attachment schema plan error for ${typeName}`);\n }\n return { body, kind: \"object\", required: false };\n}\n\nfunction compileRootPlan(\n rootName: string,\n definitions: Map<string, GraphQLInputObjectType>,\n): ObjectPlan | null {\n const reachable = attachmentReachableTypes(definitions);\n if (!reachable.has(rootName)) return null;\n\n const objectPlans = new Map<string, ObjectPlan>();\n for (const name of reachable) objectPlans.set(name, { fields: [] });\n\n for (const name of reachable) {\n const definition = definitions.get(name);\n const body = objectPlans.get(name);\n if (!definition || !body) continue;\n for (const field of Object.values(definition.getFields())) {\n const typeName = getNamedType(field.type).name;\n if (typeName !== ATTACHMENT_REF_TYPE && !reachable.has(typeName)) {\n continue;\n }\n body.fields.push({\n hasDefault: field.defaultValue !== undefined,\n name: field.name,\n value: compileValuePlan(field.type, objectPlans),\n });\n }\n }\n\n return objectPlans.get(rootName) ?? null;\n}\n\nfunction readOwnField(\n value: Record<string, unknown>,\n fieldName: string,\n context: CompilerContext,\n path: string,\n): ReadFieldResult {\n try {\n if (!Object.prototype.hasOwnProperty.call(value, fieldName)) {\n return { present: false, value: undefined };\n }\n return { present: true, value: value[fieldName] };\n } catch {\n throw extractionError(context, path, \"the declared field cannot be read\");\n }\n}\n\nfunction extractValue(\n plan: ValuePlan,\n value: unknown,\n path: string,\n context: CompilerContext,\n refs: AttachmentRef[],\n seenRefs: Set<string>,\n activeObjects: WeakSet<object>,\n): void {\n if (value === null || value === undefined) {\n if (plan.required) {\n throw extractionError(\n context,\n path,\n \"a required value is missing or null\",\n );\n }\n return;\n }\n\n if (plan.kind === \"attachment\") {\n if (typeof value !== \"string\") {\n throw extractionError(context, path, \"expected an AttachmentRef string\");\n }\n try {\n parseRef(value as AttachmentRef);\n } catch {\n throw extractionError(context, path, \"the AttachmentRef is malformed\");\n }\n if (!seenRefs.has(value)) {\n seenRefs.add(value);\n refs.push(value as AttachmentRef);\n }\n return;\n }\n\n if (plan.kind === \"list\") {\n if (!Array.isArray(value)) {\n throw extractionError(context, path, \"expected a list\");\n }\n for (let index = 0; index < value.length; index += 1) {\n extractValue(\n plan.item,\n value[index],\n `${path}[${index}]`,\n context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n return;\n }\n\n if (typeof value !== \"object\" || Array.isArray(value)) {\n throw extractionError(context, path, \"expected an input object\");\n }\n if (activeObjects.has(value)) {\n throw extractionError(context, path, \"the input value contains a cycle\");\n }\n\n activeObjects.add(value);\n try {\n const record = value as Record<string, unknown>;\n for (const field of plan.body.fields) {\n const fieldPath = `${path}.${field.name}`;\n const result = readOwnField(record, field.name, context, fieldPath);\n if ((!result.present || result.value === undefined) && field.hasDefault) {\n continue;\n }\n extractValue(\n field.value,\n result.value,\n fieldPath,\n context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n } finally {\n activeObjects.delete(value);\n }\n}\n\nclass SchemaCompiledAttachmentExtractor implements CompiledAttachmentExtractor {\n constructor(\n private readonly context: CompilerContext,\n private readonly rootPlan: ObjectPlan | null,\n ) {}\n\n extract(action: Action): AttachmentRef[] {\n if (action.type !== this.context.actionType) {\n throw extractionError(\n this.context,\n \"input\",\n \"the action type does not match the compiled schema\",\n );\n }\n if (!this.rootPlan) return [];\n if (\n action.input === null ||\n typeof action.input !== \"object\" ||\n Array.isArray(action.input)\n ) {\n throw extractionError(this.context, \"input\", \"expected an input object\");\n }\n\n const refs: AttachmentRef[] = [];\n const seenRefs = new Set<string>();\n const activeObjects = new WeakSet<object>();\n activeObjects.add(action.input);\n try {\n const input = action.input as Record<string, unknown>;\n for (const field of this.rootPlan.fields) {\n const path = `input.${field.name}`;\n const result = readOwnField(input, field.name, this.context, path);\n if (\n (!result.present || result.value === undefined) &&\n field.hasDefault\n ) {\n continue;\n }\n extractValue(\n field.value,\n result.value,\n path,\n this.context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n } finally {\n activeObjects.delete(action.input);\n }\n return refs;\n }\n}\n\nfunction compileExtractor(\n module: DocumentModelModule,\n actionType: string,\n): CompiledAttachmentExtractor {\n const context: CompilerContext = {\n actionType,\n documentType: module.documentModel.global.id,\n version: module.version ?? 1,\n };\n const specification = applicableSpecification(module, context);\n const operations = parseOperations(specification, context);\n const selected = selectOperation(operations, context);\n if (selected === null || selected.operation.schema === null) {\n return new SchemaCompiledAttachmentExtractor(context, null);\n }\n const effectiveSchema = buildEffectiveSchema(specification, context);\n\n const operationName = selected.operation.name;\n if (operationName === null) {\n throw compilationError(context, \"the operation has no name\");\n }\n const rootName = `${pascalCase(operationName)}Input`;\n const definitions = new Map<string, GraphQLInputObjectType>();\n for (const type of Object.values(effectiveSchema.getTypeMap())) {\n if (isInputObjectType(type) && !type.name.startsWith(\"__\")) {\n definitions.set(type.name, type);\n }\n }\n const rootDefinitions = selected.document?.definitions.filter(\n (definition) =>\n (definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION ||\n definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION) &&\n definition.name.value === rootName,\n );\n if (!rootDefinitions?.length || !definitions.has(rootName)) {\n throw compilationError(\n context,\n `the operation does not declare its expected root input \"${rootName}\"`,\n );\n }\n\n return new SchemaCompiledAttachmentExtractor(\n context,\n compileRootPlan(rootName, definitions),\n );\n}\n\nexport class AttachmentSchemaCompiler implements IAttachmentSchemaCompiler {\n private readonly cache = new WeakMap<\n DocumentModelModule,\n Map<string, CompiledAttachmentExtractor>\n >();\n\n forModuleAction(\n module: DocumentModelModule,\n actionType: string,\n ): CompiledAttachmentExtractor {\n let moduleCache = this.cache.get(module);\n if (!moduleCache) {\n moduleCache = new Map();\n this.cache.set(module, moduleCache);\n }\n\n const cached = moduleCache.get(actionType);\n if (cached) return cached;\n\n const compiled = compileExtractor(module, actionType);\n moduleCache.set(actionType, compiled);\n return compiled;\n }\n}\n"],"mappings":"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"}
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { A as IReservationStore, B as AttachmentStatOptions, C as IAttachmentReader, D as IAttachmentTransportFactory, E as IAttachmentTransport, F as AttachmentDownloadTargetOptions, G as AttachmentUploadTarget, H as AttachmentTargetHeaders, I as AttachmentHeader, J as ReserveAttachmentOptions, K as HashFirstReserveAttachmentOptions, L as AttachmentMetadata, N as AttachmentDownloadOptions, O as IAttachmentUpload, P as AttachmentDownloadTarget, Q as AttachmentAlreadyExists, R as AttachmentResponse, T as IAttachmentStore, U as AttachmentTransportConfig, V as AttachmentStatus, W as AttachmentUploadResult, X as TransportResponse, Y as TransportFetchResult, Z as UploadFirstReserveAttachmentOptions, _ as createRef, a as RemoteAttachmentUpload, b as parseAttachmentDownloadTarget, c as XhrUploadTransportOptions, ct as SizeMismatch, d as AttachmentUploadResponse, et as AttachmentNotFound, f as AttachmentUploadTransport, g as ParsedRef, h as SwitchboardTransportConfig, i as RemoteAttachmentUploadFactory, it as HashMismatch, k as IAttachmentUploadFactory, l as createXhrUploadTransport, lt as UploadTooLarge, m as SwitchboardAttachmentTransport, n as createRemoteAttachmentService, nt as AttachmentTransferError, o as RemoteReservationStore, ot as InvalidAttachmentRef, p as createFetchUploadTransport, q as Reservation, r as RemoteAttachmentStore, rt as AttachmentTransferStage, s as SwitchboardClientConfig, st as ReservationNotFound, t as NullAttachmentTransport, tt as AttachmentPending, u as AttachmentUploadRequest, v as parseRef, w as IAttachmentService, x as parseAttachmentUploadTarget, y as AttachmentService, z as AttachmentSendOptions } from "./null-attachment-transport-jZmzFncu.js";
1
+ import { A as InvalidAttachmentRef, D as AttachmentTransferStage, E as AttachmentTransferError, M as SizeMismatch, N as UploadTooLarge, O as HashMismatch, S as AttachmentAlreadyExists, T as AttachmentPending, _ as createRef, a as RemoteAttachmentUpload, b as parseAttachmentDownloadTarget, c as XhrUploadTransportOptions, d as AttachmentUploadResponse, f as AttachmentUploadTransport, g as ParsedRef, h as SwitchboardTransportConfig, i as RemoteAttachmentUploadFactory, j as ReservationNotFound, l as createXhrUploadTransport, m as SwitchboardAttachmentTransport, n as createRemoteAttachmentService, o as RemoteReservationStore, p as createFetchUploadTransport, r as RemoteAttachmentStore, s as SwitchboardClientConfig, t as NullAttachmentTransport, u as AttachmentUploadRequest, v as parseRef, w as AttachmentNotFound, x as parseAttachmentUploadTarget, y as AttachmentService } from "./null-attachment-transport-dg7vi6F4.js";
2
+ import { A as UploadFirstReserveAttachmentOptions, C as AttachmentUploadResult, D as ReserveAttachmentOptions, E as Reservation, O as TransportFetchResult, S as AttachmentTransportConfig, T as HashFirstReserveAttachmentOptions, _ as AttachmentResponse, a as IAttachmentTransport, b as AttachmentStatus, c as IAttachmentUploadFactory, f as AttachmentDownloadOptions, g as AttachmentMetadata, h as AttachmentHeader, i as IAttachmentStore, k as TransportResponse, l as IReservationStore, m as AttachmentDownloadTargetOptions, n as IAttachmentReader, o as IAttachmentTransportFactory, p as AttachmentDownloadTarget, r as IAttachmentService, s as IAttachmentUpload, v as AttachmentSendOptions, w as AttachmentUploadTarget, x as AttachmentTargetHeaders, y as AttachmentStatOptions } from "./interfaces-jHW9JXv1.js";
2
3
  import { AttachmentHash, AttachmentRef } from "@powerhousedao/reactor";
3
4
  //#region src/concurrency.d.ts
4
5
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","names":[],"sources":["../src/concurrency.ts","../src/progress.ts","../src/client.ts"],"mappings":";;;;;;;;KAKY,gBAAgB;EACtB;EAAe;EAAqB,OAAO;;EAC3C;EAAe;EAAoB;;KAE7B,0BAA0B;;EAEpC;;;;;;EAMA,SAAS;;;;;;;EAOT,aAAa,QAAQ,gBAAgB;;;;;;;wBAQjB,mBAAmB,GAAG,GAC1C,gBAAgB,KAChB,SAAS,MAAM,GAAG,kBAAkB,QAAQ,IAC5C,SAAS,0BAA0B,KAClC,QAAQ,gBAAgB;;;;;;;;;;;KC3Bf;KASA;EACV,OAAO;;EAEP;;EAEA;;EAEA;;EAEA;;KAGU,8BAA8B,UAAU;KAExC;EACV,aAAa;;EAEb;;qBAGW;;;;;;wBAOG,iBAAiB,UAAU;KAS/B,yBAAyB;;EAEnC;;;;;;;;;;;;;;;;;qBAkBW;mBACM;mBACA;mBACA;UAET;UACA;UACA;UACA;UACA;UACA;EAER,YAAY,UAAU;;MAOlB;;;;;;EASJ,MAAM,OAAO,iBAAiB;IAAY;;;;;;EAkB1C,MAAM,gBAAgB;;;;;EAoBtB,OAAO;IAAa;IAAiB;IAAgB;;;EAerD;;EAKA;UAWQ;;KAME;;EAEV,WAAW;;EAEX;EACA,WAAW;;;;;;;;;;;;;;;;;;;;wBAqBG,iBACd,QAAQ,eAAe,aACvB,OAAO,oBACN,eAAe;;;YClGN;EACV,KAAK;EACL,MAAM;EACN;EACA,SAAS;EACT,MAAM,eAAe;EACrB,cAAc,eAAe;;YAGnB;EACV,MAAM;EACN;EACA;;EAEA,SAAS;;;;;;;;;;YAWC;EACV,cAAc;EACd,SAAS;;;;;;YAOC;EACV;EACA,KAAK;EACL,SAAS;;;;;;;;YASC;EACV;;YAGU;EACV;EACA;;YAGU;EACV,MAAM;EACN,QAAQ;;YAGE;;EAEV;EACA,QAAQ;EACR;;YAGU;EACV;EACA,KAAK;;EAEL;EACA,SAAS;;;;;;;YAQC;EACV;EACA;;;;;;;;YASU;;EAEV;;EAEA,SAAS;EACT,cAAc,UAAU;IAAuB;;;;;;EAK/C,iBAAiB,QAAQ;;EAEzB;;YAGU;EACV;EACA;EACA;EACA;;qBAGW;iBAEI;EACf,WACE,MAAM,MACN;IAAS;IAAmB;MAC3B,QAAQ;EACX,QACE,SAAS,mCACT,OAAO,QAAQ,sBAAsB,QAAQ,0BAC5C,QAAQ;;;;;;;;EAQX,OACE,OAAO,wBAAwB,mCAC/B,UAAU,4BACT,QAAQ;;;;;;;;;EASX,SACE,OAAO,yBACP,UAAU,4BACT,QAAQ;;;;;;EAMX,aACE,OAAO,yBACP,UAAU,wBAAwB,4BACjC,QAAQ;;;;;;EAMX,eACE,OAAO,yBACP,UAAU,wBAAwB,4BACjC;;;;;;EAMH,kBACE,OAAO,yBACP,UAAU,wBAAwB,4BACjC,QAAQ;;;;;;;;EAQX,aAAa,OAAO,2BAA2B,QAAQ;EACvD,WACE,iBAAiB,yBACjB,UAAU,yBACT,QAAQ,gBAAgB;EAC3B,aACE,iBAAiB,2BACjB,UAAU,yBACT,QAAQ,gBAAgB;;wBAgYb,uBACd,SAAS,qBACR"}
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/concurrency.ts","../src/progress.ts","../src/client.ts"],"mappings":";;;;;;;;;KAKY,gBAAgB;EACtB;EAAe;EAAqB,OAAO;;EAC3C;EAAe;EAAoB;;KAE7B,0BAA0B;;EAEpC;;;;;;EAMA,SAAS;;;;;;;EAOT,aAAa,QAAQ,gBAAgB;;;;;;;wBAQjB,mBAAmB,GAAG,GAC1C,gBAAgB,KAChB,SAAS,MAAM,GAAG,kBAAkB,QAAQ,IAC5C,SAAS,0BAA0B,KAClC,QAAQ,gBAAgB;;;;;;;;;;;KC3Bf;KASA;EACV,OAAO;;EAEP;;EAEA;;EAEA;;EAEA;;KAGU,8BAA8B,UAAU;KAExC;EACV,aAAa;;EAEb;;qBAGW;;;;;;wBAOG,iBAAiB,UAAU;KAS/B,yBAAyB;;EAEnC;;;;;;;;;;;;;;;;;qBAkBW;mBACM;mBACA;mBACA;UAET;UACA;UACA;UACA;UACA;UACA;EAER,YAAY,UAAU;;MAOlB;;;;;;EASJ,MAAM,OAAO,iBAAiB;IAAY;;;;;;EAkB1C,MAAM,gBAAgB;;;;;EAoBtB,OAAO;IAAa;IAAiB;IAAgB;;;EAerD;;EAKA;UAWQ;;KAME;;EAEV,WAAW;;EAEX;EACA,WAAW;;;;;;;;;;;;;;;;;;;;wBAqBG,iBACd,QAAQ,eAAe,aACvB,OAAO,oBACN,eAAe;;;YCjGN;EACV,KAAK;EACL,MAAM;EACN;EACA,SAAS;EACT,MAAM,eAAe;EACrB,cAAc,eAAe;;YAGnB;EACV,MAAM;EACN;EACA;;EAEA,SAAS;;;;;;;;;;YAWC;EACV,cAAc;EACd,SAAS;;;;;;YAOC;EACV;EACA,KAAK;EACL,SAAS;;;;;;;;YASC;EACV;;YAGU;EACV;EACA;;YAGU;EACV,MAAM;EACN,QAAQ;;YAGE;;EAEV;EACA,QAAQ;EACR;;YAGU;EACV;EACA,KAAK;;EAEL;EACA,SAAS;;;;;;;YAQC;EACV;EACA;;;;;;;;YASU;;EAEV;;EAEA,SAAS;EACT,cAAc,UAAU;IAAuB;;;;;;EAK/C,iBAAiB,QAAQ;;EAEzB;;YAGU;EACV;EACA;EACA;EACA;;qBAGW;iBAEI;EACf,WACE,MAAM,MACN;IAAS;IAAmB;MAC3B,QAAQ;EACX,QACE,SAAS,mCACT,OAAO,QAAQ,sBAAsB,QAAQ,0BAC5C,QAAQ;;;;;;;;EAQX,OACE,OAAO,wBAAwB,mCAC/B,UAAU,4BACT,QAAQ;;;;;;;;;EASX,SACE,OAAO,yBACP,UAAU,4BACT,QAAQ;;;;;;EAMX,aACE,OAAO,yBACP,UAAU,wBAAwB,4BACjC,QAAQ;;;;;;EAMX,eACE,OAAO,yBACP,UAAU,wBAAwB,4BACjC;;;;;;EAMH,kBACE,OAAO,yBACP,UAAU,wBAAwB,4BACjC,QAAQ;;;;;;;;EAQX,aAAa,OAAO,2BAA2B,QAAQ;EACvD,WACE,iBAAiB,yBACjB,UAAU,yBACT,QAAQ,gBAAgB;EAC3B,aACE,iBAAiB,2BACjB,UAAU,yBACT,QAAQ,gBAAgB;;wBA6Xb,uBACd,SAAS,qBACR"}
package/dist/client.js CHANGED
@@ -1,4 +1,6 @@
1
- import { C as SizeMismatch, S as ReservationNotFound, _ as AttachmentPending, a as RemoteAttachmentUpload, c as createFetchUploadTransport, d as createRef, f as parseRef, g as AttachmentNotFound, h as AttachmentAlreadyExists, i as RemoteAttachmentUploadFactory, l as SwitchboardAttachmentTransport, m as parseAttachmentUploadTarget, n as createRemoteAttachmentService, o as RemoteReservationStore, p as parseAttachmentDownloadTarget, r as RemoteAttachmentStore, s as createXhrUploadTransport, t as NullAttachmentTransport, u as AttachmentService, v as AttachmentTransferError, w as UploadTooLarge, x as InvalidAttachmentRef, y as HashMismatch } from "./null-attachment-transport-CUDNBfd5.js";
1
+ import { a as parseRef, c as AttachmentPending, f as InvalidAttachmentRef, h as UploadTooLarge, i as createRef, l as AttachmentTransferError, m as SizeMismatch, o as AttachmentAlreadyExists, p as ReservationNotFound, s as AttachmentNotFound, t as SwitchboardAttachmentTransport, u as HashMismatch } from "./switchboard-attachment-transport-BH3ZAmxE.js";
2
+ import { a as RemoteAttachmentUpload, c as createFetchUploadTransport, d as parseAttachmentUploadTarget, i as RemoteAttachmentUploadFactory, l as AttachmentService, n as createRemoteAttachmentService, o as RemoteReservationStore, r as RemoteAttachmentStore, s as createXhrUploadTransport, t as NullAttachmentTransport, u as parseAttachmentDownloadTarget } from "./null-attachment-transport-BGG9cRC-.js";
3
+ import { t as sha256Hex } from "./hash-CKHeI4AH.js";
2
4
  //#region src/concurrency.ts
3
5
  /**
4
6
  * Runs `worker` over `items` with a hard upper bound on simultaneous
@@ -300,8 +302,7 @@ var AttachmentClientImpl = class {
300
302
  async preprocess(file, opts) {
301
303
  const buf = await file.arrayBuffer();
302
304
  const bytes = new Uint8Array(buf);
303
- const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
304
- const hash = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
305
+ const hash = await sha256Hex(bytes);
305
306
  const ref = createRef(hash);
306
307
  const sizeBytes = file.size;
307
308
  const options = {
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/concurrency.ts","../src/progress.ts","../src/client.ts"],"sourcesContent":["/**\n * Ordered result of one batch item. `index` always mirrors the input\n * position, so callers can correlate results with inputs regardless of\n * completion order, and successes are retained when siblings fail.\n */\nexport type BatchItemResult<R> =\n | { index: number; status: \"fulfilled\"; value: R }\n | { index: number; status: \"rejected\"; error: unknown };\n\nexport type RunWithConcurrencyOptions<R = unknown> = {\n /** Maximum simultaneously running workers. Must be a positive integer. */\n concurrency: number;\n /**\n * Whole-batch cancellation: unstarted items are rejected with the signal's\n * reason without ever starting, while already-started items keep running —\n * per-item signals are the mechanism for interrupting active work.\n */\n signal?: AbortSignal;\n /**\n * Called once per item as it settles, in completion order, including items\n * the signal skipped before they ever started. That completeness is the\n * point: a caller counting settlements must still reach `items.length`\n * after a whole-batch abort.\n */\n onSettled?: (result: BatchItemResult<R>) => void;\n};\n\n/**\n * Runs `worker` over `items` with a hard upper bound on simultaneous\n * executions. Bounding starts (not just transfers) is what keeps memory flat:\n * an item's preprocessing (hashing/buffering) only begins when a slot frees.\n */\nexport async function runWithConcurrency<T, R>(\n items: readonly T[],\n worker: (item: T, index: number) => Promise<R>,\n options: RunWithConcurrencyOptions<R>,\n): Promise<BatchItemResult<R>[]> {\n const { concurrency, signal, onSettled } = options;\n if (!Number.isInteger(concurrency) || concurrency < 1) {\n throw new Error(\n `concurrency must be a positive integer, got: ${concurrency}`,\n );\n }\n\n const results: BatchItemResult<R>[] = new Array<BatchItemResult<R>>(\n items.length,\n );\n let nextIndex = 0;\n\n async function runLane(): Promise<void> {\n for (;;) {\n const index = nextIndex++;\n if (index >= items.length) return;\n if (signal?.aborted) {\n results[index] = {\n index,\n status: \"rejected\",\n error: signalReason(signal),\n };\n onSettled?.(results[index]);\n continue;\n }\n try {\n const value = await worker(items[index], index);\n results[index] = { index, status: \"fulfilled\", value };\n } catch (error) {\n results[index] = { index, status: \"rejected\", error };\n }\n onSettled?.(results[index]);\n }\n }\n\n const lanes = Array.from(\n { length: Math.min(concurrency, items.length) },\n () => runLane(),\n );\n await Promise.all(lanes);\n return results;\n}\n\nfunction signalReason(signal: AbortSignal): unknown {\n return (\n (signal as { reason?: unknown }).reason ??\n new DOMException(\"The operation was aborted\", \"AbortError\")\n );\n}\n","/**\n * Byte-level progress for attachment transfers.\n *\n * `loaded`/`total` are per-stage, never per-operation. A per-operation\n * denominator would have to weight hashing against transfer, and a confirmed\n * dedup would shrink it mid-flight from 2N to N — the bar would run backwards.\n * Choosing that weighting is a UI decision the client cannot make.\n */\n\nexport type AttachmentStage =\n | \"hashing\"\n | \"reserving\"\n | \"uploading\"\n | \"requesting-download-target\"\n | \"downloading\"\n | \"done\"\n | \"error\";\n\nexport type AttachmentProgress = {\n stage: AttachmentStage;\n /** Bytes moved so far within `stage`. */\n loaded: number;\n /** Denominator for `stage`: file.size for uploads, header.sizeBytes for downloads. */\n total: number | undefined;\n /** True when the stage is running but bytes cannot (yet) be observed. */\n indeterminate: boolean;\n /** Only on a terminal `done` where dedup skipped the transfer entirely. */\n deduped?: true;\n};\n\nexport type AttachmentProgressListener = (progress: AttachmentProgress) => void;\n\nexport type AttachmentProgressOptions = {\n onProgress?: AttachmentProgressListener;\n /** Min ms between byte events within a stage. Default 100. 0 emits every observation. */\n throttleMs?: number;\n};\n\nexport const DEFAULT_PROGRESS_THROTTLE_MS = 100;\n\n/**\n * Fraction in 0..1 for a progress event. Indeterminate and unknown-total\n * events read as 0 — a bar cannot honestly show a position it does not know.\n * A zero total is complete by definition (the dedup case moves no bytes).\n */\nexport function progressFraction(progress: AttachmentProgress): number {\n if (progress.indeterminate) return 0;\n if (progress.total === undefined) return 0;\n if (progress.total === 0) return 1;\n const fraction = progress.loaded / progress.total;\n if (fraction < 0) return 0;\n return fraction > 1 ? 1 : fraction;\n}\n\nexport type ProgressEmitterOptions = AttachmentProgressOptions & {\n /** Injected clock so throttle tests stay synchronous. */\n now?: () => number;\n};\n\n/**\n * Emits `AttachmentProgress` for one single-item operation, upholding:\n *\n * - **I1** exactly one terminal event (`done` or `error`) — never both, never twice.\n * - **I2** on `done`: `indeterminate === false`, `total` is a number, `loaded === total`.\n * - **I3** `loaded` is non-decreasing within a stage and resets to 0 on stage change.\n * - **I4** the terminal event bypasses the throttle unconditionally.\n * - **I5** `error` carries the last known `loaded`/`total`.\n *\n * Leading edge: the first `bytes()` after a `stage()` always emits, so a bar\n * leaves 0 immediately. There is deliberately no trailing-edge timer — it\n * would leak on throw, could fire after the terminal event and violate I1's\n * ordering, and is unnecessary because `finish()` always emits\n * `loaded === total`, making a dropped last intra-stage tick invisible.\n */\nexport class ProgressEmitter {\n private readonly listener: AttachmentProgressListener | undefined;\n private readonly throttleMs: number;\n private readonly now: () => number;\n\n private currentStage: AttachmentStage | undefined;\n private loaded = 0;\n private total: number | undefined;\n private emittedBytesInStage = false;\n private lastEmitAt = Number.NEGATIVE_INFINITY;\n private settled = false;\n\n constructor(options?: ProgressEmitterOptions) {\n this.listener = options?.onProgress;\n this.throttleMs = options?.throttleMs ?? DEFAULT_PROGRESS_THROTTLE_MS;\n this.now = options?.now ?? (() => Date.now());\n }\n\n /** True when a listener is attached; lets callers skip instrumentation entirely. */\n get active(): boolean {\n return this.listener !== undefined;\n }\n\n /**\n * Enter a stage. Always emits and resets `loaded` to 0. Entry is always\n * indeterminate: no bytes have moved yet, and whether they can be observed\n * at all is only proven by a transport actually calling back.\n */\n stage(stage: AttachmentStage, options?: { total?: number }): void {\n if (this.settled) return;\n this.currentStage = stage;\n this.loaded = 0;\n this.total = options?.total;\n this.emittedBytesInStage = false;\n this.emit({\n stage,\n loaded: 0,\n total: this.total,\n indeterminate: true,\n });\n }\n\n /**\n * Report observed bytes within the current stage. Throttled by time, with\n * the first observation after a stage change always emitted.\n */\n bytes(loaded: number, total?: number): void {\n if (this.settled || this.currentStage === undefined) return;\n if (total !== undefined) this.total = total;\n if (loaded > this.loaded) this.loaded = loaded;\n\n const leadingEdge = !this.emittedBytesInStage;\n if (!leadingEdge && this.now() - this.lastEmitAt < this.throttleMs) return;\n this.emittedBytesInStage = true;\n this.emit({\n stage: this.currentStage,\n loaded: this.loaded,\n total: this.total,\n indeterminate: false,\n });\n }\n\n /**\n * Terminal success. `total` defaults to the bytes actually seen so a stage\n * with no known denominator still satisfies I2.\n */\n finish(override?: { loaded?: number; total?: number; deduped?: true }): void {\n if (this.settled) return;\n this.settled = true;\n const total = override?.total ?? this.total ?? this.loaded;\n const loaded = override?.loaded ?? total;\n this.emit({\n stage: \"done\",\n loaded,\n total,\n indeterminate: false,\n ...(override?.deduped ? { deduped: true as const } : {}),\n });\n }\n\n /** Terminal success for a confirmed dedup: no bytes moved, and none needed to. */\n finishDeduped(): void {\n this.finish({ loaded: 0, total: 0, deduped: true });\n }\n\n /** Terminal failure, carrying the last known byte counts. */\n fail(): void {\n if (this.settled) return;\n this.settled = true;\n this.emit({\n stage: \"error\",\n loaded: this.loaded,\n total: this.total,\n indeterminate: false,\n });\n }\n\n private emit(progress: AttachmentProgress): void {\n this.lastEmitAt = this.now();\n this.listener?.(progress);\n }\n}\n\nexport type ByteProgressHooks = {\n /** Cumulative bytes handed to the consumer. */\n onBytes?: (loaded: number) => void;\n /** The source ended and every byte reached the consumer. */\n onDone?: () => void;\n onError?: (error: unknown) => void;\n};\n\n/**\n * Count bytes as a consumer reads them.\n *\n * Uses the manual-reader form rather than `pipeThrough`, whose internal queue\n * reads far ahead of the consumer and would make `loaded` overstate what was\n * actually received. `highWaterMark: 0` is load-bearing for the same reason:\n * at the default of 1 the wrapper pulls one chunk the instant it is\n * constructed, so a caller who never reads still sees bytes counted.\n *\n * `cancel` propagates to the source, which is what keeps reader refcounts\n * (e.g. `KyselyAttachmentStore`'s) correct. Cancelling is an abandonment, not\n * a completion, so it reports `onError` — never `onDone` — carrying the\n * cancel reason the way an `AbortSignal` carries its own, with the DOM's\n * default `AbortError` when the canceller gave none.\n *\n * Returns `source` unchanged when no hook is supplied, so a caller with no\n * listener pays nothing.\n */\nexport function withByteProgress(\n source: ReadableStream<Uint8Array>,\n hooks: ByteProgressHooks,\n): ReadableStream<Uint8Array> {\n if (!hooks.onBytes && !hooks.onDone && !hooks.onError) return source;\n\n let loaded = 0;\n let cancelled = false;\n const reader = source.getReader();\n return new ReadableStream<Uint8Array>(\n {\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n // A cancel that lands while this read is in flight resolves it as\n // `done`, and closes the stream. Bailing out here is what stops an\n // abandoned transfer from reporting completion, and stops the\n // already-closed controller from throwing a fabricated error into\n // the catch below.\n if (cancelled) return;\n if (done) {\n hooks.onDone?.();\n controller.close();\n return;\n }\n loaded += value.byteLength;\n hooks.onBytes?.(loaded);\n controller.enqueue(value);\n } catch (err) {\n if (cancelled) return;\n hooks.onError?.(err);\n controller.error(err);\n }\n },\n cancel(reason) {\n cancelled = true;\n reader.cancel(reason).catch(() => {});\n hooks.onError?.(reason ?? abortError());\n },\n },\n { highWaterMark: 0 },\n );\n}\n\n/** What an `AbortSignal` carries when `abort()` is called with no reason. */\nfunction abortError(): DOMException {\n return new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport {\n runWithConcurrency,\n type BatchItemResult,\n type RunWithConcurrencyOptions,\n} from \"./concurrency.js\";\nimport { AttachmentAlreadyExists } from \"./errors.js\";\nexport type { AttachmentTransferStage } from \"./errors.js\";\nexport {\n runWithConcurrency,\n type BatchItemResult,\n type RunWithConcurrencyOptions,\n} from \"./concurrency.js\";\nimport type { IAttachmentService, IAttachmentUpload } from \"./interfaces.js\";\nimport {\n ProgressEmitter,\n withByteProgress,\n type AttachmentProgress,\n type AttachmentProgressOptions,\n} from \"./progress.js\";\nimport { createRef } from \"./ref.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n AttachmentSendOptions,\n AttachmentUploadResult,\n HashFirstReserveAttachmentOptions,\n} from \"./types.js\";\n\nexport { AttachmentService } from \"./attachment-service.js\";\nexport {\n AttachmentAlreadyExists,\n AttachmentNotFound,\n AttachmentPending,\n AttachmentTransferError,\n HashMismatch,\n InvalidAttachmentRef,\n ReservationNotFound,\n SizeMismatch,\n UploadTooLarge,\n} from \"./errors.js\";\nexport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentStore,\n IAttachmentTransport,\n IAttachmentTransportFactory,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nexport { parseRef, createRef } from \"./ref.js\";\nexport type { ParsedRef } from \"./ref.js\";\nexport type {\n AttachmentDownloadOptions,\n AttachmentDownloadTarget,\n AttachmentDownloadTargetOptions,\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentSendOptions,\n AttachmentStatOptions,\n AttachmentStatus,\n AttachmentTransportConfig,\n AttachmentUploadResult,\n AttachmentTargetHeaders,\n AttachmentUploadTarget,\n HashFirstReserveAttachmentOptions,\n UploadFirstReserveAttachmentOptions,\n Reservation,\n ReserveAttachmentOptions,\n TransportFetchResult,\n TransportResponse,\n} from \"./types.js\";\nexport {\n parseAttachmentDownloadTarget,\n parseAttachmentUploadTarget,\n} from \"./targets.js\";\nexport {\n SwitchboardAttachmentTransport,\n type SwitchboardTransportConfig,\n RemoteReservationStore,\n type SwitchboardClientConfig,\n RemoteAttachmentUpload,\n RemoteAttachmentUploadFactory,\n RemoteAttachmentStore,\n createRemoteAttachmentService,\n createFetchUploadTransport,\n createXhrUploadTransport,\n type AttachmentUploadRequest,\n type AttachmentUploadResponse,\n type AttachmentUploadTransport,\n type XhrUploadTransportOptions,\n} from \"./switchboard/index.js\";\nexport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\nexport {\n DEFAULT_PROGRESS_THROTTLE_MS,\n progressFraction,\n ProgressEmitter,\n withByteProgress,\n type AttachmentProgress,\n type AttachmentProgressListener,\n type AttachmentProgressOptions,\n type AttachmentStage,\n type ByteProgressHooks,\n type ProgressEmitterOptions,\n} from \"./progress.js\";\n\nexport type PreprocessResult = {\n ref: AttachmentRef;\n hash: AttachmentHash;\n sizeBytes: number;\n options: HashFirstReserveAttachmentOptions;\n data: ReadableStream<Uint8Array>;\n stream: () => ReadableStream<Uint8Array>;\n};\n\nexport type AttachmentUploadInput = {\n file: Blob;\n fileName?: string;\n mimeType?: string;\n /** Per-item cancellation, checked between stages. */\n signal?: AbortSignal;\n};\n\n/**\n * Upload bytes that were already hashed by a separate `preprocess()` call.\n *\n * This is the canonical two-call flow: the ref must reach the document before\n * the bytes are committed, so callers preprocess, dispatch the operation, then\n * upload. Passing the result back in skips the hashing stage rather than\n * faking a zero-millisecond one.\n */\nexport type AttachmentPreprocessedUploadInput = {\n preprocessed: PreprocessResult;\n signal?: AbortSignal;\n};\n\n/**\n * Every remote download names the document that authorizes its ref; batches\n * may freely mix documents because the anchor travels with each item.\n */\nexport type AttachmentDownloadInput = {\n documentId: string;\n ref: AttachmentRef;\n signal?: AbortSignal;\n};\n\n/**\n * The document keeps its own name and type for an attachment (the same\n * bytes may appear under different names in different documents), so the\n * blob-producing conveniences let callers override what the server header\n * reports from upload time.\n */\nexport type AttachmentBlobOptions = {\n mimeType?: string;\n};\n\nexport type AttachmentSaveOptions = {\n fileName?: string;\n mimeType?: string;\n};\n\nexport type AttachmentBlobResult = {\n blob: Blob;\n header: AttachmentHeader;\n};\n\nexport type AttachmentObjectUrl = {\n /** Ready for img/iframe/video src. Pins memory until revoke() is called. */\n url: string;\n header: AttachmentHeader;\n revoke: () => void;\n};\n\nexport type AttachmentShareLinkInput = {\n documentId: string;\n ref: AttachmentRef;\n /** Requested link lifetime in seconds; the server clamps to its maximum. */\n expiresIn?: number;\n signal?: AbortSignal;\n};\n\n/**\n * A self-contained public URL: anyone holding it can fetch the bytes until\n * expiresAtUtc, with no login and no document access. Minting one requires\n * document read access; once minted it cannot be revoked before expiry.\n */\nexport type AttachmentShareLink = {\n url: string;\n expiresAtUtc: string;\n};\n\n/**\n * Batch progress is per item, carrying the item's `index`. There is\n * deliberately no batch-wide byte total: weighting items against each other is\n * a presentation decision the client cannot make, and a confirmed dedup would\n * shrink the denominator mid-flight.\n */\nexport type AttachmentBatchOptions = {\n /** Bounds preprocessing and transfer together. Defaults to 4. */\n concurrency?: number;\n /** Whole-batch cancellation: stops unstarted items. */\n signal?: AbortSignal;\n onProgress?: (progress: AttachmentProgress & { index: number }) => void;\n /**\n * Fired as each item settles, including items the signal skipped before they\n * started, so `settled` always reaches `total`.\n */\n onItemSettled?: (counts: AttachmentBatchCounts) => void;\n /** Min ms between byte events, applied per item rather than batch-wide. */\n throttleMs?: number;\n};\n\nexport type AttachmentBatchCounts = {\n settled: number;\n completed: number;\n failed: number;\n total: number;\n};\n\nexport const DEFAULT_ATTACHMENT_BATCH_CONCURRENCY = 4;\n\nexport interface IAttachmentClient {\n preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult>;\n reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult>;\n /**\n * Hash, reserve, and transfer one file; a confirmed dedup skips the transfer\n * and reports a terminal `done` carrying `deduped: true` and zero bytes.\n *\n * Also accepts an already-preprocessed payload, so the canonical\n * preprocess -> dispatch -> upload flow reports progress too.\n */\n upload(\n input: AttachmentUploadInput | AttachmentPreprocessedUploadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentUploadResult>;\n /**\n * Document-authorized download of one ref.\n *\n * Resolves as soon as the byte stream is available, with `downloading` as\n * the last event emitted and `loaded: 0`. Byte events and the terminal\n * `done` arrive as the caller reads the stream — and never arrive at all if\n * the caller abandons it.\n */\n download(\n input: AttachmentDownloadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentResponse>;\n /**\n * Document-authorized download materialized as a typed Blob. The Blob's\n * type comes from the server header unless overridden — that's what makes\n * browsers render PDFs inline and images correctly.\n */\n downloadBlob(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentBlobResult>;\n /**\n * Download and hand the bytes to the browser's save-file flow. Browser\n * only. fileName defaults to the server header's; pass the document's own\n * name for per-document naming.\n */\n saveAttachment(\n input: AttachmentDownloadInput,\n options?: AttachmentSaveOptions & AttachmentProgressOptions,\n ): Promise<void>;\n /**\n * Download and expose the bytes as an object URL for inline rendering\n * (img/iframe/video src). Callers MUST call revoke() when done — the URL\n * pins the blob in memory until then.\n */\n downloadObjectUrl(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentObjectUrl>;\n /**\n * Mint a public share link: a presigned URL anyone can fetch until it\n * expires, with no login. Authorized exactly like a download (document\n * read access + the reference index). Requires a presigned-capable\n * storage backend (S3); rejects when the server answers with an\n * authenticated switchboard target, which would not be public.\n */\n getShareLink(input: AttachmentShareLinkInput): Promise<AttachmentShareLink>;\n uploadMany(\n inputs: readonly AttachmentUploadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentUploadResult>[]>;\n downloadMany(\n inputs: readonly AttachmentDownloadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentResponse>[]>;\n}\n\n/**\n * Duck-typed dedup detection: bundlers (notably Vite dev pre-bundling) can\n * load two copies of this package's error classes, one for the service and\n * one for the client wrapper, making a plain instanceof check miss the\n * cross-copy throw. Name plus payload shape identifies the error reliably.\n */\nfunction isAttachmentAlreadyExists(\n err: unknown,\n): err is AttachmentAlreadyExists {\n if (err instanceof AttachmentAlreadyExists) return true;\n return (\n err instanceof Error &&\n err.name === \"AttachmentAlreadyExists\" &&\n typeof (err as { hash?: unknown }).hash === \"string\" &&\n typeof (err as { ref?: unknown }).ref === \"string\"\n );\n}\n\n// Nothing references the blob yet, so the server header is not readable.\nfunction dedupedHeader(\n hash: AttachmentHash,\n options: HashFirstReserveAttachmentOptions,\n): AttachmentHeader {\n const now = new Date().toISOString();\n return {\n hash,\n mimeType: options.mimeType,\n fileName: options.fileName,\n sizeBytes: options.sizeBytes,\n extension: options.extension ?? null,\n status: \"available\",\n source: \"local\",\n createdAtUtc: now,\n lastAccessedAtUtc: now,\n expiresAtUtc: null,\n };\n}\n\nfunction streamFromBuffer(buf: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(buf);\n controller.close();\n },\n });\n}\n\n/** Stamps an item's index onto its progress events. */\nfunction itemProgress(\n options: AttachmentBatchOptions | undefined,\n index: number,\n): AttachmentProgressOptions | undefined {\n if (!options?.onProgress && options?.throttleMs === undefined) {\n return undefined;\n }\n return {\n ...(options.onProgress\n ? {\n onProgress: (progress: AttachmentProgress) =>\n options.onProgress?.({ ...progress, index }),\n }\n : {}),\n ...(options.throttleMs !== undefined\n ? { throttleMs: options.throttleMs }\n : {}),\n };\n}\n\nfunction batchRunOptions<R>(\n total: number,\n options: AttachmentBatchOptions | undefined,\n): RunWithConcurrencyOptions<R> {\n const onItemSettled = options?.onItemSettled;\n let settled = 0;\n let completed = 0;\n let failed = 0;\n return {\n concurrency: options?.concurrency ?? DEFAULT_ATTACHMENT_BATCH_CONCURRENCY,\n ...(options?.signal ? { signal: options.signal } : {}),\n ...(onItemSettled\n ? {\n onSettled: (result: BatchItemResult<R>) => {\n settled += 1;\n if (result.status === \"fulfilled\") completed += 1;\n else failed += 1;\n onItemSettled({ settled, completed, failed, total });\n },\n }\n : {}),\n };\n}\n\nclass AttachmentClientImpl implements IAttachmentClient {\n constructor(private readonly service: IAttachmentService) {}\n\n async preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult> {\n const buf = await file.arrayBuffer();\n const bytes = new Uint8Array(buf);\n const digest = await globalThis.crypto.subtle.digest(\"SHA-256\", bytes);\n const hash = Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\") as AttachmentHash;\n const ref = createRef(hash);\n const sizeBytes = file.size;\n const mimeType = opts?.mimeType ?? file.type;\n const fileName =\n opts?.fileName ?? (file instanceof File ? file.name : \"attachment\");\n const options: HashFirstReserveAttachmentOptions = {\n mimeType,\n fileName,\n clientHash: hash,\n sizeBytes,\n };\n const data = streamFromBuffer(bytes);\n const stream = (): ReadableStream<Uint8Array> => streamFromBuffer(bytes);\n return { ref, hash, sizeBytes, options, data, stream };\n }\n\n /**\n * Reserve, distinguishing a confirmed dedup from a live upload handle in the\n * type rather than with a flag: the two outcomes move a different number of\n * bytes, and every caller has to account for that.\n */\n private async reserveOrDedup(\n options: HashFirstReserveAttachmentOptions,\n ): Promise<\n | { kind: \"handle\"; handle: IAttachmentUpload }\n | { kind: \"deduped\"; result: AttachmentUploadResult }\n > {\n try {\n return { kind: \"handle\", handle: await this.service.reserve(options) };\n } catch (err) {\n if (!isAttachmentAlreadyExists(err)) throw err;\n return {\n kind: \"deduped\",\n result: {\n hash: err.hash,\n ref: err.ref,\n header: dedupedHeader(err.hash, options),\n },\n };\n }\n }\n\n async reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult> {\n const outcome = await this.reserveOrDedup(options);\n if (outcome.kind === \"deduped\") return outcome.result;\n return send(outcome.handle);\n }\n\n async upload(\n input: AttachmentUploadInput | AttachmentPreprocessedUploadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentUploadResult> {\n const emitter = new ProgressEmitter(options);\n const signal = input.signal;\n try {\n signal?.throwIfAborted();\n\n let preprocessed: PreprocessResult;\n if (\"preprocessed\" in input) {\n preprocessed = input.preprocessed;\n } else {\n // Hashing is genuinely unobservable: preprocess() runs one\n // crypto.subtle.digest over the whole buffer and WebCrypto has no\n // streaming digest. The stage is still reported, with a denominator\n // and indeterminate never cleared, so a UI can name what it waits on.\n emitter.stage(\"hashing\", { total: input.file.size });\n preprocessed = await this.preprocess(input.file, {\n ...(input.fileName !== undefined ? { fileName: input.fileName } : {}),\n ...(input.mimeType !== undefined ? { mimeType: input.mimeType } : {}),\n });\n }\n const sizeBytes = preprocessed.sizeBytes;\n\n signal?.throwIfAborted();\n emitter.stage(\"reserving\");\n const outcome = await this.reserveOrDedup(preprocessed.options);\n\n // A confirmed dedup never enters `uploading` — the server already holds\n // these bytes, so none are sent. Reporting zero of zero keeps the byte\n // model honest instead of stalling at 0 and then jumping to done.\n if (outcome.kind === \"deduped\") {\n emitter.finishDeduped();\n return outcome.result;\n }\n\n signal?.throwIfAborted();\n emitter.stage(\"uploading\", { total: sizeBytes });\n // `onProgress` only when someone is listening. The XHR transport keys\n // its `upload.onprogress` registration off the option being present, and\n // that registration alone forces a CORS preflight on a presigned PUT —\n // a cost an unwatched upload must not pay for a callback that would\n // discard every event anyway.\n const sendOptions: AttachmentSendOptions = {\n ...(emitter.active\n ? {\n onProgress: (loaded: number, total?: number) =>\n emitter.bytes(loaded, total ?? sizeBytes),\n }\n : {}),\n ...(signal ? { signal } : {}),\n };\n const result = await outcome.handle.send(\n preprocessed.stream(),\n sendOptions,\n );\n emitter.finish();\n return result;\n } catch (err) {\n emitter.fail();\n throw err;\n }\n }\n\n /**\n * The single site where download bytes are instrumented.\n *\n * It sits here, above the service, rather than in any store or transport:\n * one logical get can be several byte streams underneath (an evicted\n * attachment is fetched from the transport, written to disk, then re-read),\n * so counting lower down would double-count that path and still miss\n * local-filesystem reads. Wrapping what `service.get` returns covers the\n * presigned, switchboard, legacy and local paths by construction, and\n * `downloadBlob`/`saveAttachment`/`downloadObjectUrl` inherit it.\n */\n private async getWithProgress(\n input: AttachmentDownloadInput,\n emitter: ProgressEmitter,\n ): Promise<AttachmentResponse> {\n const { header, body } = await this.service.get(input.ref, {\n documentId: input.documentId,\n signal: input.signal,\n });\n emitter.stage(\"downloading\", { total: header.sizeBytes });\n if (!emitter.active) return { header, body };\n return {\n header,\n body: withByteProgress(body, {\n onBytes: (loaded) => emitter.bytes(loaded),\n onDone: () => emitter.finish(),\n onError: () => emitter.fail(),\n }),\n };\n }\n\n async download(\n input: AttachmentDownloadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentResponse> {\n const emitter = new ProgressEmitter(options);\n try {\n input.signal?.throwIfAborted();\n emitter.stage(\"requesting-download-target\");\n return await this.getWithProgress(input, emitter);\n } catch (err) {\n emitter.fail();\n throw err;\n }\n }\n\n async downloadBlob(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentBlobResult> {\n const emitter = new ProgressEmitter(options);\n try {\n input.signal?.throwIfAborted();\n emitter.stage(\"requesting-download-target\");\n const { header, body } = await this.getWithProgress(input, emitter);\n const reader = body.getReader();\n const chunks: BlobPart[] = [];\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value as BlobPart);\n }\n const blob = new Blob(chunks, {\n type: options?.mimeType ?? header.mimeType,\n });\n // Draining the stream already finished the emitter; latching makes this\n // a no-op there, and a guarantee for any body that closes without one.\n emitter.finish();\n return { blob, header };\n } catch (err) {\n emitter.fail();\n throw err;\n }\n }\n\n async saveAttachment(\n input: AttachmentDownloadInput,\n options?: AttachmentSaveOptions & AttachmentProgressOptions,\n ): Promise<void> {\n if (typeof document === \"undefined\") {\n throw new Error(\n \"saveAttachment requires a browser environment; use downloadBlob elsewhere\",\n );\n }\n const { blob, header } = await this.downloadBlob(input, options);\n const url = URL.createObjectURL(blob);\n try {\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = options?.fileName ?? header.fileName;\n anchor.click();\n } finally {\n URL.revokeObjectURL(url);\n }\n }\n\n async downloadObjectUrl(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentObjectUrl> {\n const { blob, header } = await this.downloadBlob(input, options);\n const url = URL.createObjectURL(blob);\n let revoked = false;\n return {\n url,\n header,\n revoke: () => {\n if (revoked) return;\n revoked = true;\n URL.revokeObjectURL(url);\n },\n };\n }\n\n async getShareLink(\n input: AttachmentShareLinkInput,\n ): Promise<AttachmentShareLink> {\n input.signal?.throwIfAborted();\n const target = await this.service.getDownloadTarget(input.ref, {\n documentId: input.documentId,\n ...(input.expiresIn !== undefined ? { expiresIn: input.expiresIn } : {}),\n ...(input.signal !== undefined ? { signal: input.signal } : {}),\n });\n if (target.kind !== \"presigned-get\") {\n throw new Error(\n \"Public share links require a presigned-capable storage backend (S3); this server answered with an authenticated target\",\n );\n }\n return { url: target.url, expiresAtUtc: target.expiresAtUtc };\n }\n\n uploadMany(\n inputs: readonly AttachmentUploadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentUploadResult>[]> {\n return runWithConcurrency(\n inputs,\n (input, index) => this.upload(input, itemProgress(options, index)),\n batchRunOptions(inputs.length, options),\n );\n }\n\n /**\n * `concurrency` bounds download-target negotiation, not simultaneous byte\n * transfer: each item resolves when its stream is handed over, and the bytes\n * move only as the caller reads. Byte progress makes that pre-existing\n * behavior visible; it does not change it.\n */\n downloadMany(\n inputs: readonly AttachmentDownloadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentResponse>[]> {\n return runWithConcurrency(\n inputs,\n (input, index) => this.download(input, itemProgress(options, index)),\n batchRunOptions(inputs.length, options),\n );\n }\n}\n\nexport function createAttachmentClient(\n service: IAttachmentService,\n): IAttachmentClient {\n return new AttachmentClientImpl(service);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/concurrency.ts","../src/progress.ts","../src/client.ts"],"sourcesContent":["/**\n * Ordered result of one batch item. `index` always mirrors the input\n * position, so callers can correlate results with inputs regardless of\n * completion order, and successes are retained when siblings fail.\n */\nexport type BatchItemResult<R> =\n | { index: number; status: \"fulfilled\"; value: R }\n | { index: number; status: \"rejected\"; error: unknown };\n\nexport type RunWithConcurrencyOptions<R = unknown> = {\n /** Maximum simultaneously running workers. Must be a positive integer. */\n concurrency: number;\n /**\n * Whole-batch cancellation: unstarted items are rejected with the signal's\n * reason without ever starting, while already-started items keep running —\n * per-item signals are the mechanism for interrupting active work.\n */\n signal?: AbortSignal;\n /**\n * Called once per item as it settles, in completion order, including items\n * the signal skipped before they ever started. That completeness is the\n * point: a caller counting settlements must still reach `items.length`\n * after a whole-batch abort.\n */\n onSettled?: (result: BatchItemResult<R>) => void;\n};\n\n/**\n * Runs `worker` over `items` with a hard upper bound on simultaneous\n * executions. Bounding starts (not just transfers) is what keeps memory flat:\n * an item's preprocessing (hashing/buffering) only begins when a slot frees.\n */\nexport async function runWithConcurrency<T, R>(\n items: readonly T[],\n worker: (item: T, index: number) => Promise<R>,\n options: RunWithConcurrencyOptions<R>,\n): Promise<BatchItemResult<R>[]> {\n const { concurrency, signal, onSettled } = options;\n if (!Number.isInteger(concurrency) || concurrency < 1) {\n throw new Error(\n `concurrency must be a positive integer, got: ${concurrency}`,\n );\n }\n\n const results: BatchItemResult<R>[] = new Array<BatchItemResult<R>>(\n items.length,\n );\n let nextIndex = 0;\n\n async function runLane(): Promise<void> {\n for (;;) {\n const index = nextIndex++;\n if (index >= items.length) return;\n if (signal?.aborted) {\n results[index] = {\n index,\n status: \"rejected\",\n error: signalReason(signal),\n };\n onSettled?.(results[index]);\n continue;\n }\n try {\n const value = await worker(items[index], index);\n results[index] = { index, status: \"fulfilled\", value };\n } catch (error) {\n results[index] = { index, status: \"rejected\", error };\n }\n onSettled?.(results[index]);\n }\n }\n\n const lanes = Array.from(\n { length: Math.min(concurrency, items.length) },\n () => runLane(),\n );\n await Promise.all(lanes);\n return results;\n}\n\nfunction signalReason(signal: AbortSignal): unknown {\n return (\n (signal as { reason?: unknown }).reason ??\n new DOMException(\"The operation was aborted\", \"AbortError\")\n );\n}\n","/**\n * Byte-level progress for attachment transfers.\n *\n * `loaded`/`total` are per-stage, never per-operation. A per-operation\n * denominator would have to weight hashing against transfer, and a confirmed\n * dedup would shrink it mid-flight from 2N to N — the bar would run backwards.\n * Choosing that weighting is a UI decision the client cannot make.\n */\n\nexport type AttachmentStage =\n | \"hashing\"\n | \"reserving\"\n | \"uploading\"\n | \"requesting-download-target\"\n | \"downloading\"\n | \"done\"\n | \"error\";\n\nexport type AttachmentProgress = {\n stage: AttachmentStage;\n /** Bytes moved so far within `stage`. */\n loaded: number;\n /** Denominator for `stage`: file.size for uploads, header.sizeBytes for downloads. */\n total: number | undefined;\n /** True when the stage is running but bytes cannot (yet) be observed. */\n indeterminate: boolean;\n /** Only on a terminal `done` where dedup skipped the transfer entirely. */\n deduped?: true;\n};\n\nexport type AttachmentProgressListener = (progress: AttachmentProgress) => void;\n\nexport type AttachmentProgressOptions = {\n onProgress?: AttachmentProgressListener;\n /** Min ms between byte events within a stage. Default 100. 0 emits every observation. */\n throttleMs?: number;\n};\n\nexport const DEFAULT_PROGRESS_THROTTLE_MS = 100;\n\n/**\n * Fraction in 0..1 for a progress event. Indeterminate and unknown-total\n * events read as 0 — a bar cannot honestly show a position it does not know.\n * A zero total is complete by definition (the dedup case moves no bytes).\n */\nexport function progressFraction(progress: AttachmentProgress): number {\n if (progress.indeterminate) return 0;\n if (progress.total === undefined) return 0;\n if (progress.total === 0) return 1;\n const fraction = progress.loaded / progress.total;\n if (fraction < 0) return 0;\n return fraction > 1 ? 1 : fraction;\n}\n\nexport type ProgressEmitterOptions = AttachmentProgressOptions & {\n /** Injected clock so throttle tests stay synchronous. */\n now?: () => number;\n};\n\n/**\n * Emits `AttachmentProgress` for one single-item operation, upholding:\n *\n * - **I1** exactly one terminal event (`done` or `error`) — never both, never twice.\n * - **I2** on `done`: `indeterminate === false`, `total` is a number, `loaded === total`.\n * - **I3** `loaded` is non-decreasing within a stage and resets to 0 on stage change.\n * - **I4** the terminal event bypasses the throttle unconditionally.\n * - **I5** `error` carries the last known `loaded`/`total`.\n *\n * Leading edge: the first `bytes()` after a `stage()` always emits, so a bar\n * leaves 0 immediately. There is deliberately no trailing-edge timer — it\n * would leak on throw, could fire after the terminal event and violate I1's\n * ordering, and is unnecessary because `finish()` always emits\n * `loaded === total`, making a dropped last intra-stage tick invisible.\n */\nexport class ProgressEmitter {\n private readonly listener: AttachmentProgressListener | undefined;\n private readonly throttleMs: number;\n private readonly now: () => number;\n\n private currentStage: AttachmentStage | undefined;\n private loaded = 0;\n private total: number | undefined;\n private emittedBytesInStage = false;\n private lastEmitAt = Number.NEGATIVE_INFINITY;\n private settled = false;\n\n constructor(options?: ProgressEmitterOptions) {\n this.listener = options?.onProgress;\n this.throttleMs = options?.throttleMs ?? DEFAULT_PROGRESS_THROTTLE_MS;\n this.now = options?.now ?? (() => Date.now());\n }\n\n /** True when a listener is attached; lets callers skip instrumentation entirely. */\n get active(): boolean {\n return this.listener !== undefined;\n }\n\n /**\n * Enter a stage. Always emits and resets `loaded` to 0. Entry is always\n * indeterminate: no bytes have moved yet, and whether they can be observed\n * at all is only proven by a transport actually calling back.\n */\n stage(stage: AttachmentStage, options?: { total?: number }): void {\n if (this.settled) return;\n this.currentStage = stage;\n this.loaded = 0;\n this.total = options?.total;\n this.emittedBytesInStage = false;\n this.emit({\n stage,\n loaded: 0,\n total: this.total,\n indeterminate: true,\n });\n }\n\n /**\n * Report observed bytes within the current stage. Throttled by time, with\n * the first observation after a stage change always emitted.\n */\n bytes(loaded: number, total?: number): void {\n if (this.settled || this.currentStage === undefined) return;\n if (total !== undefined) this.total = total;\n if (loaded > this.loaded) this.loaded = loaded;\n\n const leadingEdge = !this.emittedBytesInStage;\n if (!leadingEdge && this.now() - this.lastEmitAt < this.throttleMs) return;\n this.emittedBytesInStage = true;\n this.emit({\n stage: this.currentStage,\n loaded: this.loaded,\n total: this.total,\n indeterminate: false,\n });\n }\n\n /**\n * Terminal success. `total` defaults to the bytes actually seen so a stage\n * with no known denominator still satisfies I2.\n */\n finish(override?: { loaded?: number; total?: number; deduped?: true }): void {\n if (this.settled) return;\n this.settled = true;\n const total = override?.total ?? this.total ?? this.loaded;\n const loaded = override?.loaded ?? total;\n this.emit({\n stage: \"done\",\n loaded,\n total,\n indeterminate: false,\n ...(override?.deduped ? { deduped: true as const } : {}),\n });\n }\n\n /** Terminal success for a confirmed dedup: no bytes moved, and none needed to. */\n finishDeduped(): void {\n this.finish({ loaded: 0, total: 0, deduped: true });\n }\n\n /** Terminal failure, carrying the last known byte counts. */\n fail(): void {\n if (this.settled) return;\n this.settled = true;\n this.emit({\n stage: \"error\",\n loaded: this.loaded,\n total: this.total,\n indeterminate: false,\n });\n }\n\n private emit(progress: AttachmentProgress): void {\n this.lastEmitAt = this.now();\n this.listener?.(progress);\n }\n}\n\nexport type ByteProgressHooks = {\n /** Cumulative bytes handed to the consumer. */\n onBytes?: (loaded: number) => void;\n /** The source ended and every byte reached the consumer. */\n onDone?: () => void;\n onError?: (error: unknown) => void;\n};\n\n/**\n * Count bytes as a consumer reads them.\n *\n * Uses the manual-reader form rather than `pipeThrough`, whose internal queue\n * reads far ahead of the consumer and would make `loaded` overstate what was\n * actually received. `highWaterMark: 0` is load-bearing for the same reason:\n * at the default of 1 the wrapper pulls one chunk the instant it is\n * constructed, so a caller who never reads still sees bytes counted.\n *\n * `cancel` propagates to the source, which is what keeps reader refcounts\n * (e.g. `KyselyAttachmentStore`'s) correct. Cancelling is an abandonment, not\n * a completion, so it reports `onError` — never `onDone` — carrying the\n * cancel reason the way an `AbortSignal` carries its own, with the DOM's\n * default `AbortError` when the canceller gave none.\n *\n * Returns `source` unchanged when no hook is supplied, so a caller with no\n * listener pays nothing.\n */\nexport function withByteProgress(\n source: ReadableStream<Uint8Array>,\n hooks: ByteProgressHooks,\n): ReadableStream<Uint8Array> {\n if (!hooks.onBytes && !hooks.onDone && !hooks.onError) return source;\n\n let loaded = 0;\n let cancelled = false;\n const reader = source.getReader();\n return new ReadableStream<Uint8Array>(\n {\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n // A cancel that lands while this read is in flight resolves it as\n // `done`, and closes the stream. Bailing out here is what stops an\n // abandoned transfer from reporting completion, and stops the\n // already-closed controller from throwing a fabricated error into\n // the catch below.\n if (cancelled) return;\n if (done) {\n hooks.onDone?.();\n controller.close();\n return;\n }\n loaded += value.byteLength;\n hooks.onBytes?.(loaded);\n controller.enqueue(value);\n } catch (err) {\n if (cancelled) return;\n hooks.onError?.(err);\n controller.error(err);\n }\n },\n cancel(reason) {\n cancelled = true;\n reader.cancel(reason).catch(() => {});\n hooks.onError?.(reason ?? abortError());\n },\n },\n { highWaterMark: 0 },\n );\n}\n\n/** What an `AbortSignal` carries when `abort()` is called with no reason. */\nfunction abortError(): DOMException {\n return new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport {\n runWithConcurrency,\n type BatchItemResult,\n type RunWithConcurrencyOptions,\n} from \"./concurrency.js\";\nimport { AttachmentAlreadyExists } from \"./errors.js\";\nexport type { AttachmentTransferStage } from \"./errors.js\";\nexport {\n runWithConcurrency,\n type BatchItemResult,\n type RunWithConcurrencyOptions,\n} from \"./concurrency.js\";\nimport type { IAttachmentService, IAttachmentUpload } from \"./interfaces.js\";\nimport {\n ProgressEmitter,\n withByteProgress,\n type AttachmentProgress,\n type AttachmentProgressOptions,\n} from \"./progress.js\";\nimport { createRef } from \"./ref.js\";\nimport { sha256Hex } from \"./replication/hash.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n AttachmentSendOptions,\n AttachmentUploadResult,\n HashFirstReserveAttachmentOptions,\n} from \"./types.js\";\n\nexport { AttachmentService } from \"./attachment-service.js\";\nexport {\n AttachmentAlreadyExists,\n AttachmentNotFound,\n AttachmentPending,\n AttachmentTransferError,\n HashMismatch,\n InvalidAttachmentRef,\n ReservationNotFound,\n SizeMismatch,\n UploadTooLarge,\n} from \"./errors.js\";\nexport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentStore,\n IAttachmentTransport,\n IAttachmentTransportFactory,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nexport { parseRef, createRef } from \"./ref.js\";\nexport type { ParsedRef } from \"./ref.js\";\nexport type {\n AttachmentDownloadOptions,\n AttachmentDownloadTarget,\n AttachmentDownloadTargetOptions,\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentSendOptions,\n AttachmentStatOptions,\n AttachmentStatus,\n AttachmentTransportConfig,\n AttachmentUploadResult,\n AttachmentTargetHeaders,\n AttachmentUploadTarget,\n HashFirstReserveAttachmentOptions,\n UploadFirstReserveAttachmentOptions,\n Reservation,\n ReserveAttachmentOptions,\n TransportFetchResult,\n TransportResponse,\n} from \"./types.js\";\nexport {\n parseAttachmentDownloadTarget,\n parseAttachmentUploadTarget,\n} from \"./targets.js\";\nexport {\n SwitchboardAttachmentTransport,\n type SwitchboardTransportConfig,\n RemoteReservationStore,\n type SwitchboardClientConfig,\n RemoteAttachmentUpload,\n RemoteAttachmentUploadFactory,\n RemoteAttachmentStore,\n createRemoteAttachmentService,\n createFetchUploadTransport,\n createXhrUploadTransport,\n type AttachmentUploadRequest,\n type AttachmentUploadResponse,\n type AttachmentUploadTransport,\n type XhrUploadTransportOptions,\n} from \"./switchboard/index.js\";\nexport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\nexport {\n DEFAULT_PROGRESS_THROTTLE_MS,\n progressFraction,\n ProgressEmitter,\n withByteProgress,\n type AttachmentProgress,\n type AttachmentProgressListener,\n type AttachmentProgressOptions,\n type AttachmentStage,\n type ByteProgressHooks,\n type ProgressEmitterOptions,\n} from \"./progress.js\";\n\nexport type PreprocessResult = {\n ref: AttachmentRef;\n hash: AttachmentHash;\n sizeBytes: number;\n options: HashFirstReserveAttachmentOptions;\n data: ReadableStream<Uint8Array>;\n stream: () => ReadableStream<Uint8Array>;\n};\n\nexport type AttachmentUploadInput = {\n file: Blob;\n fileName?: string;\n mimeType?: string;\n /** Per-item cancellation, checked between stages. */\n signal?: AbortSignal;\n};\n\n/**\n * Upload bytes that were already hashed by a separate `preprocess()` call.\n *\n * This is the canonical two-call flow: the ref must reach the document before\n * the bytes are committed, so callers preprocess, dispatch the operation, then\n * upload. Passing the result back in skips the hashing stage rather than\n * faking a zero-millisecond one.\n */\nexport type AttachmentPreprocessedUploadInput = {\n preprocessed: PreprocessResult;\n signal?: AbortSignal;\n};\n\n/**\n * Every remote download names the document that authorizes its ref; batches\n * may freely mix documents because the anchor travels with each item.\n */\nexport type AttachmentDownloadInput = {\n documentId: string;\n ref: AttachmentRef;\n signal?: AbortSignal;\n};\n\n/**\n * The document keeps its own name and type for an attachment (the same\n * bytes may appear under different names in different documents), so the\n * blob-producing conveniences let callers override what the server header\n * reports from upload time.\n */\nexport type AttachmentBlobOptions = {\n mimeType?: string;\n};\n\nexport type AttachmentSaveOptions = {\n fileName?: string;\n mimeType?: string;\n};\n\nexport type AttachmentBlobResult = {\n blob: Blob;\n header: AttachmentHeader;\n};\n\nexport type AttachmentObjectUrl = {\n /** Ready for img/iframe/video src. Pins memory until revoke() is called. */\n url: string;\n header: AttachmentHeader;\n revoke: () => void;\n};\n\nexport type AttachmentShareLinkInput = {\n documentId: string;\n ref: AttachmentRef;\n /** Requested link lifetime in seconds; the server clamps to its maximum. */\n expiresIn?: number;\n signal?: AbortSignal;\n};\n\n/**\n * A self-contained public URL: anyone holding it can fetch the bytes until\n * expiresAtUtc, with no login and no document access. Minting one requires\n * document read access; once minted it cannot be revoked before expiry.\n */\nexport type AttachmentShareLink = {\n url: string;\n expiresAtUtc: string;\n};\n\n/**\n * Batch progress is per item, carrying the item's `index`. There is\n * deliberately no batch-wide byte total: weighting items against each other is\n * a presentation decision the client cannot make, and a confirmed dedup would\n * shrink the denominator mid-flight.\n */\nexport type AttachmentBatchOptions = {\n /** Bounds preprocessing and transfer together. Defaults to 4. */\n concurrency?: number;\n /** Whole-batch cancellation: stops unstarted items. */\n signal?: AbortSignal;\n onProgress?: (progress: AttachmentProgress & { index: number }) => void;\n /**\n * Fired as each item settles, including items the signal skipped before they\n * started, so `settled` always reaches `total`.\n */\n onItemSettled?: (counts: AttachmentBatchCounts) => void;\n /** Min ms between byte events, applied per item rather than batch-wide. */\n throttleMs?: number;\n};\n\nexport type AttachmentBatchCounts = {\n settled: number;\n completed: number;\n failed: number;\n total: number;\n};\n\nexport const DEFAULT_ATTACHMENT_BATCH_CONCURRENCY = 4;\n\nexport interface IAttachmentClient {\n preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult>;\n reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult>;\n /**\n * Hash, reserve, and transfer one file; a confirmed dedup skips the transfer\n * and reports a terminal `done` carrying `deduped: true` and zero bytes.\n *\n * Also accepts an already-preprocessed payload, so the canonical\n * preprocess -> dispatch -> upload flow reports progress too.\n */\n upload(\n input: AttachmentUploadInput | AttachmentPreprocessedUploadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentUploadResult>;\n /**\n * Document-authorized download of one ref.\n *\n * Resolves as soon as the byte stream is available, with `downloading` as\n * the last event emitted and `loaded: 0`. Byte events and the terminal\n * `done` arrive as the caller reads the stream — and never arrive at all if\n * the caller abandons it.\n */\n download(\n input: AttachmentDownloadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentResponse>;\n /**\n * Document-authorized download materialized as a typed Blob. The Blob's\n * type comes from the server header unless overridden — that's what makes\n * browsers render PDFs inline and images correctly.\n */\n downloadBlob(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentBlobResult>;\n /**\n * Download and hand the bytes to the browser's save-file flow. Browser\n * only. fileName defaults to the server header's; pass the document's own\n * name for per-document naming.\n */\n saveAttachment(\n input: AttachmentDownloadInput,\n options?: AttachmentSaveOptions & AttachmentProgressOptions,\n ): Promise<void>;\n /**\n * Download and expose the bytes as an object URL for inline rendering\n * (img/iframe/video src). Callers MUST call revoke() when done — the URL\n * pins the blob in memory until then.\n */\n downloadObjectUrl(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentObjectUrl>;\n /**\n * Mint a public share link: a presigned URL anyone can fetch until it\n * expires, with no login. Authorized exactly like a download (document\n * read access + the reference index). Requires a presigned-capable\n * storage backend (S3); rejects when the server answers with an\n * authenticated switchboard target, which would not be public.\n */\n getShareLink(input: AttachmentShareLinkInput): Promise<AttachmentShareLink>;\n uploadMany(\n inputs: readonly AttachmentUploadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentUploadResult>[]>;\n downloadMany(\n inputs: readonly AttachmentDownloadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentResponse>[]>;\n}\n\n/**\n * Duck-typed dedup detection: bundlers (notably Vite dev pre-bundling) can\n * load two copies of this package's error classes, one for the service and\n * one for the client wrapper, making a plain instanceof check miss the\n * cross-copy throw. Name plus payload shape identifies the error reliably.\n */\nfunction isAttachmentAlreadyExists(\n err: unknown,\n): err is AttachmentAlreadyExists {\n if (err instanceof AttachmentAlreadyExists) return true;\n return (\n err instanceof Error &&\n err.name === \"AttachmentAlreadyExists\" &&\n typeof (err as { hash?: unknown }).hash === \"string\" &&\n typeof (err as { ref?: unknown }).ref === \"string\"\n );\n}\n\n// Nothing references the blob yet, so the server header is not readable.\nfunction dedupedHeader(\n hash: AttachmentHash,\n options: HashFirstReserveAttachmentOptions,\n): AttachmentHeader {\n const now = new Date().toISOString();\n return {\n hash,\n mimeType: options.mimeType,\n fileName: options.fileName,\n sizeBytes: options.sizeBytes,\n extension: options.extension ?? null,\n status: \"available\",\n source: \"local\",\n createdAtUtc: now,\n lastAccessedAtUtc: now,\n expiresAtUtc: null,\n };\n}\n\nfunction streamFromBuffer(buf: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(buf);\n controller.close();\n },\n });\n}\n\n/** Stamps an item's index onto its progress events. */\nfunction itemProgress(\n options: AttachmentBatchOptions | undefined,\n index: number,\n): AttachmentProgressOptions | undefined {\n if (!options?.onProgress && options?.throttleMs === undefined) {\n return undefined;\n }\n return {\n ...(options.onProgress\n ? {\n onProgress: (progress: AttachmentProgress) =>\n options.onProgress?.({ ...progress, index }),\n }\n : {}),\n ...(options.throttleMs !== undefined\n ? { throttleMs: options.throttleMs }\n : {}),\n };\n}\n\nfunction batchRunOptions<R>(\n total: number,\n options: AttachmentBatchOptions | undefined,\n): RunWithConcurrencyOptions<R> {\n const onItemSettled = options?.onItemSettled;\n let settled = 0;\n let completed = 0;\n let failed = 0;\n return {\n concurrency: options?.concurrency ?? DEFAULT_ATTACHMENT_BATCH_CONCURRENCY,\n ...(options?.signal ? { signal: options.signal } : {}),\n ...(onItemSettled\n ? {\n onSettled: (result: BatchItemResult<R>) => {\n settled += 1;\n if (result.status === \"fulfilled\") completed += 1;\n else failed += 1;\n onItemSettled({ settled, completed, failed, total });\n },\n }\n : {}),\n };\n}\n\nclass AttachmentClientImpl implements IAttachmentClient {\n constructor(private readonly service: IAttachmentService) {}\n\n async preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult> {\n const buf = await file.arrayBuffer();\n const bytes = new Uint8Array(buf);\n const hash = await sha256Hex(bytes);\n const ref = createRef(hash);\n const sizeBytes = file.size;\n const mimeType = opts?.mimeType ?? file.type;\n const fileName =\n opts?.fileName ?? (file instanceof File ? file.name : \"attachment\");\n const options: HashFirstReserveAttachmentOptions = {\n mimeType,\n fileName,\n clientHash: hash,\n sizeBytes,\n };\n const data = streamFromBuffer(bytes);\n const stream = (): ReadableStream<Uint8Array> => streamFromBuffer(bytes);\n return { ref, hash, sizeBytes, options, data, stream };\n }\n\n /**\n * Reserve, distinguishing a confirmed dedup from a live upload handle in the\n * type rather than with a flag: the two outcomes move a different number of\n * bytes, and every caller has to account for that.\n */\n private async reserveOrDedup(\n options: HashFirstReserveAttachmentOptions,\n ): Promise<\n | { kind: \"handle\"; handle: IAttachmentUpload }\n | { kind: \"deduped\"; result: AttachmentUploadResult }\n > {\n try {\n return { kind: \"handle\", handle: await this.service.reserve(options) };\n } catch (err) {\n if (!isAttachmentAlreadyExists(err)) throw err;\n return {\n kind: \"deduped\",\n result: {\n hash: err.hash,\n ref: err.ref,\n header: dedupedHeader(err.hash, options),\n },\n };\n }\n }\n\n async reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult> {\n const outcome = await this.reserveOrDedup(options);\n if (outcome.kind === \"deduped\") return outcome.result;\n return send(outcome.handle);\n }\n\n async upload(\n input: AttachmentUploadInput | AttachmentPreprocessedUploadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentUploadResult> {\n const emitter = new ProgressEmitter(options);\n const signal = input.signal;\n try {\n signal?.throwIfAborted();\n\n let preprocessed: PreprocessResult;\n if (\"preprocessed\" in input) {\n preprocessed = input.preprocessed;\n } else {\n // Hashing is genuinely unobservable: preprocess() runs one\n // crypto.subtle.digest over the whole buffer and WebCrypto has no\n // streaming digest. The stage is still reported, with a denominator\n // and indeterminate never cleared, so a UI can name what it waits on.\n emitter.stage(\"hashing\", { total: input.file.size });\n preprocessed = await this.preprocess(input.file, {\n ...(input.fileName !== undefined ? { fileName: input.fileName } : {}),\n ...(input.mimeType !== undefined ? { mimeType: input.mimeType } : {}),\n });\n }\n const sizeBytes = preprocessed.sizeBytes;\n\n signal?.throwIfAborted();\n emitter.stage(\"reserving\");\n const outcome = await this.reserveOrDedup(preprocessed.options);\n\n // A confirmed dedup never enters `uploading` — the server already holds\n // these bytes, so none are sent. Reporting zero of zero keeps the byte\n // model honest instead of stalling at 0 and then jumping to done.\n if (outcome.kind === \"deduped\") {\n emitter.finishDeduped();\n return outcome.result;\n }\n\n signal?.throwIfAborted();\n emitter.stage(\"uploading\", { total: sizeBytes });\n // `onProgress` only when someone is listening. The XHR transport keys\n // its `upload.onprogress` registration off the option being present, and\n // that registration alone forces a CORS preflight on a presigned PUT —\n // a cost an unwatched upload must not pay for a callback that would\n // discard every event anyway.\n const sendOptions: AttachmentSendOptions = {\n ...(emitter.active\n ? {\n onProgress: (loaded: number, total?: number) =>\n emitter.bytes(loaded, total ?? sizeBytes),\n }\n : {}),\n ...(signal ? { signal } : {}),\n };\n const result = await outcome.handle.send(\n preprocessed.stream(),\n sendOptions,\n );\n emitter.finish();\n return result;\n } catch (err) {\n emitter.fail();\n throw err;\n }\n }\n\n /**\n * The single site where download bytes are instrumented.\n *\n * It sits here, above the service, rather than in any store or transport:\n * one logical get can be several byte streams underneath (an evicted\n * attachment is fetched from the transport, written to disk, then re-read),\n * so counting lower down would double-count that path and still miss\n * local-filesystem reads. Wrapping what `service.get` returns covers the\n * presigned, switchboard, legacy and local paths by construction, and\n * `downloadBlob`/`saveAttachment`/`downloadObjectUrl` inherit it.\n */\n private async getWithProgress(\n input: AttachmentDownloadInput,\n emitter: ProgressEmitter,\n ): Promise<AttachmentResponse> {\n const { header, body } = await this.service.get(input.ref, {\n documentId: input.documentId,\n signal: input.signal,\n });\n emitter.stage(\"downloading\", { total: header.sizeBytes });\n if (!emitter.active) return { header, body };\n return {\n header,\n body: withByteProgress(body, {\n onBytes: (loaded) => emitter.bytes(loaded),\n onDone: () => emitter.finish(),\n onError: () => emitter.fail(),\n }),\n };\n }\n\n async download(\n input: AttachmentDownloadInput,\n options?: AttachmentProgressOptions,\n ): Promise<AttachmentResponse> {\n const emitter = new ProgressEmitter(options);\n try {\n input.signal?.throwIfAborted();\n emitter.stage(\"requesting-download-target\");\n return await this.getWithProgress(input, emitter);\n } catch (err) {\n emitter.fail();\n throw err;\n }\n }\n\n async downloadBlob(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentBlobResult> {\n const emitter = new ProgressEmitter(options);\n try {\n input.signal?.throwIfAborted();\n emitter.stage(\"requesting-download-target\");\n const { header, body } = await this.getWithProgress(input, emitter);\n const reader = body.getReader();\n const chunks: BlobPart[] = [];\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value as BlobPart);\n }\n const blob = new Blob(chunks, {\n type: options?.mimeType ?? header.mimeType,\n });\n // Draining the stream already finished the emitter; latching makes this\n // a no-op there, and a guarantee for any body that closes without one.\n emitter.finish();\n return { blob, header };\n } catch (err) {\n emitter.fail();\n throw err;\n }\n }\n\n async saveAttachment(\n input: AttachmentDownloadInput,\n options?: AttachmentSaveOptions & AttachmentProgressOptions,\n ): Promise<void> {\n if (typeof document === \"undefined\") {\n throw new Error(\n \"saveAttachment requires a browser environment; use downloadBlob elsewhere\",\n );\n }\n const { blob, header } = await this.downloadBlob(input, options);\n const url = URL.createObjectURL(blob);\n try {\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = options?.fileName ?? header.fileName;\n anchor.click();\n } finally {\n URL.revokeObjectURL(url);\n }\n }\n\n async downloadObjectUrl(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions & AttachmentProgressOptions,\n ): Promise<AttachmentObjectUrl> {\n const { blob, header } = await this.downloadBlob(input, options);\n const url = URL.createObjectURL(blob);\n let revoked = false;\n return {\n url,\n header,\n revoke: () => {\n if (revoked) return;\n revoked = true;\n URL.revokeObjectURL(url);\n },\n };\n }\n\n async getShareLink(\n input: AttachmentShareLinkInput,\n ): Promise<AttachmentShareLink> {\n input.signal?.throwIfAborted();\n const target = await this.service.getDownloadTarget(input.ref, {\n documentId: input.documentId,\n ...(input.expiresIn !== undefined ? { expiresIn: input.expiresIn } : {}),\n ...(input.signal !== undefined ? { signal: input.signal } : {}),\n });\n if (target.kind !== \"presigned-get\") {\n throw new Error(\n \"Public share links require a presigned-capable storage backend (S3); this server answered with an authenticated target\",\n );\n }\n return { url: target.url, expiresAtUtc: target.expiresAtUtc };\n }\n\n uploadMany(\n inputs: readonly AttachmentUploadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentUploadResult>[]> {\n return runWithConcurrency(\n inputs,\n (input, index) => this.upload(input, itemProgress(options, index)),\n batchRunOptions(inputs.length, options),\n );\n }\n\n /**\n * `concurrency` bounds download-target negotiation, not simultaneous byte\n * transfer: each item resolves when its stream is handed over, and the bytes\n * move only as the caller reads. Byte progress makes that pre-existing\n * behavior visible; it does not change it.\n */\n downloadMany(\n inputs: readonly AttachmentDownloadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentResponse>[]> {\n return runWithConcurrency(\n inputs,\n (input, index) => this.download(input, itemProgress(options, index)),\n batchRunOptions(inputs.length, options),\n );\n }\n}\n\nexport function createAttachmentClient(\n service: IAttachmentService,\n): IAttachmentClient {\n return new AttachmentClientImpl(service);\n}\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ //#region src/replication/hash.ts
2
+ /**
3
+ * Lowercase SHA-256 hex of `bytes` -- the `attachment://v1:` hash format
4
+ * (`packages/reactor/src/attachments/types.ts`).
5
+ *
6
+ * WebCrypto, so the same code runs in Node and in a browser realm. There is no
7
+ * streaming digest in WebCrypto, which is why verification happens over
8
+ * collected bytes.
9
+ */
10
+ async function sha256Hex(bytes) {
11
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", digestable(bytes));
12
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
13
+ }
14
+ /** WebCrypto refuses a SharedArrayBuffer-backed view, so only that one is copied. */
15
+ function digestable(bytes) {
16
+ if (typeof SharedArrayBuffer !== "undefined" && bytes.buffer instanceof SharedArrayBuffer) {
17
+ const copy = new Uint8Array(bytes.byteLength);
18
+ copy.set(bytes);
19
+ return copy;
20
+ }
21
+ return bytes;
22
+ }
23
+ //#endregion
24
+ export { sha256Hex as t };
25
+
26
+ //# sourceMappingURL=hash-CKHeI4AH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash-CKHeI4AH.js","names":[],"sources":["../src/replication/hash.ts"],"sourcesContent":["import type { AttachmentHash } from \"@powerhousedao/reactor\";\n\n/**\n * Lowercase SHA-256 hex of `bytes` -- the `attachment://v1:` hash format\n * (`packages/reactor/src/attachments/types.ts`).\n *\n * WebCrypto, so the same code runs in Node and in a browser realm. There is no\n * streaming digest in WebCrypto, which is why verification happens over\n * collected bytes.\n */\nexport async function sha256Hex(bytes: Uint8Array): Promise<AttachmentHash> {\n const digest = await globalThis.crypto.subtle.digest(\n \"SHA-256\",\n digestable(bytes),\n );\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/** WebCrypto refuses a SharedArrayBuffer-backed view, so only that one is copied. */\nfunction digestable(bytes: Uint8Array): Uint8Array<ArrayBuffer> {\n if (\n typeof SharedArrayBuffer !== \"undefined\" &&\n bytes.buffer instanceof SharedArrayBuffer\n ) {\n const copy = new Uint8Array(bytes.byteLength);\n copy.set(bytes);\n return copy;\n }\n return bytes as Uint8Array<ArrayBuffer>;\n}\n"],"mappings":";;;;;;;;;AAUA,eAAsB,UAAU,OAA4C;CAC1E,MAAM,SAAS,MAAM,WAAW,OAAO,OAAO,OAC5C,WACA,WAAW,KAAK,CAClB;CACA,OAAO,MAAM,KAAK,IAAI,WAAW,MAAM,CAAC,CAAC,CACtC,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAAE;AACZ;;AAGA,SAAS,WAAW,OAA4C;CAC9D,IACE,OAAO,sBAAsB,eAC7B,MAAM,kBAAkB,mBACxB;EACA,MAAM,OAAO,IAAI,WAAW,MAAM,UAAU;EAC5C,KAAK,IAAI,KAAK;EACd,OAAO;CACT;CACA,OAAO;AACT"}