Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/components/icon/icon-state.broadcast.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createIconDefaultMap } from './registry/default-map.js';
import type {
BroadcastIconsChangeMessage,
IconMeta,
Expand Down Expand Up @@ -105,13 +104,16 @@ export class IconsStateBroadcast {

private _getUserRefsCollection(
collections: IconsCollection<IconMeta>
): IconsCollection<IconMeta> {
const userSetIcons = createIconDefaultMap<string, IconMeta>();
): Map<string, Map<string, IconMeta>> {
const userSetIcons = new Map<string, Map<string, IconMeta>>();

for (const [collectionKey, collection] of collections.entries()) {
for (const [iconKey, icon] of collection.entries()) {
if (icon.external) {
userSetIcons.getOrCreate(collectionKey).set(iconKey, icon);
if (!userSetIcons.has(collectionKey)) {
userSetIcons.set(collectionKey, new Map());
}
userSetIcons.get(collectionKey)!.set(iconKey, icon);
}
}
}
Expand All @@ -121,15 +123,18 @@ export class IconsStateBroadcast {

private _getUserSetCollection(
collections: IconsCollection<SvgIcon>
): IconsCollection<SvgIcon> {
const userSetIcons = createIconDefaultMap<string, SvgIcon>();
): Map<string, Map<string, SvgIcon>> {
const userSetIcons = new Map<string, Map<string, SvgIcon>>();

for (const [collectionKey, collection] of collections.entries()) {
if (collectionKey === 'internal') {
continue;
}
for (const [iconKey, icon] of collection.entries()) {
userSetIcons.getOrCreate(collectionKey).set(iconKey, icon);
if (!userSetIcons.has(collectionKey)) {
userSetIcons.set(collectionKey, new Map());
}
userSetIcons.get(collectionKey)!.set(iconKey, icon);
}
}

Expand Down
22 changes: 15 additions & 7 deletions src/components/icon/icon.registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ class IconsRegistry {
const svgIcon = this._svgIconParser.parse(iconText);
this._collections.getOrCreate(collection).set(name, svgIcon);

const icons = createIconDefaultMap<string, SvgIcon>();
icons.getOrCreate(collection).set(name, svgIcon);
const icons = new Map<string, Map<string, SvgIcon>>();
icons.set(collection, new Map([[name, svgIcon]]));

this._broadcast.send({
actionType: ActionType.RegisterIcon,
Expand Down Expand Up @@ -140,11 +140,19 @@ class IconsRegistry {
this._notifyAll(alias.name, alias.collection);
}
if (target.external) {
const refs = createIconDefaultMap<string, IconMeta>();
refs.getOrCreate(alias.collection).set(alias.name, {
name: target.name,
collection: target.collection,
});
const refs = new Map<string, Map<string, IconMeta>>();
refs.set(
alias.collection,
new Map([
[
alias.name,
{
name: target.name,
collection: target.collection,
},
],
])
);

this._broadcast.send({
actionType: ActionType.UpdateIconReference,
Expand Down
60 changes: 60 additions & 0 deletions src/components/icon/icon.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,66 @@ describe('Icon broadcast service', () => {
expect(references?.get(refCollectionName)?.get(refName)).to.be.undefined;
});
});

describe('Structured cloneability (Safari compatibility)', () => {
it('broadcast payloads for registered icons are structured-cloneable', async () => {
registerIconFromText('clone-icon', bugSvg, collectionName);
await aTimeout(0);

expect(events).lengthOf(1);
const { collections } = first(events).data;

// Verify the payload is a plain Map (not DefaultMap with non-cloneable _factoryFn)
expect(collections).to.be.instanceOf(Map);
expect(collections!.constructor).to.equal(Map);
expect(() => structuredClone(collections)).to.not.throw();
});

it('broadcast payloads for icon references are structured-cloneable', async () => {
registerIconFromText('ref-clone-target', bugSvg, collectionName);
setIconRef('ref-clone-alias', collectionName, {
name: 'ref-clone-target',
collection: collectionName,
});
await aTimeout(0);

const refEvent = events.find(
(e) => e.data.actionType === ActionType.UpdateIconReference
);
expect(refEvent).to.not.be.undefined;

const { references } = refEvent!.data;

// Verify the payload is a plain Map (not DefaultMap with non-cloneable _factoryFn)
expect(references).to.be.instanceOf(Map);
expect(references!.constructor).to.equal(Map);
expect(() => structuredClone(references)).to.not.throw();
});

it('broadcast payloads for sync state are structured-cloneable', async () => {
registerIconFromText('sync-clone-icon', bugSvg, collectionName);

// a peer is requesting a state sync
channel.postMessage({ actionType: ActionType.SyncState });
await aTimeout(0);

const syncEvent = events.find(
(e) => e.data.actionType === ActionType.SyncState
);
expect(syncEvent).to.not.be.undefined;

const { collections, references } = syncEvent!.data;

// Both collections and references must be plain Maps
expect(collections).to.be.instanceOf(Map);
expect(collections!.constructor).to.equal(Map);
expect(() => structuredClone(collections)).to.not.throw();

expect(references).to.be.instanceOf(Map);
expect(references!.constructor).to.equal(Map);
expect(() => structuredClone(references)).to.not.throw();
});
});
});

describe('Icon BFCache (pageshow/pagehide) handling', () => {
Expand Down