grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: batch Instagram import writes with applyWrites

Switch import from individual createRecord calls to batched applyWrites,
reducing round trips from N+1 to 2 per post. Bump hatk to alpha.57 which
includes the applyWrites proxy and a fix for firehose commit indexing
that matched blocks by $type instead of by CID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+141 -37
+37 -30
app/routes/settings/import/+page.svelte
··· 92 92 importProgress.current++ 93 93 try { 94 94 const createdAt = post.createdAt.toISOString() 95 - const photoUris: string[] = [] 96 95 97 - // Upload photos 96 + // 1. Upload blobs (must be individual calls) 97 + const blobs: Array<{ blob: unknown; width: number; height: number }> = [] 98 98 for (const photo of post.photos) { 99 99 const base64 = photo.dataUrl.split(',')[1] 100 100 const binary = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)) 101 - const blob = new Blob([binary], { type: 'image/jpeg' }) 101 + const blobFile = new Blob([binary], { type: 'image/jpeg' }) 102 + const uploadResult = await callXrpc('dev.hatk.uploadBlob', blobFile as never) 103 + const blobRef = (uploadResult as { blob: unknown }).blob 104 + blobs.push({ blob: blobRef, width: photo.width, height: photo.height }) 105 + } 102 106 103 - const uploadResult = await callXrpc('dev.hatk.uploadBlob', blob as any) 107 + // 2. Batch photo + gallery records in one applyWrites 108 + const createWrite = (collection: string, value: Record<string, unknown>) => ({ 109 + $type: 'dev.hatk.applyWrites#create' as const, 110 + collection, 111 + value, 112 + }) 104 113 105 - const photoResult = await callXrpc('dev.hatk.createRecord', { 106 - collection: 'social.grain.photo', 107 - record: { 108 - photo: (uploadResult as any).blob, 109 - aspectRatio: { width: photo.width, height: photo.height }, 114 + const writes = [ 115 + ...blobs.map((b) => 116 + createWrite('social.grain.photo', { 117 + photo: b.blob, 118 + aspectRatio: { width: b.width, height: b.height }, 110 119 createdAt, 111 - }, 112 - }) 113 - photoUris.push((photoResult as any).uri as string) 114 - } 115 - 116 - // Create gallery 117 - const galleryResult = await callXrpc('dev.hatk.createRecord', { 118 - collection: 'social.grain.gallery', 119 - record: { 120 + }), 121 + ), 122 + createWrite('social.grain.gallery', { 120 123 title: galleryTitle(post.createdAt), 121 124 ...(post.description.trim() ? { description: post.description.trim() } : {}), 122 125 ...(post.labels.length > 0 ··· 128 131 } 129 132 : {}), 130 133 createdAt, 131 - }, 132 - }) 133 - const galleryUri = (galleryResult as any).uri as string 134 + }), 135 + ] 134 136 135 - // Create gallery items 136 - for (let i = 0; i < photoUris.length; i++) { 137 - await callXrpc('dev.hatk.createRecord', { 138 - collection: 'social.grain.gallery.item', 139 - record: { 137 + const result = await callXrpc('dev.hatk.applyWrites', { writes }) 138 + const results = (result as { results?: Array<{ uri?: string; cid?: string }> }).results ?? [] 139 + 140 + const photoUris = results.slice(0, blobs.length).map((r) => r.uri!) 141 + const galleryUri = results[blobs.length]?.uri 142 + // 3. Batch gallery items in a second applyWrites 143 + if (galleryUri && photoUris.length > 0) { 144 + const itemWrites = photoUris.map((photoUri: string, i: number) => 145 + createWrite('social.grain.gallery.item', { 140 146 gallery: galleryUri, 141 - item: photoUris[i], 147 + item: photoUri, 142 148 position: i, 143 149 createdAt, 144 - }, 145 - }) 150 + }), 151 + ) 152 + await callXrpc('dev.hatk.applyWrites', { writes: itemWrites }) 146 153 } 147 154 148 155 importedCount++ 149 - } catch (err: any) { 156 + } catch (err: unknown) { 150 157 console.error(`Failed to import post ${post.index}:`, err) 151 158 error = `Failed on post ${importProgress.current} of ${importProgress.total}. ${importedCount} imported successfully.` 152 159 step = 'review'
+2 -2
hatk.generated.client.ts
··· 3 3 // to avoid pulling in server-only dependencies. 4 4 export type { XrpcSchema } from './hatk.generated.ts' 5 5 import type { XrpcSchema } from './hatk.generated.ts' 6 - export type { BskyActorProfile, Post, Postgate, Threadgate, BskyGraphFollow, Declaration, CreateReport, DescribeCollections, DescribeFeeds, DescribeLabels, GetFeed, GetPreferences, GetRecord, GetRecords, PutPreference, SearchRecords, UploadBlob, Search, GrainActorProfile, Comment, Favorite, Gallery, Item, Block, GrainGraphFollow, MuteActor, UnmuteActor, Photo, Exif, Story, DeleteGallery, GetActorFavorites, GetActorProfile, GetBlocks, GetCameras, GetCommentThread, GetFollowers, GetFollowing, GetGallery, GetKnownFollowers, GetLocations, GetMutes, GetNotifications, GetStories, GetStory, GetStoryArchive, GetStoryAuthors, GetSuggestedFollows, SearchActorsTypeahead, SearchGalleries, SearchProfiles, RecordRegistry, CreateRecord, DeleteRecord, PutRecord, Nux, MutedWord, SavedFeed, StatusView, BskyActorDefsProfileView, BskyActorDefsViewerState, FeedViewPref, LabelersPref, InterestsPref, KnownFollowers, MutedWordsPref, SavedFeedsPref, ThreadViewPref, DeclaredAgePref, HiddenPostsPref, LabelerPrefItem, AdultContentPref, BskyAppStatePref, ContentLabelPref, ProfileViewBasic, SavedFeedsPrefV2, VerificationView, ProfileAssociated, VerificationPrefs, VerificationState, PersonalDetailsPref, BskyActorDefsProfileViewDetailed, BskyAppProgressGuide, LiveEventPreferences, ProfileAssociatedChat, ProfileAssociatedGerm, PostInteractionSettingsPref, ProfileAssociatedActivitySubscription, BskyEmbedDefsAspectRatio, ExternalView, External, ViewExternal, ImagesView, Image, ViewImage, RecordView, ViewRecord, ViewBlocked, ViewDetached, ViewNotFound, RecordWithMediaView, VideoView, Caption, PostView, BskyFeedDefsReplyRef, ReasonPin, BlockedPost, Interaction, BskyFeedDefsViewerState, FeedViewPost, NotFoundPost, ReasonRepost, BlockedAuthor, GeneratorView, ThreadContext, ThreadViewPost, ThreadgateView, SkeletonFeedPost, SkeletonReasonPin, GeneratorViewerState, SkeletonReasonRepost, Entity, PostReplyRef, TextSlice, DisableRule, ListRule, MentionRule, FollowerRule, FollowingRule, ListView, ListItemView, Relationship, ListViewBasic, NotFoundActor, ListViewerState, StarterPackView, StarterPackViewBasic, LabelerView, LabelerPolicies, LabelerViewerState, LabelerViewDetailed, Preference, Preferences, RecordDeleted, ChatPreference, ActivitySubscription, FilterablePreference, SubjectActivitySubscription, Tag, Link, Mention, ByteSlice, Label, SelfLabels, SelfLabel, LabelValueDefinition, LabelValueDefinitionStrings, DeclarationMessageMe, RepoRef, LabelDefinition, LabelLocale, Result, MentionLabel, EmbedInfo, SearchAspectRatio, SubscopeInfo, GrainActorDefsProfileView, GrainActorDefsProfileViewDetailed, GrainActorDefsMessageMe, GrainActorDefsViewerState, CommentView, GrainDefsAspectRatio, GalleryView, CrossPostInfo, GrainGalleryDefsViewerState, PhotoView, ExifView, GalleryState, StoryView, GrainStoryDefsViewerState, BlockItem, CameraItem, GetFollowersFollowerItem, GetFollowersViewerState, FollowingItem, GetFollowingViewerState, GetKnownFollowersFollowerItem, LocationItem, MuteItem, NotificationItem, StoryAuthor, SuggestedItem, ProfileSearchResult } from './hatk.generated.ts' 6 + export type { BskyActorProfile, Post, Postgate, Threadgate, BskyGraphFollow, Declaration, ApplyWrites, CreateReport, DescribeCollections, DescribeFeeds, DescribeLabels, GetFeed, GetPreferences, GetRecord, GetRecords, PutPreference, SearchRecords, UploadBlob, Search, GrainActorProfile, Comment, Favorite, Gallery, Item, Block, GrainGraphFollow, MuteActor, UnmuteActor, Photo, Exif, Story, DeleteGallery, GetActorFavorites, GetActorProfile, GetBlocks, GetCameras, GetCommentThread, GetFollowers, GetFollowing, GetGallery, GetKnownFollowers, GetLocations, GetMutes, GetNotifications, GetStories, GetStory, GetStoryArchive, GetStoryAuthors, GetSuggestedFollows, SearchActorsTypeahead, SearchGalleries, SearchProfiles, RecordRegistry, CreateRecord, DeleteRecord, PutRecord, Nux, MutedWord, SavedFeed, StatusView, BskyActorDefsProfileView, BskyActorDefsViewerState, FeedViewPref, LabelersPref, InterestsPref, KnownFollowers, MutedWordsPref, SavedFeedsPref, ThreadViewPref, DeclaredAgePref, HiddenPostsPref, LabelerPrefItem, AdultContentPref, BskyAppStatePref, ContentLabelPref, ProfileViewBasic, SavedFeedsPrefV2, VerificationView, ProfileAssociated, VerificationPrefs, VerificationState, PersonalDetailsPref, BskyActorDefsProfileViewDetailed, BskyAppProgressGuide, LiveEventPreferences, ProfileAssociatedChat, ProfileAssociatedGerm, PostInteractionSettingsPref, ProfileAssociatedActivitySubscription, BskyEmbedDefsAspectRatio, ExternalView, External, ViewExternal, ImagesView, Image, ViewImage, RecordView, ViewRecord, ViewBlocked, ViewDetached, ViewNotFound, RecordWithMediaView, VideoView, Caption, PostView, BskyFeedDefsReplyRef, ReasonPin, BlockedPost, Interaction, BskyFeedDefsViewerState, FeedViewPost, NotFoundPost, ReasonRepost, BlockedAuthor, GeneratorView, ThreadContext, ThreadViewPost, ThreadgateView, SkeletonFeedPost, SkeletonReasonPin, GeneratorViewerState, SkeletonReasonRepost, Entity, PostReplyRef, TextSlice, DisableRule, ListRule, MentionRule, FollowerRule, FollowingRule, ListView, ListItemView, Relationship, ListViewBasic, NotFoundActor, ListViewerState, StarterPackView, StarterPackViewBasic, LabelerView, LabelerPolicies, LabelerViewerState, LabelerViewDetailed, Preference, Preferences, RecordDeleted, ChatPreference, ActivitySubscription, FilterablePreference, SubjectActivitySubscription, Tag, Link, Mention, ByteSlice, Label, SelfLabels, SelfLabel, LabelValueDefinition, LabelValueDefinitionStrings, DeclarationMessageMe, Create, Update, Delete, CreateResult, UpdateResult, DeleteResult, RepoRef, LabelDefinition, LabelLocale, Result, MentionLabel, EmbedInfo, SearchAspectRatio, SubscopeInfo, GrainActorDefsProfileView, GrainActorDefsProfileViewDetailed, GrainActorDefsMessageMe, GrainActorDefsViewerState, CommentView, GrainDefsAspectRatio, GalleryView, CrossPostInfo, GrainGalleryDefsViewerState, PhotoView, ExifView, GalleryState, StoryView, GrainStoryDefsViewerState, BlockItem, CameraItem, GetFollowersFollowerItem, GetFollowersViewerState, FollowingItem, GetFollowingViewerState, GetKnownFollowersFollowerItem, LocationItem, MuteItem, NotificationItem, StoryAuthor, SuggestedItem, ProfileSearchResult } from './hatk.generated.ts' 7 7 8 - const _procedures = new Set(['dev.hatk.createRecord', 'dev.hatk.createReport', 'dev.hatk.deleteRecord', 'dev.hatk.putPreference', 'dev.hatk.putRecord', 'social.grain.graph.muteActor', 'social.grain.graph.unmuteActor', 'social.grain.unspecced.deleteGallery']) 8 + const _procedures = new Set(['dev.hatk.applyWrites', 'dev.hatk.createRecord', 'dev.hatk.createReport', 'dev.hatk.deleteRecord', 'dev.hatk.putPreference', 'dev.hatk.putRecord', 'social.grain.graph.muteActor', 'social.grain.graph.unmuteActor', 'social.grain.unspecced.deleteGallery']) 9 9 const _blobInputs = new Set(['dev.hatk.uploadBlob']) 10 10 11 11 type CallArg<K extends keyof XrpcSchema> =
+10
hatk.generated.ts
··· 31 31 const addressLex = {"lexicon":1,"id":"community.lexicon.location.address","defs":{"main":{"type":"object","description":"A physical location in the form of a street address.","required":["country"],"properties":{"country":{"type":"string","description":"The ISO 3166 country code. Preferably the 2-letter code.","minLength":2,"maxLength":10},"postalCode":{"type":"string","description":"The postal code of the location."},"region":{"type":"string","description":"The administrative region of the country. For example, a state in the USA."},"locality":{"type":"string","description":"The locality of the region. For example, a city in the USA."},"street":{"type":"string","description":"The street address."},"name":{"type":"string","description":"The name of the location."}}}}} as const 32 32 const geoLex = {"lexicon":1,"id":"community.lexicon.location.geo","defs":{"main":{"type":"object","description":"A physical location in the form of a WGS84 coordinate.","required":["latitude","longitude"],"properties":{"latitude":{"type":"string"},"longitude":{"type":"string"},"altitude":{"type":"string"},"name":{"type":"string","description":"The name of the location."}}}}} as const 33 33 const hthreeLex = {"lexicon":1,"id":"community.lexicon.location.hthree","defs":{"main":{"type":"object","description":"A physical location in the form of a H3 encoded location.","required":["value"],"properties":{"value":{"type":"string","description":"The h3 encoded location."},"name":{"type":"string","description":"The name of the location."}}}}} as const 34 + const applyWritesLex = {"lexicon":1,"id":"dev.hatk.applyWrites","defs":{"main":{"type":"procedure","description":"Apply multiple record writes in a single atomic PDS transaction.","input":{"encoding":"application/json","schema":{"type":"object","required":["writes"],"properties":{"writes":{"type":"array","items":{"type":"union","refs":["#create","#update","#delete"]}}}}},"output":{"encoding":"application/json","schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"union","refs":["#createResult","#updateResult","#deleteResult"]}}}}}},"create":{"type":"object","required":["collection","value"],"properties":{"collection":{"type":"string"},"rkey":{"type":"string"},"value":{"type":"unknown"}}},"update":{"type":"object","required":["collection","rkey","value"],"properties":{"collection":{"type":"string"},"rkey":{"type":"string"},"value":{"type":"unknown"}}},"delete":{"type":"object","required":["collection","rkey"],"properties":{"collection":{"type":"string"},"rkey":{"type":"string"}}},"createResult":{"type":"object","required":["uri","cid"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"}}},"updateResult":{"type":"object","required":["uri","cid"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"}}},"deleteResult":{"type":"object","properties":{}}}} as const 34 35 const createRecordLex = {"lexicon":1,"id":"dev.hatk.createRecord","defs":{"main":{"type":"procedure","description":"Create a record via the user's PDS.","input":{"encoding":"application/json","schema":{"type":"object","required":["collection","repo","record"],"properties":{"collection":{"type":"string"},"repo":{"type":"string","format":"did"},"record":{"type":"unknown"}}}},"output":{"encoding":"application/json","schema":{"type":"object","properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"}}}}}}} as const 35 36 const createReportLex = {"lexicon":1,"id":"dev.hatk.createReport","defs":{"main":{"type":"procedure","description":"Report an account or record for moderation review.","input":{"encoding":"application/json","schema":{"type":"object","required":["subject","label"],"properties":{"subject":{"type":"union","description":"The account or record being reported.","refs":["#repoRef","com.atproto.repo.strongRef"]},"label":{"type":"string","description":"Label identifier for the report reason."},"reason":{"type":"string","maxLength":2000,"description":"Optional free-text explanation."}}}},"output":{"encoding":"application/json","schema":{"type":"object","required":["id","subject","label","reportedBy","createdAt"],"properties":{"id":{"type":"integer"},"subject":{"type":"unknown"},"label":{"type":"string"},"reason":{"type":"string"},"reportedBy":{"type":"string","format":"did"},"createdAt":{"type":"string","format":"datetime"}}}}},"repoRef":{"type":"object","required":["did"],"properties":{"did":{"type":"string","format":"did"}}}}} as const 36 37 const deleteRecordLex = {"lexicon":1,"id":"dev.hatk.deleteRecord","defs":{"main":{"type":"procedure","description":"Delete a record via the user's PDS.","input":{"encoding":"application/json","schema":{"type":"object","required":["collection","rkey"],"properties":{"collection":{"type":"string"},"rkey":{"type":"string"}}}},"output":{"encoding":"application/json","schema":{"type":"object","properties":{}}}}}} as const ··· 113 114 'community.lexicon.location.address': typeof addressLex 114 115 'community.lexicon.location.geo': typeof geoLex 115 116 'community.lexicon.location.hthree': typeof hthreeLex 117 + 'dev.hatk.applyWrites': typeof applyWritesLex 116 118 'dev.hatk.createRecord': typeof createRecordLex 117 119 'dev.hatk.createReport': typeof createReportLex 118 120 'dev.hatk.deleteRecord': typeof deleteRecordLex ··· 177 179 export type Threadgate = Prettify<LexRecord<typeof threadgateLex, Registry>> 178 180 export type BskyGraphFollow = Prettify<LexRecord<typeof bskyGraphFollowLex, Registry>> 179 181 export type Declaration = Prettify<LexRecord<typeof declarationLex, Registry>> 182 + export type ApplyWrites = Prettify<LexProcedure<typeof applyWritesLex, Registry>> 180 183 export type CreateReport = Prettify<LexProcedure<typeof createReportLex, Registry>> 181 184 export type DescribeCollections = Prettify<LexQuery<typeof describeCollectionsLex, Registry>> 182 185 export type DescribeFeeds = Prettify<LexQuery<typeof describeFeedsLex, Registry>> ··· 365 368 export type LabelValueDefinition = Prettify<LexDef<typeof atprotoLabelDefsLex, 'labelValueDefinition', Registry>> 366 369 export type LabelValueDefinitionStrings = Prettify<LexDef<typeof atprotoLabelDefsLex, 'labelValueDefinitionStrings', Registry>> 367 370 export type DeclarationMessageMe = Prettify<LexDef<typeof declarationLex, 'messageMe', Registry>> 371 + export type Create = Prettify<LexDef<typeof applyWritesLex, 'create', Registry>> 372 + export type Update = Prettify<LexDef<typeof applyWritesLex, 'update', Registry>> 373 + export type Delete = Prettify<LexDef<typeof applyWritesLex, 'delete', Registry>> 374 + export type CreateResult = Prettify<LexDef<typeof applyWritesLex, 'createResult', Registry>> 375 + export type UpdateResult = Prettify<LexDef<typeof applyWritesLex, 'updateResult', Registry>> 376 + export type DeleteResult = Prettify<LexDef<typeof applyWritesLex, 'deleteResult', Registry>> 368 377 export type RepoRef = Prettify<LexDef<typeof createReportLex, 'repoRef', Registry>> 369 378 export type LabelDefinition = Prettify<LexDef<typeof describeLabelsLex, 'labelDefinition', Registry>> 370 379 export type LabelLocale = Prettify<LexDef<typeof describeLabelsLex, 'labelLocale', Registry>> ··· 404 413 // ─── XRPC Schema ──────────────────────────────────────────────────── 405 414 406 415 export type XrpcSchema = { 416 + 'dev.hatk.applyWrites': ApplyWrites 407 417 'dev.hatk.createRecord': CreateRecord 408 418 'dev.hatk.createReport': CreateReport 409 419 'dev.hatk.deleteRecord': DeleteRecord
+87
lexicons/dev/hatk/applyWrites.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.applyWrites", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Apply multiple record writes in a single atomic PDS transaction.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["writes"], 13 + "properties": { 14 + "writes": { 15 + "type": "array", 16 + "items": { 17 + "type": "union", 18 + "refs": ["#create", "#update", "#delete"] 19 + } 20 + } 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "properties": { 29 + "results": { 30 + "type": "array", 31 + "items": { 32 + "type": "union", 33 + "refs": ["#createResult", "#updateResult", "#deleteResult"] 34 + } 35 + } 36 + } 37 + } 38 + } 39 + }, 40 + "create": { 41 + "type": "object", 42 + "required": ["collection", "value"], 43 + "properties": { 44 + "collection": { "type": "string" }, 45 + "rkey": { "type": "string" }, 46 + "value": { "type": "unknown" } 47 + } 48 + }, 49 + "update": { 50 + "type": "object", 51 + "required": ["collection", "rkey", "value"], 52 + "properties": { 53 + "collection": { "type": "string" }, 54 + "rkey": { "type": "string" }, 55 + "value": { "type": "unknown" } 56 + } 57 + }, 58 + "delete": { 59 + "type": "object", 60 + "required": ["collection", "rkey"], 61 + "properties": { 62 + "collection": { "type": "string" }, 63 + "rkey": { "type": "string" } 64 + } 65 + }, 66 + "createResult": { 67 + "type": "object", 68 + "required": ["uri", "cid"], 69 + "properties": { 70 + "uri": { "type": "string", "format": "at-uri" }, 71 + "cid": { "type": "string", "format": "cid" } 72 + } 73 + }, 74 + "updateResult": { 75 + "type": "object", 76 + "required": ["uri", "cid"], 77 + "properties": { 78 + "uri": { "type": "string", "format": "at-uri" }, 79 + "cid": { "type": "string", "format": "cid" } 80 + } 81 + }, 82 + "deleteResult": { 83 + "type": "object", 84 + "properties": {} 85 + } 86 + } 87 + }
+4 -4
package-lock.json
··· 6 6 "": { 7 7 "name": "grain", 8 8 "dependencies": { 9 - "@hatk/hatk": "^0.0.1-alpha.55", 9 + "@hatk/hatk": "^0.0.1-alpha.57", 10 10 "@sveltejs/adapter-node": "^5.5.4", 11 11 "@sveltejs/kit": "^2.55.0", 12 12 "@tanstack/svelte-query": "^6.1.0", ··· 164 164 } 165 165 }, 166 166 "node_modules/@hatk/hatk": { 167 - "version": "0.0.1-alpha.55", 168 - "resolved": "https://registry.npmjs.org/@hatk/hatk/-/hatk-0.0.1-alpha.55.tgz", 169 - "integrity": "sha512-N4hTZyYQlJcHtRV2BLwqRDj49qtG6Tv4sBZPV3fSUps3DIvW75plq6KdWdsO8VCCad9zlsWHYJ2RXR+9eogxIA==", 167 + "version": "0.0.1-alpha.57", 168 + "resolved": "https://registry.npmjs.org/@hatk/hatk/-/hatk-0.0.1-alpha.57.tgz", 169 + "integrity": "sha512-S0I4+83UMblcVjJF/rsvfhp+amKFIDEmJdpZJa8HqYJB3D9w5UDsFirdh0otiFqHaAoKsorWb0+sizb1bHp36Q==", 170 170 "license": "MIT", 171 171 "dependencies": { 172 172 "@bigmoves/lexicon": "^0.2.2",
+1 -1
package.json
··· 12 12 "test:browser": "npx playwright test" 13 13 }, 14 14 "dependencies": { 15 - "@hatk/hatk": "^0.0.1-alpha.55", 15 + "@hatk/hatk": "^0.0.1-alpha.57", 16 16 "@sveltejs/adapter-node": "^5.5.4", 17 17 "@sveltejs/kit": "^2.55.0", 18 18 "@tanstack/svelte-query": "^6.1.0",