experiments in a post-browser web
10
fork

Configure Feed

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

feat(tile-ipc): datastore strict-shim completion sweep (Phase 3.5g)

Audit table — legacy channel → strict shim mapping:
datastore-add-address → tile:datastore:add-address (NEW)
datastore-get-address → tile:datastore:get-address (NEW)
datastore-update-address → tile:datastore:update-address (NEW)
datastore-query-addresses → tile:datastore:query-addresses (NEW)
datastore-add-visit → tile:datastore:add-visit (NEW)
datastore-query-visits → tile:datastore:query-visits (NEW)
datastore-add-content → tile:datastore:add-content (NEW)
datastore-query-content → tile:datastore:query-content (NEW)
datastore-tag-address → tile:datastore:tag-address (NEW)
datastore-untag-address → tile:datastore:untag-address (NEW)
datastore-get-address-tags → tile:datastore:get-address-tags (NEW)
datastore-get-addresses-by-tag → tile:datastore:get-addresses-by-tag (NEW)
datastore-get-untagged-addresses → tile:datastore:get-untagged-addresses (NEW)
datastore-get-table → tile:datastore:get-table (pre-existing)
datastore-set-row → tile:datastore:set-row (pre-existing)
datastore-get-row → tile:datastore:get-row (pre-existing)
datastore-get-stats → tile:datastore:get-stats (pre-existing)
datastore-get-or-create-tag → tile:datastore:get-or-create-tag (pre-existing)
datastore-tag-address (item) → tile:datastore:tag-item (pre-existing)
datastore-untag-item → tile:datastore:untag-item (pre-existing)
datastore-get-tags-by-frecency → tile:datastore:get-tags-by-frecency (pre-existing)
datastore-rename-tag → tile:datastore:rename-tag (pre-existing)
datastore-update-tag-color → tile:datastore:update-tag-color (pre-existing)
datastore-delete-tag → tile:datastore:delete-tag (pre-existing)
datastore-add-item → tile:datastore:add-item (pre-existing)
datastore-get-item → tile:datastore:get-item (pre-existing)
datastore-update-item → tile:datastore:update-item (pre-existing)
datastore-delete-item → tile:datastore:delete-item (pre-existing)
datastore-hard-delete-item → tile:datastore:hard-delete-item (pre-existing)
datastore-update-item-title → tile:datastore:update-item-title (pre-existing)
datastore-update-item-favicon → tile:datastore:update-item-favicon (pre-existing)
datastore-query-items → tile:datastore:query-items (pre-existing)
datastore-get-item-tags → tile:datastore:get-item-tags (pre-existing)
datastore-get-items-by-tag → tile:datastore:get-items-by-tag (pre-existing)
datastore-get-history → tile:datastore:get-history (pre-existing)
datastore-record-item-visit → tile:datastore:record-item-visit (pre-existing)
datastore-get-item-visits → tile:datastore:get-item-visits (pre-existing)
datastore-query-item-visits → tile:datastore:query-item-visits (pre-existing)
datastore-track-navigation → tile:datastore:track-navigation (pre-existing)
datastore-add-item-event → tile:datastore:add-item-event (pre-existing)
datastore-get-item-event → tile:datastore:get-item-event (pre-existing)
datastore-query-item-events → tile:datastore:query-item-events (pre-existing)
datastore-delete-item-event → tile:datastore:delete-item-event (pre-existing)
datastore-delete-item-events → tile:datastore:delete-item-events (pre-existing)
datastore-get-latest-item-event → tile:datastore:get-latest-item-event (pre-existing)
datastore-count-item-events → tile:datastore:count-item-events (pre-existing)

Gaps filled: 13 (all address/visit/content/tag-address compat channels).
Dead-channel candidates: none — all 46 legacy channels have callers.

Changes:
- tile-ipc.ts: add _itemToAddress() helper, import dsAddContent/dsQueryContent
and dsTagItemAndPublish; add 13 new ipcMain.handle() shims under
"Address compat shims" section; capability-gate with ['items'],
['item_visits'], ['content'], or ['tags','item_tags'] as appropriate.
- tile-preload.cts: migrate 13 wrapper methods off legacy datastore-* channels
onto the new tile:datastore:* strict channels (pass tileToken).
- tsc --noEmit: clean.

+313 -15
+298
backend/electron/tile-ipc.ts
··· 72 72 deleteItemEvent as dsDeleteItemEvent, 73 73 deleteItemEvents as dsDeleteItemEvents, 74 74 getItemEvent as dsGetItemEvent, 75 + addContent as dsAddContent, 76 + queryContent as dsQueryContent, 75 77 addContextEntry, 76 78 getContextEntry, 77 79 queryContextHistory, ··· 80 82 getWindowsMatchingContext, 81 83 normalizeUrl, 82 84 } from './datastore.js'; 85 + import { tagItemAndPublish as dsTagItemAndPublish } from './tag-events.js'; 83 86 import type { TableName } from '../types/index.js'; 84 87 import { installFromBundle } from './feature-installer.js'; 85 88 import { resolveCapabilities, validateTileManifest, detectManifestVersion } from './tile-manifest.js'; ··· 160 163 function handleDatastoreViolation(token: string | undefined, op: string, reason: string): void { 161 164 const grant = token ? getGrantForToken(token) : null; 162 165 handleViolation(grant, 'datastore', op, reason, token); 166 + } 167 + 168 + /** 169 + * Convert an Item to an Address-compatible shape for backward-compat shims. 170 + * Mirrors ipc.ts::itemToAddress — kept local so tile-ipc has no hard dep on ipc.ts. 171 + */ 172 + function _itemToAddress(item: import('../types/index.js').Item): Record<string, unknown> { 173 + let protocol = ''; 174 + let urlPath = ''; 175 + try { 176 + const u = new URL(String(item.content || '')); 177 + protocol = u.protocol.replace(':', ''); 178 + urlPath = u.pathname; 179 + } catch { /* not a valid URL */ } 180 + return { 181 + id: item.id, 182 + uri: item.content || '', 183 + protocol, 184 + domain: item.domain || '', 185 + path: urlPath, 186 + title: item.title || '', 187 + mimeType: item.mimeType || 'text/html', 188 + favicon: item.favicon || '', 189 + description: '', 190 + tags: '', 191 + metadata: item.metadata || '{}', 192 + createdAt: item.createdAt, 193 + updatedAt: item.updatedAt, 194 + lastVisitAt: item.lastVisitAt || 0, 195 + visitCount: item.visitCount || 0, 196 + starred: item.starred || 0, 197 + archived: item.archived || 0, 198 + }; 163 199 } 164 200 165 201 /** ··· 4485 4521 try { 4486 4522 const data = dsQueryItemsByFrecency(args.filter as Parameters<typeof dsQueryItemsByFrecency>[0]); 4487 4523 return { success: true, data }; 4524 + } catch (error) { 4525 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4526 + } 4527 + }); 4528 + 4529 + // ── Address compat shims (redirect to items + backward-compat shape) ── 4530 + 4531 + ipcMain.handle('tile:datastore:add-address', async (_event, args: { 4532 + token: string; 4533 + uri: string; 4534 + options?: Record<string, unknown>; 4535 + }) => { 4536 + const check = validateTileDatastoreRequest(args?.token, ['items']); 4537 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:add-address', check.error); return { error: check.error }; } 4538 + try { 4539 + const opts = args.options || {}; 4540 + const normalizedUri = normalizeUrl(args.uri); 4541 + const metadata: Record<string, unknown> = {}; 4542 + if (opts.title) metadata.title = opts.title; 4543 + if (opts.description) metadata.description = opts.description; 4544 + if (opts.favicon) metadata.favicon = opts.favicon; 4545 + const result = dsAddItem('url', { 4546 + content: normalizedUri, 4547 + mimeType: (opts.mimeType as string) || 'text/html', 4548 + metadata: JSON.stringify(metadata), 4549 + starred: (opts.starred as number) || 0, 4550 + archived: (opts.archived as number) || 0, 4551 + }); 4552 + if (opts.favicon) { 4553 + getDb().prepare('UPDATE items SET favicon = ? WHERE id = ?').run(opts.favicon as string, result.id); 4554 + } 4555 + publish('system', scopes.GLOBAL, 'item:created', { 4556 + itemId: result.id, itemType: 'url', content: normalizedUri, 4557 + }); 4558 + return { success: true, data: result, id: result.id }; 4559 + } catch (error) { 4560 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4561 + } 4562 + }); 4563 + 4564 + ipcMain.handle('tile:datastore:get-address', async (_event, args: { 4565 + token: string; 4566 + id: string; 4567 + }) => { 4568 + const check = validateTileDatastoreRequest(args?.token, ['items']); 4569 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:get-address', check.error); return { error: check.error }; } 4570 + try { 4571 + const item = dsGetItem(args.id); 4572 + if (!item) return { success: true, data: undefined }; 4573 + return { success: true, data: _itemToAddress(item) }; 4574 + } catch (error) { 4575 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4576 + } 4577 + }); 4578 + 4579 + ipcMain.handle('tile:datastore:update-address', async (_event, args: { 4580 + token: string; 4581 + id: string; 4582 + updates: Record<string, unknown>; 4583 + }) => { 4584 + const check = validateTileDatastoreRequest(args?.token, ['items']); 4585 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:update-address', check.error); return { error: check.error }; } 4586 + try { 4587 + const updates = args.updates || {}; 4588 + const itemOpts: Record<string, unknown> = {}; 4589 + if (updates.metadata !== undefined) itemOpts.metadata = updates.metadata; 4590 + if (updates.starred !== undefined) itemOpts.starred = updates.starred; 4591 + if (updates.archived !== undefined) itemOpts.archived = updates.archived; 4592 + if (updates.mimeType !== undefined) itemOpts.mimeType = updates.mimeType; 4593 + if (updates.uri !== undefined) itemOpts.content = updates.uri; 4594 + dsUpdateItem(args.id, itemOpts); 4595 + if (updates.title !== undefined) { 4596 + getDb().prepare('UPDATE items SET title = ?, updatedAt = ? WHERE id = ?').run(updates.title as string, Date.now(), args.id); 4597 + } 4598 + if (updates.favicon !== undefined) { 4599 + getDb().prepare('UPDATE items SET favicon = ?, updatedAt = ? WHERE id = ?').run(updates.favicon as string, Date.now(), args.id); 4600 + } 4601 + const updated = dsGetItem(args.id); 4602 + return { success: true, data: updated ? _itemToAddress(updated) : undefined }; 4603 + } catch (error) { 4604 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4605 + } 4606 + }); 4607 + 4608 + ipcMain.handle('tile:datastore:query-addresses', async (_event, args: { 4609 + token: string; 4610 + filter?: Record<string, unknown>; 4611 + }) => { 4612 + const check = validateTileDatastoreRequest(args?.token, ['items']); 4613 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:query-addresses', check.error); return { error: check.error }; } 4614 + try { 4615 + const filter = args.filter || {}; 4616 + const itemFilter: Record<string, unknown> = { type: 'url' }; 4617 + if (filter.domain) itemFilter.domain = filter.domain; 4618 + if (filter.starred !== undefined) itemFilter.starred = filter.starred; 4619 + if (filter.sortBy) itemFilter.sortBy = filter.sortBy; 4620 + if (filter.limit) itemFilter.limit = filter.limit; 4621 + if (filter.search) itemFilter.search = filter.search; 4622 + const items = dsQueryItems(itemFilter); 4623 + return { success: true, data: items.map(_itemToAddress) }; 4624 + } catch (error) { 4625 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4626 + } 4627 + }); 4628 + 4629 + ipcMain.handle('tile:datastore:add-visit', async (_event, args: { 4630 + token: string; 4631 + addressId: string; 4632 + options?: Record<string, unknown>; 4633 + }) => { 4634 + const check = validateTileDatastoreRequest(args?.token, ['item_visits']); 4635 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:add-visit', check.error); return { error: check.error }; } 4636 + try { 4637 + const opts = args.options || {}; 4638 + const result = dsRecordItemVisit(args.addressId, { 4639 + source: (opts.source as string) || 'direct', 4640 + sourceId: (opts.sourceId as string) || '', 4641 + windowType: (opts.windowType as string) || 'main', 4642 + interacted: (opts.interacted as number) || 0, 4643 + }); 4644 + return { success: true, data: result, id: result.id }; 4645 + } catch (error) { 4646 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4647 + } 4648 + }); 4649 + 4650 + ipcMain.handle('tile:datastore:query-visits', async (_event, args: { 4651 + token: string; 4652 + filter?: Record<string, unknown>; 4653 + }) => { 4654 + const check = validateTileDatastoreRequest(args?.token, ['item_visits']); 4655 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:query-visits', check.error); return { error: check.error }; } 4656 + try { 4657 + const filter = args.filter || {}; 4658 + const itemFilter: Record<string, unknown> = {}; 4659 + if (filter.addressId) itemFilter.itemId = filter.addressId; 4660 + if (filter.source) itemFilter.source = filter.source; 4661 + if (filter.since) itemFilter.since = filter.since; 4662 + if (filter.until) itemFilter.until = filter.until; 4663 + if (filter.limit) itemFilter.limit = filter.limit; 4664 + const visits = dsQueryItemVisits(itemFilter); 4665 + const mapped = visits.map(v => ({ ...v, addressId: v.itemId })); 4666 + return { success: true, data: mapped }; 4667 + } catch (error) { 4668 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4669 + } 4670 + }); 4671 + 4672 + ipcMain.handle('tile:datastore:add-content', async (_event, args: { 4673 + token: string; 4674 + options?: Record<string, unknown>; 4675 + }) => { 4676 + const check = validateTileDatastoreRequest(args?.token, ['content']); 4677 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:add-content', check.error); return { error: check.error }; } 4678 + try { 4679 + const data = dsAddContent(args.options as Parameters<typeof dsAddContent>[0]); 4680 + return { success: true, data }; 4681 + } catch (error) { 4682 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4683 + } 4684 + }); 4685 + 4686 + ipcMain.handle('tile:datastore:query-content', async (_event, args: { 4687 + token: string; 4688 + filter?: Record<string, unknown>; 4689 + }) => { 4690 + const check = validateTileDatastoreRequest(args?.token, ['content']); 4691 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:query-content', check.error); return { error: check.error }; } 4692 + try { 4693 + const data = dsQueryContent(args.filter as Parameters<typeof dsQueryContent>[0]); 4694 + return { success: true, data }; 4695 + } catch (error) { 4696 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4697 + } 4698 + }); 4699 + 4700 + ipcMain.handle('tile:datastore:tag-address', async (_event, args: { 4701 + token: string; 4702 + addressId: string; 4703 + tagId: string; 4704 + }) => { 4705 + const check = validateTileDatastoreRequest(args?.token, ['tags', 'item_tags']); 4706 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:tag-address', check.error); return { error: check.error }; } 4707 + try { 4708 + const result = dsTagItemAndPublish(args.addressId, args.tagId); 4709 + if (!result.alreadyExists) { 4710 + const tag = getDb().prepare('SELECT name FROM tags WHERE id = ?').get(args.tagId) as { name: string } | undefined; 4711 + publish('system', scopes.GLOBAL, 'tag:address-added', { 4712 + tagId: args.tagId, tagName: tag?.name, addressId: args.addressId, 4713 + }); 4714 + } 4715 + return { success: true, data: result }; 4716 + } catch (error) { 4717 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4718 + } 4719 + }); 4720 + 4721 + ipcMain.handle('tile:datastore:untag-address', async (_event, args: { 4722 + token: string; 4723 + addressId: string; 4724 + tagId: string; 4725 + }) => { 4726 + const check = validateTileDatastoreRequest(args?.token, ['tags', 'item_tags']); 4727 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:untag-address', check.error); return { error: check.error }; } 4728 + try { 4729 + const tag = getDb().prepare('SELECT name FROM tags WHERE id = ?').get(args.tagId) as { name: string } | undefined; 4730 + const result = dsUntagItem(args.addressId, args.tagId); 4731 + if (result) { 4732 + publish('system', scopes.GLOBAL, 'tag:address-removed', { 4733 + tagId: args.tagId, tagName: tag?.name, addressId: args.addressId, 4734 + }); 4735 + publish('system', scopes.GLOBAL, 'tag:item-removed', { 4736 + tagId: args.tagId, tagName: tag?.name, itemId: args.addressId, 4737 + }); 4738 + } 4739 + return { success: true, data: result }; 4740 + } catch (error) { 4741 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4742 + } 4743 + }); 4744 + 4745 + ipcMain.handle('tile:datastore:get-address-tags', async (_event, args: { 4746 + token: string; 4747 + addressId: string; 4748 + }) => { 4749 + const check = validateTileDatastoreRequest(args?.token, ['tags', 'item_tags']); 4750 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:get-address-tags', check.error); return { error: check.error }; } 4751 + try { 4752 + const data = dsGetItemTags(args.addressId); 4753 + return { success: true, data }; 4754 + } catch (error) { 4755 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4756 + } 4757 + }); 4758 + 4759 + ipcMain.handle('tile:datastore:get-addresses-by-tag', async (_event, args: { 4760 + token: string; 4761 + tagId: string; 4762 + }) => { 4763 + const check = validateTileDatastoreRequest(args?.token, ['items', 'item_tags']); 4764 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:get-addresses-by-tag', check.error); return { error: check.error }; } 4765 + try { 4766 + const items = dsGetItemsByTag(args.tagId); 4767 + return { success: true, data: items.map(_itemToAddress) }; 4768 + } catch (error) { 4769 + return { success: false, error: error instanceof Error ? error.message : String(error) }; 4770 + } 4771 + }); 4772 + 4773 + ipcMain.handle('tile:datastore:get-untagged-addresses', async (_event, args: { 4774 + token: string; 4775 + }) => { 4776 + const check = validateTileDatastoreRequest(args?.token, ['items', 'item_tags']); 4777 + if ('error' in check) { handleDatastoreViolation(args?.token, 'datastore:get-untagged-addresses', check.error); return { error: check.error }; } 4778 + try { 4779 + const items = getDb().prepare(` 4780 + SELECT i.* FROM items i 4781 + LEFT JOIN item_tags it ON i.id = it.itemId 4782 + WHERE i.type = 'url' AND i.deletedAt = 0 AND it.id IS NULL 4783 + ORDER BY i.visitCount DESC 4784 + `).all() as import('../types/index.js').Item[]; 4785 + return { success: true, data: items.map(_itemToAddress) }; 4488 4786 } catch (error) { 4489 4787 return { success: false, error: error instanceof Error ? error.message : String(error) }; 4490 4788 }
+15 -15
backend/electron/tile-preload.cts
··· 906 906 ...datastoreStrict, 907 907 // Address helpers 908 908 addAddress: (uri: string, options?: unknown) => 909 - ipcRenderer.invoke('datastore-add-address', { uri, options }), 909 + ipcRenderer.invoke('tile:datastore:add-address', { token: tileToken, uri, options }), 910 910 getAddress: (id: string) => 911 - ipcRenderer.invoke('datastore-get-address', { id }), 911 + ipcRenderer.invoke('tile:datastore:get-address', { token: tileToken, id }), 912 912 updateAddress: (id: string, updates: unknown) => 913 - ipcRenderer.invoke('datastore-update-address', { id, updates }), 913 + ipcRenderer.invoke('tile:datastore:update-address', { token: tileToken, id, updates }), 914 914 queryAddresses: (filter?: unknown) => 915 - ipcRenderer.invoke('datastore-query-addresses', { filter }), 915 + ipcRenderer.invoke('tile:datastore:query-addresses', { token: tileToken, filter }), 916 916 addVisit: (addressId: string, options?: unknown) => 917 - ipcRenderer.invoke('datastore-add-visit', { addressId, options }), 917 + ipcRenderer.invoke('tile:datastore:add-visit', { token: tileToken, addressId, options }), 918 918 queryVisits: (filter?: unknown) => 919 - ipcRenderer.invoke('datastore-query-visits', { filter }), 919 + ipcRenderer.invoke('tile:datastore:query-visits', { token: tileToken, filter }), 920 920 addContent: (options: unknown) => 921 - ipcRenderer.invoke('datastore-add-content', { options }), 921 + ipcRenderer.invoke('tile:datastore:add-content', { token: tileToken, options }), 922 922 queryContent: (filter?: unknown) => 923 - ipcRenderer.invoke('datastore-query-content', { filter }), 923 + ipcRenderer.invoke('tile:datastore:query-content', { token: tileToken, filter }), 924 924 getTable: (tableName: string) => 925 925 ipcRenderer.invoke('datastore-get-table', { tableName }), 926 926 setRow: (tableName: string, rowId: string, rowData: unknown) => ··· 930 930 getStats: () => ipcRenderer.invoke('tile:datastore:get-stats', { token: tileToken }), 931 931 // Tag helpers 932 932 getOrCreateTag: (name: string) => 933 - ipcRenderer.invoke('datastore-get-or-create-tag', { name }), 933 + ipcRenderer.invoke('tile:datastore:get-or-create-tag', { token: tileToken, name }), 934 934 tagAddress: (addressId: string, tagId: string) => 935 - ipcRenderer.invoke('datastore-tag-address', { addressId, tagId }), 935 + ipcRenderer.invoke('tile:datastore:tag-address', { token: tileToken, addressId, tagId }), 936 936 untagAddress: (addressId: string, tagId: string) => 937 - ipcRenderer.invoke('datastore-untag-address', { addressId, tagId }), 937 + ipcRenderer.invoke('tile:datastore:untag-address', { token: tileToken, addressId, tagId }), 938 938 getTagsByFrecency: (domain?: string) => 939 - ipcRenderer.invoke('datastore-get-tags-by-frecency', { domain }), 939 + ipcRenderer.invoke('tile:datastore:get-tags-by-frecency', { token: tileToken, domain }), 940 940 renameTag: (tagId: string, newName: string) => 941 941 ipcRenderer.invoke('tile:datastore:rename-tag', { token: tileToken, tagId, newName }), 942 942 updateTagColor: (tagId: string, color: string) => ··· 944 944 deleteTag: (tagId: string) => 945 945 ipcRenderer.invoke('tile:datastore:delete-tag', { token: tileToken, tagId }), 946 946 getAddressTags: (addressId: string) => 947 - ipcRenderer.invoke('datastore-get-address-tags', { addressId }), 947 + ipcRenderer.invoke('tile:datastore:get-address-tags', { token: tileToken, addressId }), 948 948 getAddressesByTag: (tagId: string) => 949 - ipcRenderer.invoke('datastore-get-addresses-by-tag', { tagId }), 949 + ipcRenderer.invoke('tile:datastore:get-addresses-by-tag', { token: tileToken, tagId }), 950 950 getUntaggedAddresses: () => 951 - ipcRenderer.invoke('datastore-get-untagged-addresses', {}), 951 + ipcRenderer.invoke('tile:datastore:get-untagged-addresses', { token: tileToken }), 952 952 // Item helpers 953 953 addItem: (type: string, options: unknown = {}) => 954 954 ipcRenderer.invoke('datastore-add-item', { type, options }),